wrap shf
__shfl_xor_sync 是 CUDA 中一个用于线程束(warp)内数据交换的同步函数。它允许线程通过按位异或 (XOR) 操作自己的通道 ID(lane ID)来确定要从哪个线程获取数据,从而实现一种称为“蝶式”交换的高效数据交换模式。
1. 函数原型
T __shfl_xor_sync(unsigned mask, T var, int laneMask, int width=warpSize);
mask:- 类型:
unsigned int(32位) - 含义: 线程掩码。每一位代表 warp 中的一个线程。只有掩码中对应位为 1 的线程才会参与此次数据交换。通常使用
0xffffffff表示 warp 中所有32个线程都参与。
- 类型:
var:- 类型:
T(支持int,float,double等多种类型) - 含义: 当前线程要提供出去,并期望从目标线程获取的变量值。
- 类型:
laneMask:- 类型:
int - 含义: 异或掩码。目标线程的通道 ID 由
(当前线程的laneId ^ laneMask)计算得出。
- 类型:
width(可选):- 类型:
int - 默认值:
warpSize(通常是32) - 含义: 逻辑上划分 warp 的子分组大小。必须是2的幂(如 2, 4, 8, 16, 32)。交换操作仅在当前线程所在的
width大小的子分组内进行。
- 类型:
2. 核心行为与数据流
__shfl_xor_sync 最核心的特征是其交叉交换的数据流向。它通过 target_lane = current_lane ^ laneMask 的规则,让线程间形成一种对称的数据交换。
关键行为规则:
- 对于 warp 中由
mask指定的每个参与线程,其目标线程 ID 通过(thread_lane_id ^ laneMask)计算得出。 - 如果计算出的目标线程 ID 在
width指定的有效范围内,则函数返回该目标线程的var值。 - 如果目标线程 ID 超出范围或目标线程未参与(不活跃),则返回值未定义(通常返回调用线程自己的
var值,但不应依赖此行为)。
3. 工作原理示例
假设 warpSize = 8 (为简化说明,实际通常为32),laneMask = 1(二进制 001)。所有8个线程都参与(mask = 0xFF)。数据交换将如下进行:
| 当前线程ID (十进制) | 当前线程ID (二进制) | laneMask (二进制) | 目标线程ID (二进制) | 目标线程ID (十进制) | 数据流向 |
|---|---|---|---|---|---|
| 0 | 000 | XOR 001 | 001 | 1 | 0 ⇄ 1 |
| 1 | 001 | XOR 001 | 000 | 0 | 1 ⇄ 0 |
| 2 | 010 | XOR 001 | 011 | 3 | 2 ⇄ 3 |
| 3 | 011 | XOR 001 | 010 | 2 | 3 ⇄ 2 |
| 4 | 100 | XOR 001 | 101 | 5 | 4 ⇄ 5 |
| 5 | 101 | XOR 001 | 100 | 4 | 5 ⇄ 4 |
| 6 | 110 | XOR 001 | 111 | 7 | 6 ⇄ 7 |
| 7 | 111 | XOR 001 | 110 | 6 | 7 ⇄ 6 |
可以看到,laneMask = 1 实现了相邻两个线程为一对的交换。如果 laneMask = 3(二进制 011),则会实现更复杂的交叉配对,例如线程0和3交换,线程1和2交换等。
4. 典型应用场景:Warp级归约(Reduction)
__shfl_xor_sync 最经典的应用是实现 warp 内的快速归约操作(如求和、求最大值)。它通过不断减半 laneMask(从 16, 8, 4, 2, 1),以蝶式网络的方式将最终结果汇集到每个线程(或特定线程,如 lane 0)。
__device__ int warp_reduce_sum(int val) {
// 使用全线程掩码,width为32
// 步长从16开始,依次减半:8, 4, 2, 1
val += __shfl_xor_sync(0xffffffff, val, 16);
val += __shfl_xor_sync(0xffffffff, val, 8);
val += __shfl_xor_sync(0xffffffff, val, 4);
val += __shfl_xor_sync(0xffffffff, val, 2);
val += __shfl_xor_sync(0xffffffff, val, 1);
return val; // 现在所有线程的val都等于整个warp的原始val之和
}
__global__ void kernel(int *input, int *output) {
int laneId = threadIdx.x % 32;
int val = input[threadIdx.x];
int sum = warp_reduce_sum(val);
// 通常只需要线程束内第一个线程(lane 0)将结果写出
if (laneId == 0) {
output[blockIdx.x] = sum;
}
}
在这个例子中,laneMask 从 16 递减至 1,完美地展示了其“蝴蝶归约”的特性。
5. 与其他 Shuffle 函数的对比
| 函数 | 数据流向 | 目标线程计算方式 | 典型应用 |
|---|---|---|---|
__shfl_xor_sync | 交叉交换 | target_id = current_id ^ laneMask | 蝶式归约、全交换 |
__shfl_down_sync | 向高线程ID流动 | target_id = current_id + delta | 前缀和、向下广播 |
__shfl_up_sync | 向低线程ID流动 | target_id = current_id - delta | 后缀和、向上广播 |
__shfl_sync | 直接复制 | target_id = srcLane (固定) | 广播 |
6. 重要注意事项
- 同步与掩码 :
_sync后缀表明这是一个同步函数。mask参数中指定的所有线程必须共同执行此函数,否则结果未定义。在 Volta 及更高架构(支持独立线程调度)上,即使线程处 于不同的分支路径(如 if-else),只要它们最终都执行了匹配的__shfl_xor_sync调用,操作仍可正确完成。 - 目标线程活跃性 : 线程只能从积极参与本次
__shfl_xor_sync调用的线程读取数据。如果目标线程不活跃(例如,因分支而未执行此函数),则读取到的值是未定义的。 width参数 : 如果指定了width,它必须是一个2的幂且不大于32。操作将在width大小的子分组内独立进行。- 性能优势 : 相比使用共享内存进行数据交换,shuffle 指令直接在寄存器上操作,延迟更低(在现代架构上可能仅为1个周期),并且不占用共享内存,也无需显式的地址计算。
总结来说,__shfl_xor_sync 是一个强大的 warp 级原语,通过异或运算实现灵活、高效的线程间数据交换,是高性能 CUDA 编程中实现归约、扫描等并行算法的重要工具。