验证调用cuda内核的次数

时间:2018-04-20 17:08:50

标签: c++ cuda

假设您有一个想要运行2048次的cuda内核,那么您可以像这样定义内核:

__global__ void run2048Times(){ }

然后从主代码中调用它:

run2048Times<<<2,1024>>>();
到目前为止一切似乎都很顺利。但是现在说,当您调用内核数百万次时,为了进行调试,您需要验证您实际多次调用内核。

我所做的是每次内核运行时都将指针传递给内核,然后将指针传递给++。

__global__ void run2048Times(int *kernelCount){ 
    kernelCount[0]++; // Add to the pointer
}

然而,当我将指针复制回主函数时,我得到“2”。

起初它让我感到困惑,然后经过5分钟的咖啡和来回踱步我意识到这可能是有道理的,因为cuda内核同时运行了1024个自身实例,这意味着内核会覆盖“kernelCount” [0]“而不是真正添加它。

所以我决定这样做:

__global__ void run2048Times(int *kernelCount){

   // Get the id of the kernel
   int id = blockIdx.x * blockDim.x + threadIdx.x;

   // If the id is bigger than the pointer overwrite it
   if(id > kernelCount[0]){
        kernelCount[0] = id; 
   }
}

天才!!我保证这可以保证工作。直到我运行它并获得0到2000之间的各种数字。

这告诉我上面提到的问题仍然在这里发生。

有没有办法做到这一点,即使它涉及强制内核暂停并等待彼此运行?

2 个答案:

答案 0 :(得分:1)

该计数器的唯一要点就是进行分析(即分析代码的运行方式),而不是实际计算某些东西(即对程序没有任何功能性好处)。

有可用于此任务的分析工具。例如,nvprof给出了代码库中每个内核的调用次数和一些时间度量标准。

答案 1 :(得分:1)

假设这是一个简化的示例,并且您实际上并不像其他人已经建议的那样尝试进行性能分析,但是想要在更复杂的场景中使用它,您可以使用atomicAdd获得所需的结果,这将确保增量操作作为单个原子操作执行:

__global__ void run2048Times(int *kernelCount){ 
    atomicAdd(kernelCount, 1); // Add to the pointer
}

为什么您的解决方案无效:

您的第一个解决方案的问题是它被编译成以下PTX代码(有关PTX指令的说明,请参阅here):

ld.global.u32   %r1, [%rd2];
add.s32     %r2, %r1, 1;
st.global.u32   [%rd2], %r2;

您可以通过使用nvcc选项调用--ptx来验证这一点,以便仅生成中间代表。

这里可能发生的是以下时间轴,假设您启动了2个线程(注意:这是一个简化的示例,而不是GPU究竟是如何工作的,但它足以说明问题):< / p>

  1. 主题00
  2. 读取kernelCount
  3. 主题10
  4. 读取kernelCount
  5. 主题0通过1
  6. 增加了本地副本
  7. 主题01存储回kernelCount
  8. 主题1通过1
  9. 增加了本地副本
  10. 主题11存储回kernelCount
  11. ,即使启动了2个帖子,您也会得到1

    即使线程是按顺序启动的,第二个解决方案也是错误的,因为线程索引是从0开始的。所以我假设你想要这样做:

    __global__ void run2048Times(int *kernelCount){
    
       // Get the id of the kernel
       int id = blockIdx.x * blockDim.x + threadIdx.x;
    
       // If the id is bigger than the pointer overwrite it
       if(id + 1 > kernelCount[0]){
            kernelCount[0] = id + 1; 
       }
    }
    

    这将编译成:

        ld.global.u32   %r5, [%rd1];
        setp.lt.s32 %p1, %r1, %r5;
        @%p1 bra    BB0_2;
    
        add.s32     %r6, %r1, 1;
        st.global.u32   [%rd1], %r6;
    
    BB0_2:
        ret;
    

    这里可能发生的是以下时间表:

    1. 主题00
    2. 读取kernelCount
    3. 主题10
    4. 读取kernelCount
    5. 主题101 + 1进行比较,并将2存储到kernelCount
    6. 主题000 + 1进行比较,并将1存储到kernelCount
    7. 你最终得错了1的结果。

      如果你想更好地理解同步和非原子操作的问题,我建议你选择一本好的并行编程/ CUDA书。

      修改

      为完整起见,使用atomicAdd的版本编译为:

          atom.global.add.u32     %r1, [%rd2], 1;