Memory 在Nvidia OpenCL环境中使用映射(零拷贝)内存机制的正确和最有效的方法是什么?

Memory 在Nvidia OpenCL环境中使用映射(零拷贝)内存机制的正确和最有效的方法是什么?,memory,opencl,nvidia,bandwidth,Memory,Opencl,Nvidia,Bandwidth,Nvidia提供了一个关于如何配置主机和设备之间带宽的示例,您可以在这里找到代码:搜索带宽。 实验是在Ubuntu 12.04 64位计算机上进行的。 我正在检查固定内存和映射访问模式,可通过调用进行测试: ./带宽测试-内存=固定-访问=映射 主机到设备带宽的核心测试环路在736~748线附近。我还在此处列出它们,并添加一些注释和上下文代码: //create a buffer cmPinnedData in host cmPinnedData = clCreateBuffer

Nvidia提供了一个关于如何配置主机和设备之间带宽的示例,您可以在这里找到代码:搜索带宽。 实验是在Ubuntu 12.04 64位计算机上进行的。 我正在检查固定内存和映射访问模式,可通过调用进行测试: ./带宽测试-内存=固定-访问=映射

主机到设备带宽的核心测试环路在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时: ./带宽测试-内存=固定-访问=映射-模式=范围-开始=1000000-结束=1000000-增量=1000000 MEMCOPY_迭代次数从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之后调用内核,那么内核可能会也可能不会获得有效数据

如果我们在分析循环中进行映射和取消映射操作,我认为我们可以在取消映射操作之后安全地调用内核,因为这个操作保证数据已经安全地在设备中。新代码如下所示:

// 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-大小-33.5MB。881.9MB/s@block-大小-1MB。映射和取消映射操作的开销似乎没有零拷贝声明那么小

此映射取消映射甚至比2909.6MB慢得多/s@block-大小-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环境中使用mappedzero复制机制的正确和最有效的方法是什么

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

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

这里要注意的第一件事是,OpenCL不允许在2.0版本中使用固定零拷贝—它是可用的,但尚未准备好使用。这意味着您无论如何都必须执行到GPU内存的复制

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

clEnqueueWriteBuffer/clenqueueredbuffer:它们执行从上下文中的OpenCL对象到主机端指针的直接复制。效率很高,但对于少量字节,它们可能效率不高

clEnqueueMapBuffer/CLENQUEUNMAPBUFFER:这些调用首先将设备内存区域映射到主机内存区域。此映射生成内存的1:1副本。然后,在映射之后,您可以使用memcopy或其他方法使用该内存。完成内存编辑后,调用取消映射,然后将此内存传输回设备。 通常,此选项速度更快,因为在映射时OpenCL会为您提供指针。很可能您已经在上下文的主机缓存中写入。但对应的是,当您调用map时,内存传输以另一种方式围绕GPU->host进行

编辑:在最后一种情况下,如果仅为映射选择标志CL_WRITE_,则可能不会触发设备在映射操作上承载复制。只读也会发生同样的情况,这不会在取消映射时触发设备拷贝

在您的示例中,很明显,使用映射/取消映射方法操作会更快。 但是,如果在循环内执行memcpy而不调用unmap,则实际上不会将任何内容复制到设备端。如果放置一个映射/取消映射循环,性能将下降,如果缓冲区大小很小,则传输速率将非常低。然而,这也会发生在令状中 如果在较小大小的for循环中执行写操作,则为e/Read

一般来说,您不应该使用1MB大小,因为在这种情况下,开销将非常高,除非您以非阻塞模式将许多写调用排队

PD:我个人的建议是,简单地使用正常的写/读操作,因为对于大多数常见的用途来说,差异并不明显。特别是对于重叠的I/O和内核执行。但是,如果您确实需要性能,请使用具有读/写功能的映射/取消映射或固定内存,这样可以提高10-30%的传输速率

编辑:与您正在经历的行为相关,在检查nVIDIA代码后,我可以向您解释。您看到的问题主要是由阻塞和非阻塞调用产生的,这些调用隐藏了OpenCL调用的开销

第一个代码:nVIDIA

是否将阻塞映射排队一次 然后执行许多memcpy,但只有最后一个会进入GPU端。 然后以非阻塞方式取消映射。 没有完成就读取结果 这个代码示例是错误的!它并不是真正测量主机GPU拷贝速度。因为memcpy不能确保GPU拷贝,而且缺少clFinish。这就是为什么你会看到速度超过极限

第二个代码:你的

正在循环中将块映射多次排队。 然后对每个映射执行1个memcpy。 然后以非阻塞方式取消映射。 没有完成就读取结果 您的代码只缺少clFinish。然而,由于循环中的映射正在阻塞,因此结果几乎是正确的。但是,在CPU参与下一次迭代之前,GPU是空闲的,因此您会看到一个不现实的非常低的性能

写入/读取代码:nVIDIA

正在对非阻塞写入进行多次排队。 使用clFinish读取结果 这段代码正确地进行了并行复制,您可以在这里看到真正的带宽

以便将映射示例转换为与写/读案例类似的内容。 您应该这样做,因为它没有固定内存:


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

如果你仔细阅读我的问题,你可能会发现这句话,但如果我们使用这种新的分析方法,报告的带宽将变得非常低:915.2MB/s@block-大小-33.5MB。在第三个代码块之后。为什么此映射取消映射比正常显式写入慢?传输速度取决于块大小。例如,如果复制10KB,传输速率可能低至10MB/s。顺便说一句:我刚刚检查了nVIDIA代码,他们没有在clUnmap之后添加必要的clFinish。这完全取决于你测试什么以及如何测试它。对我来说,英伟达例子只是一个例子,它可能在代码中有漏洞。我将编辑/完成我的回答,描述在您的具体案例中发生的情况。离题,但您说OpenCL不允许零拷贝;然而,AMD和Intel的集成GPU都有零拷贝。我的意思是,没有扩展的1.0、1.1、1.2官方OpenCL不支持它们。但是,标准将一些标志定义为实现定义的标志。我必须稍微修改一下您的原始语句。更重要的是,OpenCL不保证零拷贝。关键在于实现定义的部分。即使是2.0也不能保证这一点。即使使用svm,设备也可能执行缓冲区的完整缓存副本。
//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