在1-D网格中计算warp id / lane id的最有效方法是什么?

时间:2017-06-02 20:55:12

标签: optimization cuda ptx

在CUDA中,每个线程都知道它在网格中的块索引和块内的线程索引。但似乎没有明确提供两个重要的值:

  • 其指数作为其经线内的一条车道(其"车道ID")
  • 经线的索引,它是块内的一条线(其"经线ID")

假设网格是1维的(a.k.a。线性,即blockDim.yblockDim.z是1),显然可以如下获得:

enum : unsigned { warp_size = 32 };
auto lane_id = threadIdx.x % warp_size;
auto warp_id = threadIdx.x / warp_size;

如果您不信任编译器对其进行优化,可以将其重写为:

enum : unsigned { warp_size = 32, log_warp_size = 5 };
auto lane_id = threadIdx.x & (warp_size - 1);
auto warp_id = threadIdx.x >> log_warp_size;

是最有效率的事情吗?对于每个线程来说,计算它仍然需要很多浪费。

(受this question启发。)

2 个答案:

答案 0 :(得分:8)

天真的计算目前效率最高。

注意:此答案已经过严格修改。

尝试完全避免计算是非常诱人的 - 因为如果你深入了解这两个值似乎已经可用了。

您可以看到,nVIDIA GPU具有特殊寄存器,您的(已编译)代码可以读取这些寄存器以访问各种有用信息。一个这样的寄存器持有threadIdx.x;另一个持有blockDim.x;另一个 - 时钟滴答计数;等等。 C ++作为一种语言显然没有这些暴露;事实上,CUDA也没有。但是,编译CUDA代码的中间表示,名为PTX 会公开这些special registers(因为PTX 1.3,即CUDA版本> = 2.1)。 / p>

其中两个特殊寄存器是%warpid%laneid。现在,CUDA支持使用asm关键字在CUDA代码中内联PTX代码 - 就像它可以用于主机端代码直接发出CPU汇编指令一样。使用这种机制,可以使用这些特殊寄存器:

__forceinline__ __device__ unsigned lane_id()
{
    unsigned ret; 
    asm volatile ("mov.u32 %0, %laneid;" : "=r"(ret));
    return ret;
}

__forceinline__ __device__ unsigned warp_id()
{
    // this is not equal to threadIdx.x / 32
    unsigned ret; 
    asm volatile ("mov.u32 %0, %warpid;" : "=r"(ret));
    return ret;
}

......但这里有两个问题。

第一个问题 - 正如@Patwie建议的那样 - 是%warp_id没有给你你真正想要的东西 - 它不是网格环境中扭曲的索引,而是在物理SM的背景(一次可以容纳如此多的经线),而这两者并不相同。因此请勿使用%warp_id

对于%lane_id,它确实为您提供了正确的价值,但它具有误导性的非正常性:即使它是"注册",它也是"与寄存器文件中的常规寄存器不同,具有1个周期的访问延迟。它是一个特殊的寄存器,在实际硬件中为retrieved using an S2R instruction,可能会出现长延迟。


底线:只需从线程ID计算warp ID和线程ID。我们现在无法解决这个问题。

答案 1 :(得分:1)

另一个答案是非常 危险!自己计算lane-id和warp-id。

#include <cuda.h>
#include <iostream>

inline __device__ unsigned get_lane_id() {
  unsigned ret;
  asm volatile("mov.u32 %0, %laneid;" : "=r"(ret));
  return ret;
}

inline __device__ unsigned get_warp_id() {
  unsigned ret;
  asm volatile("mov.u32 %0, %warpid;" : "=r"(ret));
  return ret;
}

__global__ void kernel() {
  const int actual_warpid = get_warp_id();
  const int actual_laneid = get_lane_id();
  const int expected_warpid = threadIdx.x / 32;
  const int expected_laneid = threadIdx.x % 32;
  if (expected_laneid == 0) {
    printf("[warp:] actual: %i  expected: %i\n", actual_warpid,
           expected_warpid);
    printf("[lane:] actual: %i  expected: %i\n", actual_laneid,
           expected_laneid);
  }
}

int main(int argc, char const *argv[]) {
  dim3 grid(8, 7, 1);
  dim3 block(4 * 32, 1);

  kernel<<<grid, block>>>();
  cudaDeviceSynchronize();
  return 0;
}

给出类似

的内容
[warp:] actual: 4  expected: 3
[warp:] actual: 10  expected: 0
[warp:] actual: 1  expected: 1
[warp:] actual: 12  expected: 1
[warp:] actual: 4  expected: 3
[warp:] actual: 0  expected: 0
[warp:] actual: 13  expected: 2
[warp:] actual: 12  expected: 1
[warp:] actual: 6  expected: 1
[warp:] actual: 6  expected: 1
[warp:] actual: 13  expected: 2
[warp:] actual: 10  expected: 0
[warp:] actual: 1  expected: 1
...
[lane:] actual: 0  expected: 0
[lane:] actual: 0  expected: 0
[lane:] actual: 0  expected: 0
[lane:] actual: 0  expected: 0
[lane:] actual: 0  expected: 0
[lane:] actual: 0  expected: 0
[lane:] actual: 0  expected: 0
[lane:] actual: 0  expected: 0
[lane:] actual: 0  expected: 0
[lane:] actual: 0  expected: 0
[lane:] actual: 0  expected: 0

另请参阅PTX文档

  

预定义的只读特殊寄存器,返回线程的   扭曲标识符。经纱标识符提供唯一的经纱编号   在CTA中,但不在网格中的所有CTA中。变形标识符   一次扭曲内的所有线程都将是相同的。

     

请注意,%warpid是易失的,并返回线程在以下位置的位置   读取的那一刻,但其值可能会在执行过程中发生变化,例如,   由于抢占后重新安排了线程。

因此,它是调度程序的warp-id,不能保证与虚拟warp-id匹配(从0开始计数)。

The docs makes this clear

  

因此,如果内核代码中需要这样的值,则应使用%ctaid和%tid来计算虚拟扭曲索引; %warpid是   主要用于使概要分析和诊断代码能够采样和   日志信息,例如工作场所映射和负载分配。

如果您认为,那么就让我们使用CUB:这甚至会影响cub::WarpId()

  

返回调用线程的扭曲ID。经向ID保证为   在经线中唯一,但可能不对应于从零开始的排名   在线程块中。

编辑:使用%laneid似乎很安全。