← 返回目录

cp.async / ldmatrix 消融实验 —— 初步规划(SM80 专用)

目标设备:SM80(Ampere,如 A100)

目标:设计一套逐步叠加优化的消融实验(ablation),量化每一项优化(ldmatrix vs ld.shared、cp.async vs 同步 load)到底带来了多少性能提升。

本文档是 初步规划.md 的 SM80 裁剪版:已移除全部 TMA 相关内容(原 TMA 章节改为「SM80 能力边界」),并补充了 Ampere 特有的实验注意事项。 与原版的差异见文末 §6 与完整版的差异。


0. SM80 能力边界

0.1 各特性的可用性

特性 SM80 (A100) 说明
cp.async.cg.shared.global (16B) ✅ 可用 Ampere 引入,.cg 绕过 L1 直达 L2
cp.async.ca.shared.global (4/8/16B) ✅ 可用 会经过 L1
cp.async.commit_group / cp.async.wait_group N ✅ 可用 SM80 上唯一的异步完成追踪手段
cp.async.mbarrier.arrive ❌ 要求 sm_90
cp.async.bulk / cp.async.bulk.tensor(TMA) ❌ 要求 sm_90
st.async ❌ 要求 sm_90
cp.reduce.async.bulk ❌ 要求 sm_90
mbarrier.init/arrive ⚠️ 基础版可用 但 expect_tx / try_wait.parity 需 sm_90;SM80 上无法用它精确追踪 cp.async 完成
ldmatrix.sync.aligned.m8n8.x1/x2/x4[.trans].shared.b16 ✅ 可用 自 Turing (sm_75) 引入,SM80 完整支持
mma.sync.aligned.m16n8k16.row.col.f32.f16/f16.f32 / .bf16 ✅ 可用 非 Hopper wgmma
wgmma(warpgroup MMA) ❌ 要求 sm_90
cp.async + 多级流水(STAGES≥3) ✅ 可用 Ampere 上的标准做法
cuda::memcpy_async / cuda::barrier (libcu++) ✅ 可用 内部就是 cp.async + mbarrier 的封装

结论:TMA 及一切依赖 TMA 的代码在 SM80 上不可用。

0.2 仓库中受影响的文件

文件 是否可用 原因
kernels/hgemm/hgemm.cu ✅ 可用,推荐作为实验基座 纯 cp.async + ldmatrix + mma.sync,无 TMA
kernels/sgemm/sgemm_tf32.cu ✅ 可用 纯 cp.async + ldmatrix + mma.sync(TF32)
kernels/sgemm/sgemm.cu ✅ 可用 SIMT 版本,可作对照组
kernels/hgemm_sm120/hgemm_sm120.cu ❌ 不可用 依赖 TMA(cp_async_bulk_tensor_2d)与 mbarrier.expect_tx
kernels/flash_attn/flash_attn.cu ❌ 不可用 TMA-only(flash_attn.cu:21 的 CP_ASYNC_CG 宏定义了但从未调用)
kernels/flash_decode/flash_decode.cu ❌ 不可用 TMA-only(cp_async_bulk_tensor_3d)
kernels_dsl/** — 无相关内容(仅 triton/tilelang 的 vector_add、softmax toy 示例)

⚠️ 注意:kernels/hgemm_sm120/readme.md 里的性能数字(cp.async 34.00 TF / TMA 34.31 TF 等)在 SM80 上无法复现,不能直接当作 baseline。所有 baseline 必须在你自己的 SM80 设备上重测。

0.3 可直接复用的 PTX 宏(SM80 安全)

来自 kernels/hgemm/hgemm.cu:22-40、kernels/sgemm/sgemm_tf32.cu:22-35:

#define CP_ASYNC_CG(dst, src)  asm volatile("cp.async.cg.shared.global.L2::128B [%0], [%1], 16;\n" ...)
#define CP_ASYNC_COMMIT_GROUP()      // cp.async.commit_group
#define CP_ASYNC_WAIT_GROUP_0()      // cp.async.wait_group 0
#define LDMATRIX_X4(R0,R1,R2,R3,PTR)   // ldmatrix.sync.aligned.m8n8.x4.shared.b16
#define LDMATRIX_X2(R0,R1,PTR)         // ldmatrix.sync.aligned.m8n8.x2.shared.b16
#define LDMATRIX_X2_TRANS(R0,R1,PTR)   // ldmatrix.sync.aligned.m8n8.x2.trans.shared.b16

多级流水版本需要的 wait_group<N>(N>0)在 hgemm_sm120 里有现成写法,但那个文件依赖 TMA 不可用 —— 需要把这段 PTX 抄到新文件里:

template <int N>
__device__ __forceinline__ void cp_async_wait_group() {
    if constexpr (N == 0)      asm volatile("cp.async.wait_group 0;\n" ::);
    else if constexpr (N == 1) asm volatile("cp.async.wait_group 1;\n" ::);
    else if constexpr (N == 2) asm volatile("cp.async.wait_group 2;\n" ::);
    else                       asm volatile("cp.async.wait_group 3;\n" ::);
}

0.4 已有对照数据(仅供参考,不能直接当 SM80 baseline)

kernels/hgemm/readme.md(4096³ bf16,来源机型未知):

hgemm_cublas              4.210 ms  32.64 TF
hgemm_naive               5.191 ms  26.47 TF   ← ldmatrix + mma(无 swizzle / 无双缓冲)
hgemm_bcf                 4.337 ms  31.69 TF   ← + BCF swizzle
hgemm_bcf_dbf             4.096 ms  33.55 TF   ← + double buffer
hgemm_bcf_dbf_rw          4.076 ms  33.72 TF   ← + coalesced gmem r/w

kernels/sgemm/readme.md(4096³ TF32):

sgemm_tf32_bt_swizzle_dbf      8.723 ms  15.76 TF   (非 cp.async:普通 load + st.shared)
sgemm_tf32_swizzle_bcf_dbf     8.276 ms  16.61 TF   (cp.async)

⚠️ 这组差异里 swizzle / dbf / grid-swizzle 三个因素是混在一起的,不能直接归因到 cp.async —— 这正是需要做受控消融的原因。


1. 消融实验设计

核心思路:把优化拆成两条独立的阶梯,每条阶梯逐档叠加,每档只改变一个变量,从而把增量收益归因清楚。

固定基线:GEMM 形状 M=N=K=4096、bf16、BM=BN=128、BK=32、256 线程 / 8 warp、warp tile 64×32、mma.m16n8k16。 (建议全部在 kernels/hgemm/hgemm.cu 的结构上做,保持其它变量一致。)

1.1 阶梯 A:smem → reg(ldmatrix vs ld.shared)

对应 ldmatrix 这一项。需要三档,且必须给 ld.shared 变体最有利布局,否则测到的是布局差异而非指令效率。

档 A fragment(16×16) B fragment(8×8) LSU 指令数 / lane
A0 直接从 gmem 读(无 smem) 同 无 smem,最慢
A1 ld.shared.b32 ×4 ld.shared.b16 ×4(Bs[BK][BN] 布局,k 方向不连续) 8
A2 ld.shared.b32 ×4 ld.shared.b32 ×2(B 预转置为 Bs[BN][BK],ld.shared 最优情况) 6
A3 ldmatrix.x4 ×1 ldmatrix.x2.trans ×1 2

A3 即现有 kernels/hgemm/hgemm.cu:131/142 的写法。

关键:A3 相比 A1/A2 省的是 LSU 指令数和地址计算指令,不省带宽(数据搬运量相同)。 另外注意 ldmatrix.x4 是整个 warp 协作指令(32 个 lane 各提供一个 8×8 tile 的行地址,硬件自动 gather+转置+广播),而 ld.shared 是 per-lane 独立访存 —— 这正是 A3 的核心优势来源。

1.2 阶梯 B:gmem → smem(cp.async vs 同步 load)

档 搬运用什么 完成同步 能否与 mma 重叠
B0 ld.global.v4 + st.shared.v4 __syncthreads() ❌ 完全串行
B1 cp.async.cg 16B commit_group + wait_group<0> + __syncthreads() ❌ 单 buffer 下无收益
B2 cp.async.cg + 双缓冲(STAGES=2) wait_group<0>(读旧 buffer 前 wait 住新 buffer) ✅
B3 cp.async.cg + 多级流水(STAGES=3) wait_group<STAGES-2> ✅ 带宽利用更充分

SM80 上没有 TMA 作为「第三档」,B3 就是搬运路径的终点。若还想再往上走,下一档是 B4:cp.async + 寄存器级双缓冲(ldmatrix 结果在寄存器里预取下一 k_step,见 hgemm/hgemm.cu:355 的 bcf_dbf 思路)。

1.3 ⚠️ 三个会让结论作废的坑

坑 1:B1 单 buffer 下 cp.async 几乎无收益,甚至更慢。 若按 B0→B1→B2 顺序测,会得出"cp.async 没用"的错误结论。cp.async 的价值在于释放 LSU 和 issue slot,只有当有第二级 buffer 承接、让搬运算与 mma 重叠时才能兑现。

✅ 正确阶梯顺序:同步 load + 双缓冲 → cp.async + 双缓冲 → cp.async + 多级流水。 也就是说,"双缓冲"和"cp.async"必须拆成两步分别加,否则无法区分收益来自哪一项。

实现上:同步 load + 双缓冲 = 普通的 ld.global+st.shared 到 buffer[write],__syncthreads() 后读 buffer[read],再 __syncthreads() 换索引; cp.async + 双缓冲 = 把搬运算换成 cp.async.cg + commit_group/wait_group。

坑 2:在最终 kernel 上测 ldmatrix vs ld.shared,差异会被淹没。 若 kernel 被 mma 吞吐或 DRAM 带宽卡住,smem→reg 段的差异在 wall-clock 上看不见。 在 A100 上这个风险尤其高:A100 的 FP16 Tensor Core 峰值约 312 TFLOPS,而实测 GEMM 常在 150~250 TFLOPS,两者差距不像消费级卡那么大,LSU 段更容易被掩盖。

✅ 建议额外做一个 smem→reg 微基准(只 load 不 mma,循环 N 次),直接测 LSU 吞吐差异; 同时用 ncu 采集:

smsp__inst_executed_pipe_lsu_mem_shared_op_ld.sum
l1tex__data_bank_conflicts_pipe_lsu_mem_shared_op_ld.sum
sm__inst_executed_pipe_lsu.avg.pct_of_peak_sustained_active

坑 3:只测一种 shape 会被误导。

✅ 建议扫 K ∈ {256, 512, 1024, 4096, 8192} × M/N ∈ {512, 4096},画热力图。

另外两条硬性要求:

1.4 SM80 / A100 特有的注意事项

  1. smem 容量:A100 每 SM 164 KB,单 block 最多 163 KB(需 cudaFuncSetAttribute(cudaFuncAttributeMaxDynamicSharedMemorySize, ...) 显式开启 >48KB)。 本实验 BM=BN=128, BK=32, bf16 → A tile = 128×32×2 = 8 KB,B tile = 32×128×2 = 8 KB,每 stage 16 KB。

  2. cp.async.cg 的 16B 对齐:cp.async.cg 只支持 16B 粒度,且 gmem 地址需 16B 对齐。若 K 或 N 不是 8 的倍数(bf16 下 16B = 8 个元素),边界 tile 需要 fallback 路径。 实验时务必保证 M/N/K 都是 8 的倍数,把 fallback 从性能数据里排除掉。

  3. cp.async 与 L2 行为:.cg 绕过 L1,命中 L2。A100 L2 为 40 MB(大),所以 K 较小的 shape 会大量命中 L2 —— 这也是为什么坑 3 里 K 的扫描很重要。

  4. cp.async.wait_group 的语义是"按组计数",而不是按字节。STAGES=3 时用 wait_group<1> 允许 1 组仍在飞行中,不能用 wait_group<0>(那会把流水退化成单 buffer)。这一点在写 B3 时最容易写错。

  5. A100 上没有 wgmma,所有 mma 都是 warp 级 mma.sync,因此 ldmatrix 的收益不会被 Hopper 的 wgmma 掩盖 —— 这其实是 SM80 做这个实验的优势:在 SM90 上 wgmma 直接从 smem 取数,ldmatrix 的收益会大幅缩水。SM80 是观察 ldmatrix 价值更干净的平台。

  6. TF32 的注意:如果同时在 sgemm_tf32.cu 上做对照,A100 上 TF32 Tensor Core 吞吐约为 FP16 的一半,别把两者的 TFLOPS 直接横向比较。


1.5 SIMT / Tensor Core 与访存指令的层级关系(重要澄清)

这一节回答一个容易踩坑的问题:SIMT 和 cp.async / ldmatrix 是同一层级的东西吗? 答案:不是。 把 SIMT 误当作 ldmatrix / cp.async 的"对照组"会直接导致实验结论作废。

1.5.1 三个层级

SIMT = Single Instruction, Multiple Threads,是 NVIDIA 的硬件执行模型:一条指令由一个 warp 的 32 个线程同时执行。

它与 SIMD 的区别:SIMD 是"一条指令操作 N 个数据槽,程序员手动向量化";SIMT 是"一条指令操作 1 个数据,硬件在 warp 内自动 SIMD 化",程序员写标量代码。

关键:所有 CUDA kernel 都是 SIMT 的,包括 Tensor Core kernel —— 只是 mma.sync 指令本身的执行方式(一条指令被 warp 内多个线程分组协作完成)比普通 FMA 更"特殊"。

层级 东西 说明
执行模型 / 计算单元 SIMT(标量 FMA pipe) vs Tensor Core(MMA pipe) SM 内的硬件单元,决定峰值算力,差 8~16 倍
访存指令 ld.global / st.shared / ld.shared / cp.async / ldmatrix 单条指令,属于"怎么把数据喂给计算单元",差 5~30%

1.5.2 为什么 SIMT 不能当 ldmatrix / cp.async 的对照组

如果把两者混在同一张消融表里,会犯一个经典错误:把 8 倍的架构收益算到 ldmatrix 头上。

1.5.3 SIMT 的正确用法

用途 1:性能上下界锚点(不是消融实验的一环)

// SIMT 版(kernels/sgemm/sgemm.cu):每个线程算若干个输出元素
for (int k = 0; k < BK; ++k)
    sum[i][j] += As[i][k] * Bs[k][j];   // 标量 FMA,走 FP32 pipe

// Tensor Core 版(kernels/sgemm/sgemm_tf32.cu)
MMA_M16N8K8(...);   // mma.sync,走 Tensor Core

A100 峰值参考:

路径 A100 峰值
SIMT FP32 FMA ~19.5 TFLOPS
Tensor Core TF32 ~156 TFLOPS
Tensor Core FP16/BF16 ~312 TFLOPS

差 816 倍的量级会完全淹没 ldmatrix / cp.async 带来的 530% 提升 —— 这正是 §1.3「坑 2」的成因。

用途 2:架构级对照,回答"这套优化路线值不值得"

真正要回答的问题分两层:

问题 对照方式
Q1: "上 Tensor Core 值得吗?" SIMT vs Tensor Core
Q2: "在 Tensor Core 框架内,ldmatrix / cp.async 各值多少?" 阶梯 A / 阶梯 B 内部互比

1.5.4 仓库里的现成素材正好构成干净对照

kernels/sgemm/ 里是一对标准对照:

kernels/sgemm/sgemm.cu        ← SIMT 版(标量 FMA),已做到 tiling + swizzle + double buffer + coalesced r/w
kernels/sgemm/sgemm_tf32.cu   ← Tensor Core 版(mma.sync + ldmatrix + cp.async)

而且 SIMT 版本身也有一套完整的优化阶梯(见 sgemm/readme.md):

sgemm_tiling                   ← 基础 tiling
sgemm_at_tiling                ← + A 转置入 smem
sgemm_at_bcf_swizzling         ← + swizzle 消 bank conflict
sgemm_at_bcf_swizzling_rw      ← + coalesced 写回
sgemm_at_bcf_swizzling_dbf_rw  ← + double buffer(反超 cuBLAS)

这是教科书级的消融阶梯写法 —— 每步只加一个东西,readme 里就有逐档 TFLOPS。 本实验的 cp.async / ldmatrix 阶梯应当照抄这个范式。

📌 公平性提醒:SIMT 版也可以做 shared memory tiling 和向量化解码(仓库里已经做了), 所以 SIMT vs Tensor Core 的对比必须是"都优化到位"的两套实现之比,而不是"裸 SIMT" vs "花活 Tensor Core"。

1.5.5 小结

是什么 在实验中的角色
SIMT 执行模型 / 计算单元选择(标量 FMA vs Tensor Core),差 8~16 倍 架构级对照,单独一张表
cp.async / ldmatrix 喂数据给 Tensor Core 的访存指令,差 5~30% 实验变量,阶梯 A / 阶梯 B

2. 代码组织建议

现在 kernels/hgemm/hgemm.cu 里有 4 个 kernel 几乎完全重复(naive / bcf / bcf_dbf / bcf_dbf_rw),继续加变体会爆炸。

建议新建 kernels/ablation/(基于 kernels/hgemm/hgemm.cu 改造,SM80 安全),用一个模板参数化的 kernel 覆盖全组合:

template <int LOAD_MODE,   // 0=ldg+st.shared, 1=cp.async
          int READ_MODE,   // 0=ld.shared, 1=ldmatrix
          int B_LAYOUT,    // 0=Bs[BK][BN], 1=Bs[BN][BK]
          int STAGES,      // 1,2,3,4
          int SWIZZLE>     // 0/1
__global__ void hgemm_ablation_kernel(...);

用 if constexpr 分支(编译器消掉未走分支,零运行时开销),变体命名 abl_L{lm}_R{rm}_bl{bl}_s{s}_sw{sw}。

实现要点:


3. 预期结论骨架(待实测填数)

最终输出 3 张独立的表,不要把 SIMT 与 ldmatrix / cp.async 混排在同一张表里(理由见 §1.5)。

表 1:架构级对照(SIMT vs Tensor Core)

实现 路径 TFLOPS 相对 SIMT
sgemm_at_bcf_swizzling_dbf_rw SIMT FP32 FMA - 1x
sgemm_tf32_swizzle_bcf_dbf TC / TF32 / mma.sync - ~8x
hgemm_bcf_dbf_rw TC / BF16 / mma.sync - ~16x
cublas (bf16) 厂商库 - -

这一表回答"上 Tensor Core 值不值"。注意 SIMT 与 TC 两边必须都是优化到位的版本。

表 2:阶梯 A —— ldmatrix vs ld.shared(固定 cp.async + STAGES=3 + swizzle)

变体 描述 LSU 指令/lane TFLOPS vs 上一档
abl_L1_R0_bl0_s3_sw1 ld.shared,B 自然布局 Bs[BK][BN] 8 - -
abl_L1_R0_bl1_s3_sw1 ld.shared,B 预转置 Bs[BN][BK](最有利对手) 6 - -
abl_L1_R1_bl1_s3_sw1 ldmatrix.x4 + ldmatrix.x2.trans 2 - -

表 3:阶梯 B —— cp.async vs 同步 load(固定 ldmatrix)

变体 描述 TFLOPS vs 上一档 累计
abl_L0_R1_s1_sw1 同步 ld.global+st.shared,单 buffer - - 基准
abl_L0_R1_s2_sw1 同步 load + 双缓冲 - -
abl_L1_R1_s1_sw1 cp.async,单 buffer - -
abl_L1_R1_s2_sw1 cp.async + 双缓冲 - -
abl_L1_R1_s3_sw1 cp.async + 多级流水 (STAGES=3) - -
abl_L1_R1_s4_sw1 cp.async + STAGES=4 - -

关键读法:"双缓冲"与"cp.async"必须拆成两步分别加,增量才可归因。 单 buffer 的 cp.async(B3 那一档)预期与同步 load 持平甚至略慢 —— 这是正常的,不是实验失败。

表 4:形状敏感性(每个变体跑一遍 shape 扫描)

变体 \ shape M=N=4096,K=256 K=1024 K=4096 K=8192 M=N=512,K=4096
(同上各变体) - - - - -

K 小 → latency-bound,cp.async / 多级流水收益最大;K 大 → compute-bound,收益被 mma 吞吐盖住。


4. 待办清单


5. 附录:参考指标(ncu)

关注点 指标
ldmatrix 增量 smsp__inst_executed_pipe_lsu_mem_shared_op_ld.sum、l1tex__data_bank_conflicts_pipe_lsu_mem_shared_op_ld.sum、sm__inst_executed_pipe_lsu.avg.pct_of_peak_sustained_active
cp.async 增量 smsp__inst_executed_op_global_ld.sum、gpu__time_duration.sum、l1tex__throughput、dram__throughput
同步 load 的延迟暴露 smsp__warp_issue_stalled_long_scoreboard_per_warp_active.pct(同步 load 会明显偏高)
流水是否真的重叠 smsp__warp_issue_stalled_mio_throttle_per_warp_active.pct / smsp__warp_issue_stalled_lg_throttle_per_warp_active.pct

已有 profiling 脚手架:kernels/hgemm/run_ncu.sh、kernels/hgemm/test_for_ncu.py (用法:ncu -k regex:"gemm|Kernel" --set full -o profile_res -f python test_for_ncu.py)。


6. 与完整版的差异

章节 完整版 SM80 版
TMA 介绍 完整章节(描述符 / 发起 / mbarrier / 约束) 删除,替换为「§0.1 SM80 能力边界」对照表
阶梯 B B0 同步 / B1 cp.async / B2 cp.async 多级 / B3 TMA 删除 B3(TMA),改为 B0 同步 / B1 cp.async / B2 双缓冲 / B3 多级 / B4 寄存器双缓冲
异步完成机制 mbarrier + expect_tx(按字节) commit_group + wait_group<N>(按组计数)
swizzle 软件 swizzle vs TMA 硬件 swizzle 只有软件 swizzle(SWIZZLE_A/B),无需考虑二者冲突
基座文件 hgemm_sm120 kernels/hgemm/hgemm.cu
不可用文件 — 明确标注 hgemm_sm120 / flash_attn / flash_decode 不可用
新增 — A100 smem 容量与 opt-in、16B 对齐、L2 行为、wait_group 语义、wgmma 缺失(SM80 做 ldmatrix 实验反而更干净)

7. 原版 TMA 章节(存档,SM90+ 才需要)

点击展开:完整版中的 TMA 说明(SM90+/Hopper 及以上才适用)

TMA = Tensor Memory Accelerator,Hopper (SM90) 引入的硬件 DMA 引擎,Blackwell 保留并增强。负责"把一整块多维 tile 从 global 搬到 shared"。

与 cp.async 的对比

cp.async TMA
指令粒度 每线程一条指令搬 16B 一个线程一条指令搬整个 tile
地址计算 每线程自己算 gmem/smem 地址 + swizzle XOR 硬件按描述符算,零地址计算指令
搬运宽度 4B/8B/16B 一行最长 256B,tile 可任意大
落 smem 排布 软件 XOR swizzle 硬件自动 swizzle(CU_TENSOR_MAP_SWIZZLE_128B/64B)
完成通知 commit_group / wait_group N(按组,粗) mbarrier + expect_tx(bytes)(按字节,精确)
访问模式 任意(可 gather) 只能整 tile copy
支持 Ampere+ Hopper+ (SM90+)

三个组成部分

  1. TensorMap 描述符(host 端 cuTensorMapEncodeTiled,见 hgemm_sm120.cu:731)
  2. Kernel 端发起:cp.async.bulk.tensor.2d.shared::cta.global.mbarrier::complete_tx::bytes(见 hgemm_sm120.cu:114)
  3. 完成跟踪:mbarrier + expect_tx

约束:global 地址 16B 对齐;box 最内维字节数匹配 swizzle 宽度;rank ≤ 5;descriptor 需在 device 生命周期内有效;不支持任意 gather。


8. 更新记录

日期 变更
v1 初版:从 初步规划.md 裁剪出 SM80 版本,移除 TMA
v2 追加 §1.5「SIMT / Tensor Core 与访存指令的层级关系」;§3 改为 4 张分表(架构级对照 / 阶梯 A / 阶梯 B / 形状敏感性);§4 增加「复现 SIMT 优化阶梯」待办