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 相同的性能。