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。当传输大小通过以下方式减少到 1MB 时: ./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);

我认为问题在于 memcpy 不能保证数据已经真正传输到设备中,因为循环内没有任何显式的同步 API。因此,如果我们尝试在 memcpy 之后调用内核,内核可能会也可能不会获得有效数据。

如果我们在 profiling 循环中进行 map 和 unmap 操作,我认为我们可以在 unmap 操作之后安全地调用内核,因为这个操作保证了数据已经安全地在设备中。新代码在这里给出:

// 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@block-size-1MB。map 和 unmap 操作的开销似乎不像“零复制”声明那么小。

这个 map-unmap 甚至比 2909.6MB/s@block-size-33.5MB 慢得多,后者是通过 clEnqueueWriteBuffer() 的普通方式得到的:

    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。它仍然显着低于正常的写入缓冲区方法。更重要的是,这种方法实际上并不接近主机和设备之间的实际情况,因为映射操作超出了分析间隔。所以我们不能多次运行分析间隔。如果我们想多次运行分析区间,我们必须将映射操作放在分析区间内。因为如果我们想把 profiling interval/block 作为一个子函数来传输数据,我们每次调用这个子函数之前都要进行 map 操作(因为子函数里面有 unmap)。所以map操作应该计入分析区间。所以我做了第二个测试:

//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

0

首先要注意的是,OpenCL 不允许固定零拷贝(在 2.0 中可用,但尚未准备好使用)。这意味着无论如何您都必须对 GPU 内存执行复制。

有两种方法可以执行内存复制:

  1. clEnqueueWriteBuffer()/clEnqueueReadBuffer():这些在上下文中(通常在设备中)执行从/到 OpenCL 对象的直接复制到主机端指针。效率很高,但可能对少量字节效率不高。

  2. clEnqueueMapBuffer()/clEnqueueUnmapBuffer():这些调用首先将设备内存区域映射到主机内存区域。这map会生成内存的 1:1 副本。然后,在地图之后,您可以使用memcopy()或其他方法使用该内存。完成内存编辑后,您调用unmap,然后将此内存传输回设备。通常,此选项更快,因为 OpenCL 会在您映射时为您提供指针。很可能您已经在上下文的主机缓存中写入。但对应的是,当您调用map内存传输时,内存传输发生了相反的情况(GPU-> 主机)

编辑:在最后一种情况下,如果您选择标志 CL_WRITE_ONLY 进行映射,它可能不会触发设备在映射操作上托管副本。只读也会发生同样的事情,这不会触发取消映射上的设备复制。

在您的示例中,很明显,使用 Map/Unmap 方法操作会更快。但是,如果您memcpy()在没有调用的情况下在循环中执行unmap,则实际上不会将任何内容复制到设备端。如果你放一个循环map/unmap的性能会下降,并且如果缓冲区大小很小(1MB)传输速率会很差。但是,如果您在小尺寸的 for 循环中执行写入,这也会在写入/读取情况下发生。

通常,您不应该使用 1MB 大小,因为在这种情况下开销会非常高(除非您在非阻塞模式下对许多写入调用进行排队)。

PD:我个人的建议是,简单地使用普通的写入/读取,因为对于大多数常见用途来说差异并不明显。特别是重叠的 I/O 和内核执行。但是如果你真的需要性能,使用映射/取消映射或固定内存和读/写它应该可以提高 10-30% 的传输率。


编辑:与您遇到的行为有关,在检查 nVIDIA 代码后,我可以向您解释。您看到的问题主要是由阻塞和非阻塞调用产生的,它们“隐藏”了 OpenCL 调用的开销

第一个代码:(nVIDIA)

  • 正在排队一次 BLOCKING 地图
  • 然后执行许多 memcpys(但只有最后一个会转到 GPU 端)。
  • 然后以非阻塞方式取消映射。
  • 不读结果clFinish()

此代码示例是错误的!它并不是真正测量 HOST-GPU 的复制速度。因为memcpy()它不能确保 GPU 副本并且因为存在clFinish()缺失。这就是为什么您甚至会看到速度超过限制的原因。

第二个代码:(你的)

  • 正在循环中多次排队 BLOCKING 映射。
  • 然后memcpy()为每张地图执行 1。
  • 然后以非阻塞方式取消映射。
  • 不读结果clFinish()

您的代码仅缺少clFinish(). 然而,由于循环中的地图阻塞,结果几乎是正确的。但是,在 CPU 参加下一次迭代之前,GPU 一直处于空闲状态,因此您会看到不切实际的非常低的性能。

写/读代码:(nVIDIA)

  • 多次排队非阻塞写入。
  • 读取结果clFinish()

此代码并行正确地进行复制,您在这里看到了真正的带宽。

为了将地图示例转换为与写入/读取案例相当的内容。你应该这样(这没有固定内存):

//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];
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, dm_idata, 0, NULL, NULL);
}
clFinish(cqCommandQueue);

//Measure the ENDTIME

您不能在映射的情况下重用相同的缓冲区,否则在每次迭代后您会阻塞。GPU 将处于空闲状态,直到 CPU 重新排队下一个复制作业。

于 2013-11-14T10:02:15.203 回答