arganzheng · 2026-05-20
\[T \ge \max\!\left(\frac{\text{FLOPs}}{P_{peak}},\ \frac{\text{Bytes}}{BW}\right),\qquad I = \frac{\text{FLOPs}}{\text{Bytes}}\ \ \text{vs}\ \ \text{ridge} = \frac{P_{peak}}{BW}\]每个 kernel 先算它理论上应该多快,再测它实际多快,再用 profiler 解释差距,再动手缩小差距。
| 硬件基线 | 带宽 | BF16 | FP32 | SM | ridge(BF16) |
|---|---|---|---|---|---|
| A100 | 2.0 TB/s | 312 TFLOPS | 19.5 TFLOPS | 108 | 156 |
| H100 | 3.35 TB/s | 989 TFLOPS | 67 TFLOPS | 132 | 295 |
%% 图:CUDA 主线与 Ascend C 独立对照的十一篇路径
%%{init: {"flowchart": {"wrappingWidth": 180}}}%%
flowchart LR
G1["01 硬件与<br/>Roofline"] --> G2["02 CUDA<br/>编程模型"] --> G3["03 访存合并<br/>elementwise"]
G3 --> G4["04 shared<br/>reduction"] & G5["05 GEMM"]
G4 & G5 --> NX["06–11 进阶<br/>(下图)"]
%%{init: {"flowchart": {"wrappingWidth": 180}}}%%
flowchart TB
G6["06 Tensor Core<br/>CUTLASS · CuTe"] & G7["07 Triton"] --> G8["08 Attention"] & G9["09 量化融合"]
G8 & G9 --> G10["10 剖析测试"] --> G11["11 昇腾"]
GPU 用零开销 warp 切换隐藏延迟——面积给了 ALU 不给缓存
| 算子 | I(FLOP / Byte) | vs ridge 156(A100 BF16) |
|---|---|---|
| elementwise(add) | (I=1/6) | memory-bound,差三个数量级 |
| RMSNorm | ≈ 1 | memory-bound |
| decode attention(GQA g = 4) | ≈ 4 | memory-bound |
| GEMM 4096³ BF16 | ≈ 1365 | compute-bound |
DRAM 可达带宽只有标称的 80–92%
__global__ void add(const float* a, const float* b, float* c, int n) {
int i = blockIdx.x * blockDim.x + threadIdx.x; // 全局线程编号
if (i < n) c[i] = a[i] + b[i];
}
// add<<<(n + 255) / 256, 256>>>(a, b, c, n);
| 量 | 数 |
|---|---|
| 3 × 1 GiB 读写 | 下界 3 GiB / 2.0 TB/s = 1.6 ms;实测 1.7–2.0 ms |
| block 大小 | 32 的倍数,常用 128–256 |
| 计时 | cudaEventRecord 前后各一个 event——launch 立即返回,CPU 时钟量到的是提交时间 |
| L2 flush | 128 MB ≥ 2 × 40 MB,否则测的是 L2 |
按 32 B sector 合并:连续 100%、跨步 2 是 50%、跨步 ≥32 B 是 12.5%
%%{init: {"flowchart": {"wrappingWidth": 220}}}%%
flowchart LR
BW["带宽 2.0 TB/s"] --> L["Little's law<br/>在飞字节 = 带宽 × 延迟"]
LAT["HBM 延迟 ~600 ns"] --> L
L --> F["整卡需保持 ≈ 1.2 MB 在飞<br/>÷ 108 SM ≈ 11 KB / SM"]
F --> A["每 warp 一条 32-bit 加载 = 128 B<br/>→ 需 ≈ 88 warp / SM,超过上限 64"]
F --> B["每 warp 一条 128-bit 加载 = 512 B<br/>→ 需 ≈ 22 warp / SM"]
F --> C["每 warp 两条 128-bit 在飞(ILP)<br/>→ 需 ≈ 11 warp / SM"]
classDef k fill:#dbeafe,stroke:#1d4ed8
class BW,LAT k
合并到 32 B sector;90% 之后只能减字节
| 要点 | 细节 |
|---|---|
| 16 B / 线程的向量化加载 | 「memory-bound 不需要高占用率」——1.2 MB 必须在飞 |
| 三个 elementwise kernel 各读写 16 B → 融合成一个 8 B |
warp shuffle 5 步归约,sync 10 → 1;online softmax 一遍就够
| 量 | 数 |
|---|---|
| 128 MiB 的 RMSNorm | 67 µs(接近带宽) |
| bank conflict | bank = (addr / 4) mod 32;stride s → gcd(s, 32)-way |
| d ≤ 1024 | 一行一个 warp |
| \(e^x\) 溢出 | FP16 于 x > 11.09,BF16 / FP32 于 88.7——softmax 必须减最大值 |
atomicAdd 做行归约最简单」——同地址串行化;float 加法不满足结合律、不可复现;树形归约 + shuffle,跨 block 只用原子计数naive 读 512 GiB、I=0.25;128×128 分块 → 4 GiB、I=32
Tensor Core 不是更快的 FMA——累加器属于 warp、布局由指令定
| 量 | 数 |
|---|---|
| 312 TFLOPS | FP32 的 16 倍 |
mma.sync.m16n8k16 |
4096 FLOP,占 8 周期 |
| BF16 4096³ | 100.7 MB、I = 1365、下界 0.44 ms |
| 未 swizzle 的 64 B 行 | 4 路 bank conflict |
| 手写到 cuBLAS | 80–90% |
编译器写 matmul:少 80% 代码、到 cuBLAS 80–95%
| kernel | Triton | CUDA |
|---|---|---|
| vector add | 12 行 | 50–80 行 |
| softmax | 25 行 | 100–150 行 |
| matmul | 60 行 | 200–300 行 |
不物化 S、P:online softmax 逐块累加,HBM 132 → 66 MiB(近 4 MiB)
%%{init: {"flowchart": {"wrappingWidth": 190}}}%%
flowchart TB
subgraph std["标准:三个 kernel,S 与 P 往返 HBM"]
direction LR
q1["Q, K 各 1 MiB"] --> g1["GEMM #1"] -- "写 / 读 S 32 MiB" --> sm1["softmax"] -- "写 / 读 P 32 MiB" --> g2["GEMM #2"] --> o1["O 1 MiB"]
end
subgraph fa["FlashAttention:一个 kernel,S / P 只在片上"]
direction LR
q2["Qᵢ, Kⱼ, Vⱼ tile"] --> g3["Sᵢⱼ 寄存器"] --> sm2["online softmax<br/>更新 m, l"] --> g4["Oᵢ += P̃ᵢⱼ Vⱼ"] -- "遍历完写 1 次" --> o2["O 1 MiB"]
end
std ~~~ fa
classDef hbm fill:#fde2e2,stroke:#c0392b
classDef chip fill:#dff5e1,stroke:#1e8449
class q1,o1,q2,o2 hbm
class g3,sm2,g4 chip
| 量 | 数 |
|---|---|
| Llama-3-8B 每 token KV | 128 KiB |
| B × s > 约 131k token 时 | KV 读取超过权重读取 |
| PagedAttention | block 表把逻辑块映射到物理块:只改地址不改字节,解决碎片与共享前缀 |
| FlashDecoding | 沿序列维切 split-K,让小 batch 也填满 SM |
INT4 字节 ÷4、FLOPs 不变——decode 8 → 约 2 ms
| 融合 | 每元素字节 |
|---|---|
| residual + RMSNorm | 10 → 8 B |
| RMSNorm + FP8 量化 | 7 → 3 B |
| SiLU × up(SwiGLU 激活) | 三读一写 → 两读一写 |
| RoPE + KV 写入 | 省一次往返 |
ncu 先看 SoL:任一接近 90% 就不用改,都低才查延迟
| ncu 读法 | 判断 |
|---|---|
| SOL > 80% | 到顶,换算法或减字节 |
| 两者 < 40–50% | latency-bound:看 stall 原因与占用率 |
| 寄存器 > 32 | 压占用率;128+ 是 GEMM 常态 |
| stall long scoreboard 60% + 占用 25% | 等 HBM 且 warp 不够——加 ILP 或提占用 |
不变:每元素两读一写、一次加法;先正确性、后性能。
改变:线程索引 → 每核一段再分 tile;直接全局 load/store → 显式 CopyIn → Compute → CopyOut。
| CUDA 概念 | Ascend C 的对应与边界 |
|---|---|
| grid / blockIdx | launch 的 blockDim / GetBlockIdx;不要混淆 block 内线程数 |
| 显式暂存与生产者/消费者同步 | TPipe / TQue / LocalTensor;队列不是 warp |
| cp.async 与流水 | 两块 buffer 提供重叠机会,不承诺加速 |
| Tensor Core / CUTLASS | Cube / Matmul;只是功能角色与分层分块的类比 |
Table: CUDA 与 Ascend C 的核心映射
[8,2048]:8 个逻辑块,每块 16 轮 × 128 元素。| 篇 | 在 Roofline 上做了什么 |
|---|---|
| 01 | 画出这张图:ridge 156 / 295 |
| 02 · 03 | 斜线上的 kernel 贴到 90% 带宽;之后只能减字节(融合) |
| 04 | 归约形态让 memory-bound kernel 离斜线 10 倍;shuffle 贴回去 |
| 05 · 06 | 分块把 I 从 0.25 抬到 32 再到 1365,从斜线爬上屋顶;Tensor Core 把屋顶抬高 16 倍 |
| 07 | 编译器替你走前六篇的路,差 10% |
| 08 | FlashAttention 把 attention 从斜线搬到屋顶(减 IO) |
| 09 | INT4 沿斜线右移(减字节);FP8 同时右移和抬顶 |
| 10 | SOL 告诉你现在在图的哪里 |
| 11 | 账本可迁移,执行模型与 profiler 要换;小 Add 不代表 HBM 带宽 |
| 篇 | 一个公式 / 一个数 |
|---|---|
| 01 | \(T = \max(F/P,\ B/BW)\);ridge 156 / 295 |
| 02 | 3 GiB → 1.6 ms;80–92% |
| 03 | 32 B sector;1.2 MB 在飞;≥ 22 warp / SM |
| 04 | bank = (addr/4) mod 32;\(l = l_a e^{m_a - m} + l_b e^{m_b - m}\) |
| 05 | \(MNK(1/BM + 1/BN)\);128×128 → I = 32 |
| 06 | mma m16n8k16 = 4096 FLOP;swizzle |
| 07 | 60 行 vs 200–300 行;80–95% |
| 08 | 132 → 66 MiB(实际 4);\(\Theta(N^2d^2/M)\) |
| 09 | W4A16 I ≈ 4M,交叉 M ≈ 40 |
| 10 | SOL > 80% 到顶;rtol 1.6e-2 |
| 11 | 8 × 2048;16 × 128;1536 B 队列 payload;性能留待 NPU 实测 |
/gpu-kernel-engineering.html;通关自测在系列总结