Warp-level Programming——从 Shuffle 到 Cooperative Groups

你已经在 Reduction 里见过 __shfl_down_sync,在 Shared Memory 与 Bank Conflict 里理解了 __syncthreads() 的代价——每次 barrier 都是几十个 cycle 的 stall。但你可能还没有系统地回答这些问题:

  • __shfl_down_sync_sync 后缀是什么意思?那个 0xffffffff 参数可以改吗?
  • 除了 Reduction 的 shfl_down,还有 shfl_upshfl_xor——它们分别用于什么场景?
  • __ballot_sync 是什么?为什么 FlashAttention 和 vLLM 的 kernel 里到处是它?
  • if (tid < 32) 这样的 divergent 路径上调用 shuffle 安全吗?如果不安全,__activemask() 能救吗?
  • A100 号称有硬件 __reduce_add_sync——它和手写 shuffle 哪个更快?

这些问题的共同点:它们都围绕一个 CUDA 编程中最底层的执行单元——Warp。32 个线程共享一个 PC(Program Counter),每个 cycle 执行同一条指令。在这 32 个线程之间,寄存器是互通的——通过一组名为 Warp Primitives 的指令,数据可以在 1 个 cycle 内完成线程间交换,不需要 Shared Memory、不需要 __syncthreads()

本文将系统讲解这组指令:Shuffle → Vote → Match → Cooperative Groups → A100 硬件加速。每一节从一个你会遇到的实际场景出发,先建立直觉,再给出代码,最后回到”什么时候用这个、什么时候不适用”。

前置要求:你已经理解 Thread/Warp/Block 执行模型(见 GPU 编程导论),用过 Shared Memory 并理解 __syncthreads 的作用(见 Shared Memory 与 Bank Conflict),知道 Reduction 的基本概念(见 Reduction)。

验证代码:本文所有实测数据来自配套 Benchmark:14_warp_level_bench.cu。包含 Shuffle/SMEM Reduce、Prefix Sum Scan、Ballot Filter、Match Dedup 四项测试。


1. Warp 执行模型——所有 Warp 原语的共同前提

在学 API 之前,必须先理解 Warp 的硬件行为。这是后续所有讨论的基础。

1.1 SIMT 与 Lockstep:32 线程共享一个 PC

GPU 的 SIMT(Single Instruction, Multiple Threads)模型的核心约束:一个 Warp 内的 32 个线程共享同一个 Program Counter。这意味着它们在每个时钟周期执行完全相同的指令,只是操作的数据不同。

Warp(32 threads, 共享 1 个 PC)
  │
  ├── thread 0:  r0 = r1 + r2   ← 对 r1[0]、r2[0] 操作
  ├── thread 1:  r0 = r1 + r2   ← 对 r1[1]、r2[1] 操作
  ├── ...
  └── thread 31: r0 = r1 + r2   ← 对 r1[31]、r2[31] 操作

  所有 32 线程同时执行 "r0 = r1 + r2",只是各自的寄存器值不同。

正是这个 lockstep 特性,使得 Warp 内的数据交换不需要任何同步原语——因为你知道所有 32 个线程一定在同一个 cycle 到达同一行代码。Shuffle 指令利用这一点,直接在寄存器文件内完成数据交换。

CPU 类比:SIMD(如 AVX-512)在一个 core 上操作 512-bit 寄存器(16 个 float)。GPU 的 Warp 可以看作把 32 个 scalar 线程映射到一个 SIMD 单元的 32 条 lane 上。

1.2 Active Mask 与 Divergence

当 Warp 遇到 if-else 分支时,不同线程可能走不同路径。但由于 lockstep 约束,GPU 串行执行两个分支,只激活对应路径上的线程:

if (tid < 16) {
    // 路径 A:只有 tid 0-15 活跃,tid 16-31 空闲(mask = 0x0000FFFF)
    x = a + b;
} else {
    // 路径 B:只有 tid 16-31 活跃,tid 0-15 空闲(mask = 0xFFFF0000)
    x = c + d;
}

硬件维护一个 Active Mask——一个 32-bit 的位掩码,bit i = 1 表示 thread i 当前活跃。Divergent 路径上的指令只对 active 线程产生效果。

关键约束:在 divergent 路径上调用 Warp 原语(shuffle、vote 等)是未定义行为——因为原语假定所有 32 个线程都参与。这就是 _sync 后缀的作用:你必须显式告诉硬件哪 32 个线程应该参与这次通信

1.3 _sync 后缀与 Mask 参数

CC 7.0 (Volta) 引入了显式的 _sync 版本:

// 旧 (CC < 7.0, 隐式假定全 warp 参与)
int val = __shfl_down(val, offset);

// 新 (CC ≥ 7.0, 显式指定参与线程)
int val = __shfl_down_sync(0xffffffff, val, offset);
//                           ^~~~~~~~~~
//                           32-bit mask: bit i = 1 → thread i 参与

0xffffffff = 全部 32 线程参与。如果你在 divergent 路径上,mask 必须匹配当前活跃的线程集合——少一个线程都会导致 UB

// 安全:在已知全部活跃的路径上用全 mask
if (lane_id < 32) {  // 整个 warp 在这里通常是统一的
    val = __shfl_down_sync(0xffffffff, val, 8);
}

// 危险:在可能 divergent 的路径上
if (lane_id % 2 == 0) {
    val = __shfl_down_sync(0xffffffff, val, 8);  // ← UB! 奇数 lane 不参与
}

后面 5.3 节会讲如何用 __activemask() 处理这种情况。

1.4 Warp vs Block:通信边界

属性 Warp 内 Block 内 (跨 Warp)
通信方式 Shuffle / Vote / Match Shared Memory
延迟 ~1 cycle ~20-30 cycles
同步需要 不需要(lockstep) __syncthreads()
线程数 ≤ 32 ≤ 1024

Warp Shuffle 不能替代 Shared Memory:跨 Warp 的数据交换必须回到 Shared Memory。Shuffle 的优势仅在 Warp 内部——对于许多算法(Reduction、Scan、Filter),Warp 内的操作可以完全在寄存器中完成。


2. Shuffle——Warp 内最快的线程间通信

2.1 四种模式

Shuffle 允许一个线程从 Warp 内另一个线程的寄存器读取数据。四种模式对应四种线程间通信的拓扑:

模式 语义 数据流 典型用途
__shfl_sync(mask, val, src_lane) 所有线程读 lane src_laneval Broadcast 分发常量、warp leader
__shfl_up_sync(mask, val, delta) thread i 读 thread i-deltaval 向上传递 Prefix Sum (Scan)
__shfl_down_sync(mask, val, delta) thread i 读 thread i+deltaval 向下传递 Reduction
__shfl_xor_sync(mask, val, lane_mask) thread i 读 thread i^lane_maskval Butterfly All-to-all、FFT

图示(以 8 线程为例):

__shfl_down_sync(mask, val, 2):

  thread:  0   1   2   3   4   5   6   7
            ↓   ↓   ↓   ↓   ↓   ↓   ↓   ↓
  源:      2   3   4   5   6   7   ?   ?    ← 越界的 lane 保持原值

  结果:   val[2] val[3] val[4] val[5] val[6] val[7] val[6] val[7]


__shfl_xor_sync(mask, val, 1):  ← Butterfly (lane_mask = 1)

  thread:  0⟷1   2⟷3   4⟷5   6⟷7
  交换:   每个线程读 (自己的 lane_id ^ 1) 的值

2.2 实战:Warp Reduction with __shfl_down_sync

这是最常见的用法——在 Reduction 中已经展示过。核心代码只需 3 行:

// Warp reduce: 32 elements → 1 (在 thread 0)
__device__ inline float warpReduceSum(float val) {
    for (int offset = 16; offset > 0; offset /= 2)
        val += __shfl_down_sync(0xffffffff, val, offset);
    return val;
}

执行过程(以 8 线程为例):

offset=4:  t0←t0+t4  t1←t1+t5  t2←t2+t6  t3←t3+t7
offset=2:  t0←t0+t2  t1←t1+t3
offset=1:  t0←t0+t1

最终 t0 = sum(all 8 threads),共 3 次 shuffle (log₂8 = 3)
对于 32 线程:5 次 shuffle (log₂32 = 5) → 共 ~5 cycles

对比 Shared Memory 版:每个 sdata[tid] += sdata[tid + s] 都需要一次 SMEM 读(~20 cycles)+ __syncthreads()(~10 cycles)→ 总共 ~150 cycles。Shuffle 版节省了 ~145 cycles

2.3 实战:Warp-level Prefix Sum (Scan) with __shfl_up_sync

Prefix Sum(每个元素 = 前面所有元素之和)是 Filter、Radix Sort 等算法的基础。用 shfl_up 实现:

__device__ inline float warpPrefixSum(float val) {
    for (int offset = 1; offset < 32; offset *= 2) {
        float tmp = __shfl_up_sync(0xffffffff, val, offset);
        if (lane_id >= offset) val += tmp;
    }
    return val;
}

执行过程(8 线程示例):

初始:      t0=v0  t1=v1  t2=v2  t3=v3  t4=v4  t5=v5  t6=v6  t7=v7

offset=1:  t0=v0  t1=v0+v1  t2=v1+v2  t3=v2+v3  ...  t7=v6+v7
offset=2:  t0=v0  t1=∑v0:1  t2=∑v0:2  t3=∑v1:3  ...  t7=∑v5:7
offset=4:  t0=v0  t1=∑v0:1  t2=∑v0:2  t3=∑v0:3  t4=∑v0:4 ... t7=∑v3:7

最终每个线程持有 [0..lane_id] 的前缀和。

2.4 实战:Butterfly Broadcast with __shfl_xor_sync

shfl_xor 实现的是 Butterfly(蝶形)交换——lane i 和 lane i ^ lane_mask 互相读取。这在需要成对交换的算法中很常见(FFT、Bitonic Sort):

// Warp 内成对交换数据
for (int mask = 16; mask > 0; mask >>= 1) {
    float other = __shfl_xor_sync(0xffffffff, my_val, mask);
    my_val = fmaxf(my_val, other);  // Bitonic Sort 的比较步骤
}

2.5 Shuffle vs Shared Memory 性能实测

通信方式 延迟 何时用
Shuffle (__shfl_down_sync) ~1 cycle Warp 内数据交换
Shared Memory (无 conflict) ~20-30 cycles Block 内任意线程间
Shared Memory (32-way conflict) ~600+ cycles 需要优化(见 #13)

关键结论:如果你需要的数据只在 Warp 内,永远优先用 Shuffle 而不是 Shared Memory


3. Vote——Warp 内条件聚合与判断

Shuffle 交换的是数据,Vote 交换的是条件——每个线程贡献 1 bit,32 个线程聚合为 32-bit 的结果。

3.1 __ballot_sync(mask, predicate):Warp 级的条件路由

ballot 是最强大的 Vote 原语——每个线程提交一个 bool,返回一个 32-bit 掩码,其中 bit i = 1 当且仅当 thread i 的 predicate 为 true:

// 哪些线程的 lane_id 是偶数?
unsigned mask = __ballot_sync(0xffffffff, (lane_id % 2) == 0);
// mask = 0x55555555

// 哪些线程的值 > 阈值?
unsigned mask = __ballot_sync(0xffffffff, val > threshold);

用法一:知道”还有哪些线程需要处理”:

分批处理不规则数据——FlashAttention 用 ballot 来确定哪些 query 需要继续 causal mask 检查:

unsigned remaining = __ballot_sync(0xffffffff, need_more_work);
while (remaining) {
    int next_lane = __ffs(remaining) - 1;  // 找到下一个需要处理的 lane
    // ... 处理 ...
    remaining &= remaining - 1;  // 清除这个 lane 的 bit
}

用法二:Warp 级数据压缩 (Compact):

将满足条件的线程数据紧凑排列到前半部分:

// 输入: 每个线程有一个值和一个 bool (keep)
// 输出: 前 N 个线程存放被 keep 的值

unsigned keep_mask = __ballot_sync(0xffffffff, keep);
int prefix = __popc(keep_mask & ((1u << lane_id) - 1));  // 排在当前线程之前的 keep 线程数

CPU 类比:__ballot_sync 相当于 32 个 bit 的 SIMD compare + movemask。在 AVX 中,_mm256_movemask_ps 将 8 个 float 的符号位压缩为 8-bit 整数——GPU 的 ballot 将 32 个 bool 压缩为 32-bit 整数。

3.2 __all_sync / __any_sync:Warp 级 AND / OR

这两个是 ballot 的简化版——只需知道”全部为真”还是”存在为真”,不需要知道具体哪些线程:

// 所有线程都完成了它们的工作?
bool all_done = __all_sync(0xffffffff, is_done);

// 至少有一个线程发现了需要 refine 的元素?
bool needs_refine = __any_sync(0xffffffff, found_better);

实际使用场景:迭代算法中检测收敛条件——__all_sync 判断是否所有线程都收敛了,__any_sync 判断是否有任何线程需要继续迭代。

3.3 实战:Warp 内 Filter (Compact)

利用 ballot + popc 的组合实现零 Shared Memory 的 warp 内 filter:

// 保留 warp 内 > 0 的元素,紧凑排列
__device__ int warpCompact(float *output, float val) {
    unsigned keep = __ballot_sync(0xffffffff, val > 0.0f);

    // 当前线程在 keep 线程中的 rank
    int local_rank = __popc(keep & ((1u << lane_id) - 1));

    // keep 线程将自己的值写到正确位置(Warp 内 Shuffle)
    // 注意:存回 Shared Memory 的代码已在 warp 外处理

    return __popc(keep);  // 返回 keep 的总数
}

3.4 实战:Ballot + __popc 做 Warp-level Histogram

// 每个线程有一个类别标签 (0-31),统计每个类别在 warp 内的数量
int hist = 0;
unsigned mask = __ballot_sync(0xffffffff, category == my_category);
hist += __popc(mask);  // 这个类别在 warp 内的计数

4. Match——Warp 内去重与分组

4.1 __match_any_sync:找出”谁和我的值相同”

// 线程 i 想知道 warp 内哪些线程和它有相同的 key
unsigned mask = __match_any_sync(0xffffffff, key);
// mask 中所有 key == key[i] 的线程的 bit 被置位

用途:哈希表 conflict resolution。如果多个线程哈希到同一个 slot,用 match_any 检查 warp 内是否有冲突。

4.2 __match_all_sync:检查全 warp 的值是否一致

// 返回 {mask, predicate}
// mask: 如果所有线程值相同则 = 0xffffffff,否则每个线程的 mask 不定
// pred: 所有线程值相同 → 1,否则 → 0
unsigned mask;
int all_same = __match_all_sync(0xffffffff, key, &mask);

4.3 实战:Warp 内去重 (Dedup)

// 每个线程有一个 key,找出 warp 内唯一的 key
unsigned seen = 0;
int is_unique = 0;

// 每个 key,只有 lane_id 最小的那个线程标记为 unique
unsigned same_key = __match_any_sync(0xffffffff, key);
if ((same_key & ((1u << lane_id) - 1)) == 0)  // 我是第一个
    is_unique = 1;

4.4 适用场景

场景 原语 作用
哈希表探测 match_any 检测 warp 内 slot 冲突
图算法 match_any 邻接去重
字典压缩 match_any 相同 key 只保留一份
收敛检测 match_all 检查全 warp 达到一致状态

5. __activemask() —— 运行时探测 Divergence

5.1 它返回什么

unsigned active = __activemask();

返回当前 cycle 的 active mask——也就是硬件此刻真正在执行的那些线程的位掩码。这和在 CPU 上读取当前 PID 类似——它是运行时状态。

5.2 用途:Divergent 路径上安全做 Partial Warp Reduction

当你在一个 if 分支内(只有部分线程活跃),仍需要在这些活跃线程之间做 reduction:

if (complex_condition) {
    // 只有部分线程在这里
    unsigned active = __activemask();
    float partial = __shfl_down_sync(active, my_val, 16);
    // 只在 active 线程之间做 shuffle
}

关键约束__shfl_down_sync 的 mask 必须只包含当前活跃的线程。用 __activemask() 作为 mask 参数是实现这一点的唯一安全方式。

5.3 陷阱——activemask 是瞬时快照

unsigned active = __activemask();  // 快照: active = 0x0000FFFF
// ... 几行代码(不改变控制流) ...
// active 可能已经过时!中间可能有隐式的收敛(reconvergence)

__activemask() 返回的是调用瞬间的值。不要在几行代码之后再使用——它已经过期了。始终在同一个表达式或下一行就使用。


6. Cooperative Groups tiled_partition——现代抽象

6.1 为什么需要 CG

手写 __shfl_down_sync(0xffffffff, val, offset) 有三个问题:

  1. mask 是魔法数字0xffffffff 看起来和 32 无关
  2. 易出错:在 divergent 路径上忘记改 mask → UB
  3. 不可移植:假设 warp size = 32——虽然目前所有 NVIDIA GPU 都如此,但未来可能改变

Cooperative Groups 提供了类型安全的抽象:

#include <cooperative_groups.h>
namespace cg = cooperative_groups;

auto cta = cg::this_thread_block();           // Block-level CG
auto tile32 = cg::tiled_partition<32>(cta);   // Warp-sized tile

// 替代 __shfl_down_sync(0xffffffff, val, offset)
float res = tile32.shfl_down(val, offset);    // ← mask 自动推导,永远正确

6.2 CG API 与原语对照

CG API 等价原语
tile32.shfl_down(val, offset) __shfl_down_sync(0xffffffff, val, offset)
tile32.shfl_up(val, offset) __shfl_up_sync(0xffffffff, val, offset)
tile32.shfl(val, src_lane) __shfl_sync(0xffffffff, val, src_lane)
tile32.shfl_xor(val, mask) __shfl_xor_sync(0xffffffff, val, mask)
tile32.ballot(pred) __ballot_sync(0xffffffff, pred)
tile32.any(pred) __any_sync(0xffffffff, pred)
tile32.all(pred) __all_sync(0xffffffff, pred)
tile32.match_any(val) __match_any_sync(0xffffffff, val)
tile32.match_all(val, &mask) __match_all_sync(0xffffffff, val, &mask)

6.3 CG vs 原语的性能差距

零。 Cooperative Groups 的 tiled_partition 在编译期完全展开为原始 PTX 指令——模板参数 <32> 是编译期常量,所有 shuffle/vote 调用在 SASS 层面与原语完全相同。代价是代码中多了一行 tiled_partition 构造,以及需要 #include <cooperative_groups.h>

6.4 灵活 Tile 大小

CG 的真正优势不在于替换 Warp 原语,而在于支持任意 2 的幂次的 tile 大小

auto tile16 = cg::tiled_partition<16>(cta);  // 16 线程 tile
auto tile8  = cg::tiled_partition<8>(cta);
auto tile4  = cg::tiled_partition<4>(cta);

// tile16 内的 shuffle 仅在 16 线程间进行
float res = tile16.shfl_down(val, 8);

这在需要分层 reduce 的算法中非常有用——先 tile8 内 reduce,再在 Shared Memory 中跨 tile 合并。


7. A100 硬件加速——__reduce_add_sync 与局限

7.1 硬件 Reduce 指令 (SM 8.0+)

A100 的 SM 8.0 引入了硬件 Warp Reduce 指令(PTX: red.sync),整个 32 线程的 reduce 在一条指令内完成:

// 手写 shuffle reduce: 5 条指令 + 5 个 cycle
float sum = warpReduceSum(my_val);  // 见 2.2 节

// A100 硬件 reduce: 1 条指令,~1 cycle
float sum = __reduce_add_sync(0xffffffff, my_val);

支持的硬件 reduce 操作:

指令 操作
__reduce_add_sync(mask, val) 求和
__reduce_min_sync(mask, val) 最小值
__reduce_max_sync(mask, val) 最大值
__reduce_and_sync(mask, val) 按位与
__reduce_or_sync(mask, val) 按位或
__reduce_xor_sync(mask, val) 按位异或

7.2 性能差异

Warp Reduce 32 floats (A100):

  Shuffle loop (__shfl_down_sync):    ~5 cycles  (5 次 shuffle)
  Hardware reduce (__reduce_add_sync): ~1 cycle   (1 条 red.sync 指令)

  差异: 5x,但绝对值仅 4 cycles → 对于大 kernel,差距可忽略

7.3 局限

  1. 仅 32 线程:不能做 16/8/4 线程的 partial reduce
  2. 仅固定操作:不能做自定义 combiner(如 fmaxf(fabs(x), fabs(y))
  3. 仅 CC ≥ 8.0:V100 (CC 7.0) 不可用

7.4 何时用 shuffle vs 硬件 reduce

需要 Warp Reduce?
  ├─ 操作是 add/min/max → 用 __reduce_add_sync (CC 8.0+)
  ├─ 自定义 combiner → 必须手写 shuffle loop
  └─ 需要 < 32 线程 partial reduce → 必须手写 shuffle loop

8. 权衡——Warp 原语的边界

8.1 Shuffle vs Shared Memory 的决策树

数据在 Warp 内通信?
  ├─ 是 → 优先用 Shuffle
  │   ├─ 全 32 线程 → __shfl_down_sync / CG tiled_partition<32>
  │   └─ 部分线程 → __activemask() + shuffle
  └─ 否 → 必须用 Shared Memory
      ├─ Warp 间通信 → sdata + __syncthreads
      └─ Block 间通信 → Global Memory + atomic / cooperative groups

8.2 Divergent 路径上的安全法则

// 法则:如果这段代码在 if/else 里,必须用 mask = __activemask()
if (some_condition) {
    unsigned active = __activemask();
    float res = __shfl_down_sync(active, val, offset);  // ← 用 active,不是 0xffffffff
}

忘了这条法则 → UB → 随机正确或随机错误 → 最难查的 Bug。

8.3 Cooperative Groups 的取舍

  原始 _sync 原语 Cooperative Groups
代码量 多一行 tiled_partition
正确性保证 手动管理 mask 编译器自动
可移植性 假设 warp_size=32 类型系统保护
灵活 tile 大小 不支持 支持任意 2 的幂次
性能 完全一样 完全一样

建议:新代码用 CG。只有当你需要支持 CC < 7.0(非常旧的 GPU)时才用原始 _sync 原语。


9. 总结——Warp 原语选型速查

9.1 选型表

你想做什么 用哪个 示例
Warp 内求和/求最值 __shfl_down_sync__reduce_add_sync Reduction
Warp 内前缀和 __shfl_up_sync Scan / Filter
Warp 内广播 __shfl_sync 分发 warp leader 的值
成对交换 __shfl_xor_sync Bitonic Sort / FFT
收集所有满足条件的线程 mask __ballot_sync Filter / Compact / 条件路由
检查全 warp 条件 __all_sync / __any_sync 收敛检测
查找相同 key 的线程 __match_any_sync 去重 / 哈希表探测
Divergent 路径安全 shuffle __activemask() + __shfl_down_sync 分支内 partial reduce
现代抽象(推荐) cg::tiled_partition<32> + .shfl_down() 以上全部

9.2 与其他文章的衔接

本文涉及的 详见
Reduction 中的 shuffle 用法 Reduction 第 5 节
Shared Memory 与 __syncthreads 的开销 Shared Memory 与 Bank Conflict 第 1 节
Global Memory coalesced access GPU 内存管理 第 1 节
Tensor Core 与 Warp 的关系 Tensor Core GEMM

附录:Warp 原语速查

A. Shuffle

float __shfl_sync(unsigned mask, float val, int src_lane, int width=32);
float __shfl_up_sync(unsigned mask, float val, unsigned delta, int width=32);
float __shfl_down_sync(unsigned mask, float val, unsigned delta, int width=32);
float __shfl_xor_sync(unsigned mask, float val, int lane_mask, int width=32);

B. Vote

unsigned __ballot_sync(unsigned mask, int predicate);
int      __all_sync(unsigned mask, int predicate);
int      __any_sync(unsigned mask, int predicate);

C. Match

unsigned __match_any_sync(unsigned mask, int value);
unsigned __match_all_sync(unsigned mask, int value, unsigned *pred_mask);

D. Active Mask

unsigned __activemask();  // 返回当前 cycle 的活跃线程 mask

E. A100 Hardware Reduce (CC ≥ 8.0)

int   __reduce_add_sync(unsigned mask, int val);
int   __reduce_min_sync(unsigned mask, int val);
int   __reduce_max_sync(unsigned mask, int val);
unsigned __reduce_and_sync(unsigned mask, unsigned val);
unsigned __reduce_or_sync(unsigned mask, unsigned val);
unsigned __reduce_xor_sync(unsigned mask, unsigned val);