为什么使用cudaMallocManaged时,NVIDIA Pascal GPU在运行CUDA内核时速度较慢

11

我正在测试新的CUDA 8和Pascal Titan X GPU,期望我的代码能够加速,但由于某些原因,它变得更慢了。我使用的是Ubuntu 16.04。

以下是能够重现结果的最小代码:

CUDASample.cuh

class CUDASample{
 public:
  void AddOneToVector(std::vector<int> &in);
};

CUDASample.cu

__global__ static void CUDAKernelAddOneToVector(int *data)
{
  const int x  = blockIdx.x * blockDim.x + threadIdx.x;
  const int y  = blockIdx.y * blockDim.y + threadIdx.y;
  const int mx = gridDim.x * blockDim.x;

  data[y * mx + x] = data[y * mx + x] + 1.0f;
}

void CUDASample::AddOneToVector(std::vector<int> &in){
  int *data;
  cudaMallocManaged(reinterpret_cast<void **>(&data),
                    in.size() * sizeof(int),
                    cudaMemAttachGlobal);

  for (std::size_t i = 0; i < in.size(); i++){
    data[i] = in.at(i);
  }

  dim3 blks(in.size()/(16*32),1);
  dim3 threads(32, 16);

  CUDAKernelAddOneToVector<<<blks, threads>>>(data);

  cudaDeviceSynchronize();

  for (std::size_t i = 0; i < in.size(); i++){
    in.at(i) = data[i];
  }

  cudaFree(data);
}

Main.cpp

std::vector<int> v;

for (int i = 0; i < 8192000; i++){
  v.push_back(i);
}

CUDASample cudasample;

cudasample.AddOneToVector(v);

唯一的区别在于NVCC标志,对于Pascal Titan X而言是:

-gencode arch=compute_61,code=sm_61-std=c++11;

对于旧版的Maxwell Titan X而言:

-gencode arch=compute_52,code=sm_52-std=c++11;

编辑:以下是运行NVIDIA可视化分析的结果。

对于旧的Maxwell Titan,内存传输时间约为205毫秒,核启动时间约为268微秒。 enter image description here

对于Pascal Titan,内存传输时间约为202毫秒,核启动时间约为惊人的8343微秒,这让我认为出现了一些问题。 enter image description here

我进一步通过将cudaMallocManaged替换为老式的cudaMalloc并进行一些分析来排除问题,并观察到一些有趣的结果。

CUDASample.cu

__global__ static void CUDAKernelAddOneToVector(int *data)
{
  const int x  = blockIdx.x * blockDim.x + threadIdx.x;
  const int y  = blockIdx.y * blockDim.y + threadIdx.y;
  const int mx = gridDim.x * blockDim.x;

  data[y * mx + x] = data[y * mx + x] + 1.0f;
}

void CUDASample::AddOneToVector(std::vector<int> &in){
  int *data;
  cudaMalloc(reinterpret_cast<void **>(&data), in.size() * sizeof(int));
  cudaMemcpy(reinterpret_cast<void*>(data),reinterpret_cast<void*>(in.data()), 
             in.size() * sizeof(int), cudaMemcpyHostToDevice);

  dim3 blks(in.size()/(16*32),1);
  dim3 threads(32, 16);

  CUDAKernelAddOneToVector<<<blks, threads>>>(data);

  cudaDeviceSynchronize();

  cudaMemcpy(reinterpret_cast<void*>(in.data()),reinterpret_cast<void*>(data), 
             in.size() * sizeof(int), cudaMemcpyDeviceToHost);

  cudaFree(data);
}

旧的Maxwell Titan的内存传输时间大约为5毫秒,双向均是如此,核心启动时间则为264微秒。 enter image description here

Pascal Titan的内存传输时间也是双向都约为5毫秒,但核心启动时间只有194微秒,这导致了我希望看到的性能提升… enter image description here

为什么使用cudaMallocManaged时,Pascal GPU在运行CUDA核函数时如此缓慢?如果我必须将所有现有使用cudaMallocManaged的代码还原为cudaMalloc,则这将是一场灾难。这个实验还表明,使用cudaMallocManaged进行内存传输的时间比使用cudaMalloc要慢得多,这也感觉有些不对劲。如果使用这种方法会导致运行时间变慢,即使代码更易于编写,这也应该是不可接受的,因为使用CUDA而不是纯C++的整个目的就是为了加速。我做错了什么,为什么出现这种结果?


7
  1. 向量加法并不是测试GPU速度的特别有趣的测试。
  2. 不可能准确地知道你正在测量什么,以及如何测量。
  3. 在任何GPU上,4096个元素的向量加法内核都不应该需要花费约70毫秒的时间。70微秒更为合理。这是一个非常微小的问题,你几乎可以确定正在测量某种开销,而不是实际的GPU计算性能。
- Robert Crovella
3
将问题规模增加到1亿个元素。 修改您的代码,使其连续调用您的内核两次。 然后使用nvprof运行代码。 在新的Titan X上,第二次调用内核应该运行得更快。 - Robert Crovella
@RobertCrovella,我用nvvp的结果进行了更新,请看一下。谢谢! - user3667089
你好,你使用的驱动程序版本是什么? - harrism
@harrism 最新的367.44版本 - user3667089
显示剩余2条评论
3个回答

23
在CUDA 8和Pascal GPU下,统一内存(UM)模式下的管理内存数据迁移通常会与以前的架构不同,并且您正在经历这种影响。(有关Windows的CUDA 9更新行为,请参见末尾的注释。)
在以前的架构(例如Maxwell)中,由特定内核调用使用的管理分配将一次性迁移所有内容,在内核启动时,大约就像您自己调用cudaMemcpy来移动数据一样。
在CUDA 8和Pascal GPU中,数据迁移通过需求分页进行。默认情况下,在内核启动时,没有数据明确迁移到设备上(*)。当GPU设备代码尝试访问未驻留在GPU内存中的特定页面中的数据时,将发生页面错误。这个页面错误的净效果是:
  1. 导致GPU内核代码(访问页面的线程或线程)暂停(直到完成第2步)
  2. 导致该内存页从CPU迁移到GPU
这个过程将根据需要重复进行,因为GPU代码会触及各种数据页。在上述第二步中涉及的操作序列包括一些延迟,因为页面错误被处理时需要一定时间,另外还需要实际移动数据的时间。由于此过程每次只能移动一页数据,因此可能比一次性移动所有数据(使用cudaMemcpy或通过预Pascal UM安排)要不高效得多,无论是否需要以及内核代码实际需要数据的时间。

两种方法都有其优缺点,我不想辩论各种观点和看法的优点或缺点。分页需求过程为Pascal GPU提供了许多重要的功能和能力。

这个代码示例并不受益。这是预料到的,因此建议使用cudaMemPrefetchAsync()调用来使行为与之前(例如maxwell)的行为/性能保持一致。
您可以使用CUDA流语义来强制在内核启动之前完成此调用(如果内核启动没有指定流,则可以将流参数传递为NULL,以选择默认流)。我相信该函数调用的其他参数非常容易理解。
在您的内核调用之前使用此函数调用,覆盖相关数据,您不应该观察到任何Pascal情况下的页面错误,并且配置文件行为应与Maxwell情况类似。
正如我在评论中提到的那样,如果您创建了一个涉及两个内核调用的测试用例,您会发现第二个调用在Pascal情况下以近乎全速运行,因为所有数据已经通过第一个内核执行迁移到GPU端。因此,这个预取函数的使用不应被视为强制性或自动的,而应该谨慎使用。在某些情况下,GPU可能能够在某种程度上隐藏页面错误的延迟,显然,已经驻留在GPU上的数据不需要预取。
请注意,步骤1中提到的“停顿”可能会误导人。单独的内存访问并不会触发停顿。但是,如果所请求的数据实际上需要用于操作,例如乘法,则warp将在乘法操作处停顿,直到必要的数据变得可用。相关的一点是,以这种方式从主机向设备进行数据需求分页只是GPU可以在其隐藏延迟的架构中可能隐藏的另一个“延迟”,如果有足够的其他可用“工作”来处理。
作为补充说明,在CUDA 9中,Pascal及以上架构的需求分页机制仅适用于Linux系统;CUDA 8中针对Windows系统的先前支持已被取消。请参见此处。在Windows系统上,即使是针对Pascal及以上架构的设备,在CUDA 9中,UM机制与Maxwell及之前的设备相同;在内核启动时,数据会被批量迁移到GPU。
(*) 这里的假设是数据在主机上是“常驻”的,即已经在CPU代码中“触发”或初始化了,在管理分配调用之后。管理分配本身会创建与设备关联的数据页面,当CPU代码“触发”这些页面时,CUDA运行时将要求分页必要的页面以常驻在主机内存中,以便CPU可以使用它们。如果您执行了分配但从未在CPU代码中“触发”数据(这可能是一种奇怪的情况),那么在内核运行时,它实际上已经“常驻”在设备内存中,观察到的行为将是不同的。但这不是这个特定示例/问题的情况。
此博客文章提供了额外信息

使用cudaMemPrefetchAsync对我很有帮助。此外,Pascal和Maxwell之间的区别解释得非常好! - user3667089
@user3667089 不开玩笑,这个答案真是太棒了。我怀疑任何人都无法更简洁、更有效地解释它。 - J.Todd

0

我可以在1060和1080上的三个程序中复现这个问题。例如,我使用一个带有过程转移函数的体积渲染,几乎可以在960上实时交互,但在1080上却稍微慢了一些。所有数据都存储在只读纹理中,只有我的转移函数存储在受管内存中。与我的其他代码不同,体积渲染特别慢,因为与我的其他代码不同,我的转移函数是从内核传递到其他设备方法的。

我认为这不仅仅是使用cudaMallocManaged数据调用内核的问题。我的经验是每次调用内核或设备方法都会出现这种行为,并且效果会累加。此外,体积渲染的基础部分是提供的没有受管内存的CudaSample,它在Maxwell和Pascal GPU(1080、1060、980Ti、980、960)上运行得如预期。

昨天我才发现了这个错误,因为我们将所有研究系统都改为了Pascal。接下来几天我会在980上对我的软件进行性能分析,以进行比较。我还不确定是否应该在NVIDIA开发者区报告错误。


如果这不是NVIDIA的一个bug,请让我知道你是否找到了解决方法。我仍然在使用Maxwell GPU上卡住了,因为我不想将所有的代码从cudaMallocManaged更改为cudaMalloc。 - user3667089

-2

这是NVIDIA在Windows系统上与PASCAL架构发生的一个BUG。

我知道这个问题已经有几天了,但因为我去度假没有网络连接,所以无法在这里写出来。

有关详细信息,请参见https://devblogs.nvidia.com/parallelforall/unified-memory-cuda-beginners/的评论,NVIDIA的Mark Harris确认了该Bug。它应该在CUDA 9中得到纠正。他还表示应该将其通知Microsoft以帮助原因。但我现在还没有找到合适的Microsoft Bug报告页面。


你混淆了两件不同的事情。你提到的错误是关于在Windows WDDM上管理内存的实现问题,与OP报告的问题不同。而且,OP已经确认接受的答案修复了观察到的问题。实际上,CUDA 9已经停止支持Windows上的需求分页,即使对于Pascal及更高设备的UM,Windows的行为也会恢复到Pascal之前的状态(没有需求分页)。请参见[此处](http://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#um-requirements)。 - Robert Crovella

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接