1

Nvidia は、ホストとデバイス間の帯域幅をプロファイリングする方法の例を提供しています。コードはhttps://developer.nvidia.com/opencl (「帯域幅」を検索)で見つけることができます。実験は、Ubuntu 12.04 64 ビット コンピューターで実行されます。固定されたメモリとマップされたアクセス モードを調べています。これは呼び出しでテストできます: ./bandwidthtest --memory=pinned --access=mapped

ホストからデバイスへの帯域幅のコア テスト ループは、736 ~ 748 行付近にあります。また、それらをここにリストし、いくつかのコメントとコンテキスト コードを追加します。

    //create a buffer cmPinnedData in host
    cmPinnedData = clCreateBuffer(cxGPUContext, CL_MEM_READ_WRITE | CL_MEM_ALLOC_HOST_PTR, memSize, NULL, &ciErrNum);

    ....(initialize cmPinnedData with some data)....

    //create a buffer in device
    cmDevData = clCreateBuffer(cxGPUContext, CL_MEM_READ_WRITE, memSize, NULL, &ciErrNum);

    // get pointer mapped to host buffer cmPinnedData
    h_data = (unsigned char*)clEnqueueMapBuffer(cqCommandQueue, cmPinnedData, CL_TRUE, CL_MAP_READ, 0, memSize, 0, NULL, NULL, &ciErrNum);

    // get pointer mapped to device buffer cmDevData
    void* dm_idata = clEnqueueMapBuffer(cqCommandQueue, cmDevData, CL_TRUE, CL_MAP_WRITE, 0, memSize, 0, NULL, NULL, &ciErrNum);

    // copy data from host to device by memcpy
    for(unsigned int i = 0; i < MEMCOPY_ITERATIONS; i++)
    {
        memcpy(dm_idata, h_data, memSize);
    }
    //unmap device buffer.
    ciErrNum = clEnqueueUnmapMemObject(cqCommandQueue, cmDevData, dm_idata, 0, NULL, NULL);

転送サイズが 33.5MB の場合、測定されたホストからデバイスへの帯域幅は 6430.0MB/s です。./bandwidthtest --memory=pinned --access=mapped --mode=range --start=1000000 --end=1000000 --increment=1000000 (MEMCOPY_ITERATIONS が 100 からタイマーがあまり正確でない場合は 10000 です。) 報告された帯域幅は 12540.5MB/s になります。

PCI-e x16 Gen2 インターフェイスの最大帯域幅が 8000MB/s であることは誰もが知っています。したがって、プロファイリング方法に問題があるとは思えません。

コア プロファイリング コードをもう一度確認しましょう。

    // get pointer mapped to device buffer cmDevData
    void* dm_idata = clEnqueueMapBuffer(cqCommandQueue, cmDevData, CL_TRUE, CL_MAP_WRITE, 0, memSize, 0, NULL, NULL, &ciErrNum);

    // copy data from host to device by memcpy
    for(unsigned int i = 0; i < MEMCOPY_ITERATIONS; i++)
    {
        memcpy(dm_idata, h_data, memSize);
        //can we call kernel after memcpy? I don't think so.
    }
    //unmap device buffer.
    ciErrNum = clEnqueueUnmapMemObject(cqCommandQueue, cmDevData, dm_idata, 0, NULL, NULL);

問題は、ループ内に明示的な同期 API がないため、データが実際にデバイスに転送されたことを memcpy が許可できないことだと思います。そのため、memcpy の後にカーネルを呼び出そうとすると、カーネルは有効なデータを取得する場合と取得しない場合があります。

プロファイリング ループ内でマップおよびマップ解除操作を行う場合、マップ解除操作の後にカーネルを安全に呼び出すことができると思います。この操作により、データが安全にデバイスにあることが保証されるからです。新しいコードは次のとおりです。

// copy data from host to device by memcpy
for(unsigned int i = 0; i < MEMCOPY_ITERATIONS; i++)
{
    // get pointer mapped to device buffer cmDevData
    void* dm_idata = clEnqueueMapBuffer(cqCommandQueue, cmDevData, CL_TRUE, CL_MAP_WRITE, 0, memSize, 0, NULL, NULL, &ciErrNum);

    memcpy(dm_idata, h_data, memSize);

    //unmap device buffer.
    ciErrNum = clEnqueueUnmapMemObject(cqCommandQueue, cmDevData, dm_idata, 0, NULL, NULL);

    //we can call kernel here safely?
}

しかし、この新しいプロファイリング方法を使用すると、レポートされる帯域幅は非常に低くなり、915.2MB/s@block-size-33.5MB になります。881.9MB/s@ブロックサイズ-1MB。マップおよびマップ解除操作のオーバーヘッドは、「ゼロコピー」宣言ほど小さくないようです。

この map-unmap は、clEnqueueWriteBuffer() の通常の方法を使用して得られる 2909.6MB/s@block-size-33.5MB よりもさらに遅くなります。

    for(unsigned int i = 0; i < MEMCOPY_ITERATIONS; i++)
    {
        clEnqueueWriteBuffer(cqCommandQueue, cmDevData, CL_TRUE, 0, memSize, h_data, 0, NULL, NULL);
        clFinish(cqCommandQueue);
    }

それで、私の最後の質問は、Nvidia OpenCL環境でマップされた(ゼロコピー)メカニズムを使用するための正しくて最も効率的な方法は何ですか?

@DarkZeros の提案に従って、map-unmap メソッドでさらにテストを行いました。

方法 1 は @DarkZeros の方法と同じです。

//create N buffers in device
for(int i=0; i<MEMCOPY_ITERATIONS; i++)
    cmDevData[i] = clCreateBuffer(cxGPUContext, CL_MEM_READ_WRITE, memSize, NULL, &ciErrNum);

// get pointers mapped to device buffers cmDevData
void* dm_idata[MEMCOPY_ITERATIONS];
for(int i=0; i<MEMCOPY_ITERATIONS; i++)
    dm_idata[i] = clEnqueueMapBuffer(cqCommandQueue, cmDevData[i], CL_TRUE, CL_MAP_WRITE, 0, memSize, 0, NULL, NULL, &ciErrNum);

//Measure the STARTIME
for(unsigned int i = 0; i < MEMCOPY_ITERATIONS; i++)
{
    // copy data from host to device by memcpy
    memcpy(dm_idata[i], h_data, memSize);

    //unmap device buffer.
    ciErrNum = clEnqueueUnmapMemObject(cqCommandQueue, cmDevData[i], dm_idata[i], 0, NULL, NULL);
}
clFinish(cqCommandQueue);
//Measure the ENDTIME

上記の方法では 1900MB/s が得られました。それでも、通常の書き込みバッファ方式よりも大幅に低くなります。さらに重要なことは、マップ操作がプロファイリング間隔外であるため、この方法は実際にはホストとデバイスの間の実際のケースに近くありません。そのため、プロファイリング間隔を何度も実行することはできません。プロファイリング間隔を何度も実行したい場合は、マップ操作をプロファイリング間隔内に配置する必要があります。プロファイリング間隔/ブロックをデータを転送するサブ関数として使用する場合、このサブ関数を呼び出すたびにマップ操作を行う必要があるためです (サブ関数内に unmap があるため)。したがって、マップ操作はプロファイリング間隔でカウントする必要があります。だから私は2番目のテストをしました:

//create N buffers in device
for(int i=0; i<MEMCOPY_ITERATIONS; i++)
    cmDevData[i] = clCreateBuffer(cxGPUContext, CL_MEM_READ_WRITE, memSize, NULL, &ciErrNum);

void* dm_idata[MEMCOPY_ITERATIONS];

//Measure the STARTIME
for(unsigned int i = 0; i < MEMCOPY_ITERATIONS; i++)
{
    // get pointers mapped to device buffers cmDevData
    dm_idata[i] = clEnqueueMapBuffer(cqCommandQueue, cmDevData[i], CL_TRUE, CL_MAP_WRITE, 0, memSize, 0, NULL, NULL, &ciErrNum);

    // copy data from host to device by memcpy
    memcpy(dm_idata[i], h_data, memSize);

    //unmap device buffer.
    ciErrNum = clEnqueueUnmapMemObject(cqCommandQueue, cmDevData[i], dm_idata[i], 0, NULL, NULL);
}
clFinish(cqCommandQueue);
//Measure the ENDTIME

これにより、前の結果とまったく同じ 980MB/s が生成されます。Nvida の OpenCL 実装は、データ転送の観点から CUDA と同じパフォーマンスを達成することはほとんどないようです。

4

1 に答える 1