9. 最佳实践
把前八章的散点经验汇成一张可对照的速查表:哪些模式应该养成肌肉记忆、哪些坑必须绕开、出问题时去哪里找答案。
学会语法是入门,写出能上生产的核函数才是出师。本章把 Triton 项目中反复出现的工程经验整理成 模式、清单、流程 三类,再附调试技巧、PyTorch 集成方法与社区资源索引。建议遇到具体问题时直接跳到对应小节查。
本章内容概览
- 9.1 常见编程模式(pattern catalogue)
- 9.2 性能优化 checklist
- 9.3 调试技巧
- 9.4 与 PyTorch 集成的最佳方式
- 9.5 社区资源与进阶学习路径
- 9.6 中英文术语对照表
9.1 常见编程模式
把这几个模板背下来,80% 的核函数都能套出来。
9.1.1 1D elementwise(向量加法骨架)
最基础的模板,所有 elementwise 算子(add / mul / relu / sigmoid …)都是这个变种:
@triton.jit
def elementwise_kernel(x_ptr, y_ptr, out_ptr, n_elements,
BLOCK_SIZE: tl.constexpr):
pid = tl.program_id(axis=0)
offsets = pid * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
mask = offsets < n_elements
x = tl.load(x_ptr + offsets, mask=mask)
y = tl.load(y_ptr + offsets, mask=mask)
tl.store(out_ptr + offsets, x + y, mask=mask)
grid = lambda meta: (triton.cdiv(n_elements, meta['BLOCK_SIZE']),)
elementwise_kernel[grid](x, y, out, n, BLOCK_SIZE=1024)适用场景
N 维任意张量的 elementwise——把张量 .contiguous().view(-1) 拍平成 1D 即可。
9.1.2 按行 reduce(softmax 骨架)
每个程序实例处理一行(或几行),整行装进 SRAM 做 reduce:
@triton.jit
def row_reduce_kernel(x_ptr, out_ptr, n_cols,
stride_row,
BLOCK_SIZE: tl.constexpr):
row = tl.program_id(0)
cols = tl.arange(0, BLOCK_SIZE)
mask = cols < n_cols
x = tl.load(x_ptr + row * stride_row + cols,
mask=mask, other=-float('inf'))
# 这里做你的 reduce
result = tl.max(x, axis=0) # 或 tl.sum / tl.min ...
tl.store(out_ptr + row, result)约束:BLOCK_SIZE >= n_cols,且必须是 2 的幂。n_cols 超过 ~32K 时必须改成多块 reduce(参考官方 02-fused-softmax 的多块版本)。
9.1.3 2D 分块 (matmul 骨架)
每个程序实例算输出的一个 [BM, BN] 分块,K 维做内循环:
@triton.jit
def matmul_kernel(a_ptr, b_ptr, c_ptr,
M, N, K,
stride_am, stride_ak, stride_bk, stride_bn,
stride_cm, stride_cn,
BLOCK_SIZE_M: tl.constexpr,
BLOCK_SIZE_N: tl.constexpr,
BLOCK_SIZE_K: tl.constexpr):
pid_m = tl.program_id(0)
pid_n = tl.program_id(1)
offs_m = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
offs_n = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
offs_k = tl.arange(0, BLOCK_SIZE_K)
acc = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
a = tl.load(a_ptr + offs_m[:, None] * stride_am
+ (k * BLOCK_SIZE_K + offs_k)[None, :] * stride_ak)
b = tl.load(b_ptr + (k * BLOCK_SIZE_K + offs_k)[:, None] * stride_bk
+ offs_n[None, :] * stride_bn)
acc = tl.dot(a, b, acc)
c = acc.to(tl.float16)
tl.store(c_ptr + offs_m[:, None] * stride_cm
+ offs_n[None, :] * stride_cn, c)三件套
fp32 累加 + tl.dot + grouped ordering,是所有 GEMM 类核函数的标配。完整版见 examples/03_matmul.py。
9.1.4 持久化核函数(处理多行 / 多分块)
当程序实例数远大于 SM 数时,不如让每个 SM 上常驻一组程序实例,循环消费多个任务:
@triton.jit
def persistent_kernel(..., n_rows, BLOCK_SIZE: tl.constexpr):
row_start = tl.program_id(0)
row_step = tl.num_programs(0)
for row in tl.range(row_start, n_rows, row_step):
# 处理这一行
...
grid = (NUM_SM,) # 而不是 (n_rows,)收益:减少程序实例 launch 开销;配合 num_stages > 1 能更好流水化 DRAM → SRAM 拷贝。Spheron 实测在 H100 上对 softmax / layernorm 这类访存密集型算子能提升 10~20%。
9.1.5 在线 reduce + 串行更新(FlashAttention 骨架)
主循环里维护 running 统计量 + 修正旧累加器:
m = tl.zeros([BLOCK_M], dtype=tl.float32) - float('inf')
l = tl.zeros([BLOCK_M], dtype=tl.float32)
acc = tl.zeros([BLOCK_M, D], dtype=tl.float32)
for start_n in range(0, N, BLOCK_N):
new_block = compute(...) # 本块原始值
m_new = tl.maximum(m, tl.max(new_block, axis=1))
alpha = tl.exp(m - m_new)
p = tl.exp(new_block - m_new[:, None])
l = l * alpha + tl.sum(p, axis=1)
acc = acc * alpha[:, None] + tl.dot(p, V_block)
m = m_new
out = acc / l[:, None]详见第 8 章。这个模板可以推广到任何需要"流式 reduce"的场景:在线 layernorm、流式 top-k、长序列 RNN cell 等。
9.2 性能优化 checklist
按这个顺序检查,能解决 80% 的性能问题。
✅ 第一步:先确认是不是 memory-bound
ms = triton.testing.do_bench(lambda: my_kernel(x, y))
bytes_moved = x.numel() * x.element_size() * 2 # 读 1 次 + 写 1 次
achieved_bw = bytes_moved * 1e-9 / (ms * 1e-3)
peak_bw = 1500 # A100 ~1.5 TB/s, H100 ~3 TB/s
print(f'{achieved_bw / peak_bw * 100:.1f}% of peak HBM BW')- 如果已经 > 70%——你的核函数是 memory-bound,继续优化只能融合更多算子。
- 如果 < 30%——大概率是 launch 开销或 SRAM / 寄存器没用足,继续往下查。
✅ 第二步:用 @triton.autotune 探最优 config
不要手动猜 BLOCK_SIZE / num_warps / num_stages。最低限度给一个 4~8 个 config 的小集合:
@triton.autotune(
configs=[
triton.Config({'BLOCK_SIZE': 1024}, num_warps=4),
triton.Config({'BLOCK_SIZE': 1024}, num_warps=8),
triton.Config({'BLOCK_SIZE': 2048}, num_warps=4),
triton.Config({'BLOCK_SIZE': 2048}, num_warps=8),
],
key=['n_elements'],
)autotune 的副作用
- 首次每个 key 会跑遍所有 config,可能耗时数秒到数分钟。社区有报告单个 FA 核函数 autotune 跑了 82 分钟(GitHub issue #9401)。
- 写入型核函数必须配
reset_to_zero或restore_value,否则 autotune 多次运行后输出被累加。 - 用环境变量
TRITON_PRINT_AUTOTUNING=1实时打印选中的 config。
✅ 第三步:检查 BLOCK_SIZE 与 SRAM 占用
经验法则:
| 数据类型 | 单分块推荐字节数上限 |
|---|---|
| matmul 输入分块 (fp16) | < 32 KB / 分块 |
| matmul 单核函数总 SRAM | < 64 KB(保证 4+ warps/SM 占用率) |
| softmax 单行 SRAM | < SM 共享内存的 1/4 |
读取核函数实际 SRAM 占用:
kernel = my_kernel.warmup(..., grid=(1,))
kernel._init_handles()
print(f'shared: {kernel.metadata.shared} bytes')
print(f'n_regs: {kernel.n_regs}')✅ 第四步:检查 num_stages
软件流水线深度,控制 DRAM→SRAM 的双/多缓冲:
| 场景 | NVIDIA 推荐 | AMD 推荐 |
|---|---|---|
| 单 GEMM | 2~5 | 0 |
| GEMM + 融合(FA) | 2~3 | 1 |
| 无 GEMM(softmax) | 2~4 | 1 |
平台差异
AMD ROCm 上 num_stages 推荐 0~1,与 NVIDIA 完全不同。跨硬件部署时务必分平台 autotune。来源:ROCm 官方调优指南。
✅ 第五步:检查 grouped 程序实例排序(matmul / conv 类)
当输出有二维分块结构时,朴素 pid → (pid_m, pid_n) 行主序会浪费 L2。改用 grouped ordering:
num_pid_in_group = GROUP_SIZE_M * num_pid_n
group_id = pid // num_pid_in_group
first_pid_m = group_id * GROUP_SIZE_M
group_size_m = min(num_pid_m - first_pid_m, GROUP_SIZE_M)
pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m)
pid_n = (pid % num_pid_in_group) // group_size_mA100 上官方报告:220 → 245 TFLOPS(+11%)。
✅ 第六步:消除冗余计算与冗余 mask
- 内层循环里的常量计算上移到循环外
- 用
% M折回越界索引,省掉一组 mask - 用
tl.assume(stride > 0)给编译器整数分析器提示
✅ 第七步:必要时启用 Hopper+ 特性
- TMA:用
tl.make_block_ptr或TensorDescriptor替代手写指针算术 - Warp specialization:
tl.range(..., warp_specialize=True)让 load 和 compute 并行 - FP8:H100/B200 上 GEMM/FA 可再提速 ~1.6×
9.3 调试技巧
9.3.1 TRITON_INTERPRET:CPU 解释模式
最重要的调试武器。设置环境变量后,核函数在 CPU 上单线程解释执行,print() 和 Python 调试器都能用:
TRITON_INTERPRET=1 python my_script.py@triton.jit
def my_kernel(x_ptr, ...):
pid = tl.program_id(0)
x = tl.load(x_ptr + offsets, mask=mask)
print(f'pid={pid}, x={x}') # 在解释模式下能正常打印
...性能极差
解释模式比 GPU 慢几个数量级,只用来定位逻辑 bug,跑通后立刻关闭。
9.3.2 device_print:GPU 上的打印
不切到解释模式也想看中间值,用 tl.device_print:
tl.device_print('qk =', qk) # 注意 GPU 上输出会大量乱序9.3.3 dump IR / PTX 看编译结果
TRITON_CACHE_DIR=./triton_cache python my_script.py
ls -R ./triton_cache # 找到 .ttir / .ttgir / .ptx 文件.ttir:Triton dialect MLIR.ttgir:GPU dialect MLIR(已做 lowering).ptx:最终 NVIDIA PTX
看到生成的 PTX 中出现大量 ld.local / st.local,说明发生了 寄存器溢出——把 BLOCK_SIZE 或 num_warps 调小试试。
9.3.4 用 Nsight Compute 看硬件指标
ncu --set full --kernel-id ::my_kernel:1 \
--target-processes all python my_script.py关键指标:
| 指标 | 含义 |
|---|---|
dram__bytes.sum | DRAM 流量,对比理论值看融合是否生效 |
sm__pipe_tensor_cycles_active.avg.pct_of_peak_sustained_active | Tensor Core 利用率 |
l1tex__data_pipe_lsu_wavefronts_mem_lg_cmd_load.sum | local memory 访问数 = 寄存器溢出量 |
smsp__warps_active.avg.pct_of_peak_sustained_active | warp 占用率 |
9.3.5 正确性检查清单
写完核函数第一时间跑这四件套:
# 1. 与 PyTorch 朴素实现对比(默认 atol/rtol)
torch.testing.assert_close(y_triton, y_torch, atol=1e-2, rtol=1e-2)
# 2. 边界 case:尺寸不是 BLOCK_SIZE 整数倍
test_shapes = [(1,), (BLOCK_SIZE - 1,), (BLOCK_SIZE,),
(BLOCK_SIZE + 1,), (BLOCK_SIZE * 3 + 7,)]
# 3. dtype 全覆盖
for dtype in [torch.float32, torch.float16, torch.bfloat16]:
...
# 4. 非 contiguous 输入(transpose / slice 后)
x_t = x.transpose(-2, -1) # 故意打乱 stride
y = my_kernel(x_t)9.4 与 PyTorch 集成
9.4.1 直接包装成 Python 函数(最简单)
适合脚本和研究代码:
def my_op(x: torch.Tensor) -> torch.Tensor:
out = torch.empty_like(x)
my_kernel[grid](x, out, n_elements=x.numel(), BLOCK_SIZE=1024)
return out9.4.2 注册为 torch.library 自定义算子(推荐生产用)
让 torch.compile、torch.export、AOTInductor 等能识别你的核函数:
@torch.library.custom_op("mylib::triton_softmax", mutates_args=())
def triton_softmax(x: torch.Tensor) -> torch.Tensor:
return softmax(x) # 调用你的 Triton wrapper
@triton_softmax.register_fake
def _(x):
return torch.empty_like(x) # meta tensor 推导输出 shape9.4.3 配合 torch.compile
torch.compile 的 Inductor 后端会自动 codegen Triton 核函数,但有时你想 手写核函数让编译器调用:
@torch.compile(mode='max-autotune')
def forward(x):
return triton_softmax(x) # 自动算子会被识别为 black box何时手写 vs 让 Inductor 生成
- 标准 elementwise / reduce / matmul:让 Inductor 生成
- 复杂融合(attention、自定义 norm、稀疏算子):手写 + torch.library 注册
- 研究阶段快速迭代:手写
9.4.4 自定义 backward
写训练用的核函数,必须实现 backward。推荐 torch.autograd.Function:
class MyOp(torch.autograd.Function):
@staticmethod
def forward(ctx, x):
y = my_forward_kernel(x)
ctx.save_for_backward(x, y)
return y
@staticmethod
def backward(ctx, dy):
x, y = ctx.saved_tensors
dx = my_backward_kernel(x, y, dy)
return dx
my_op = MyOp.apply9.5 社区资源与进阶学习路径
9.5.1 一手权威资料
| 资源 | 用途 |
|---|---|
| Triton 官方文档 | 语法、API、教程 |
| 官方 tutorial 仓库 | 01~06 完整核函数 |
| Triton GitHub Discussions | 问答、版本通告 |
| OpenAI 原始博客 | 设计哲学 |
9.5.2 深度教程与视频
- GPU MODE Lecture 14(Practitioner's Guide)—— christianjmills.com 笔记
- PyTorch 官方 Triton 系列博文 —— pytorch.org/blog/triton-kernel-compilation-stages
- Towards Data Science: Learning Triton One Kernel at a Time —— towardsdatascience.com
- FlashAttention 教学版 —— hkproj/triton-flash-attention(含视频)
9.5.3 进阶论文
| 论文 | 推荐理由 |
|---|---|
| Tillet et al. Triton: An IR for Tiled Neural Network Computations (MAPL 2019) | Triton 的设计起源 |
| Dao et al. FlashAttention (NeurIPS 2022) | online softmax + tiling 的开山之作 |
| Dao FlashAttention-2 (2023) | outer-loop on Q 的关键优化 |
| Rabe & Staats Self-attention Does Not Need O(N²) Memory (2021) | online softmax 的早期推导 |
9.5.4 优秀开源项目(可作为代码参考)
FlagOpen/FlagGems—— 大模型常用算子的 Triton 实现集linkedin/Liger-Kernel—— LinkedIn 开源的训练加速核函数集unslothai/unsloth—— LoRA / QLoRA 训练加速triton-lang/kernels—— 官方维护的核函数仓库
9.5.5 学习路径建议
- 第 1 周:跑通本教程 01~04 章 +
examples/01_vector_add.py,理解程序实例 / 块 / mask - 第 2 周:05~07 章 +
examples/02_fused_softmax.py+examples/03_matmul.py,体验融合与 autotune - 第 3~4 周:第 8 章 +
examples/04_flash_attention.py+ 阅读官方06-fused-attention.py - 进阶:挑一个开源项目(推荐 FlagGems)读 5~10 个核函数,尝试给一个简单算子提 PR
- 专家:研究 Triton 编译器源码(
triton/python/triton/compiler/)、读 IR dump、对比不同后端
9.6 中英文术语对照表
本表是全教程统一术语的权威索引。前 6 行是贯穿全教程的核心术语,请优先记住。
| 中文(教程统一) | 英文 | 说明 |
|---|---|---|
| 核函数 | kernel | GPU 上并行执行的函数;@triton.jit 装饰的 Python 函数 |
| 程序实例 | program | Triton 的并行单元,约等于 CUDA 的 thread block |
| 网格 | grid | 程序实例构成的 1D/2D/3D 启动布局 |
| 块 / 分块 | block / tile | 单个程序实例一次处理的数据块;二维场景常称 tile / 分块 |
| 共享内存 | shared memory (SRAM) | SM 内的高速暂存器,Triton 编译器自动管理 |
| warp | warp | NVIDIA 上 32 线程的同步执行组(不翻译) |
| 线程 | thread | CUDA 的最小执行单元;Triton 中由编译器自动管理 |
| 块大小 | block size / BLOCK_SIZE | 单个程序实例处理的元素数,必须是 2 的幂 |
| 编译期常量 | compile-time constant / constexpr | 用 tl.constexpr 声明 |
| 指针算术 | pointer arithmetic | ptr + offsets 风格的内存访问 |
| 掩码 | mask | 边界保护的 bool 向量 |
| 寄存器 | register | 线程私有的最快存储 |
| 寄存器溢出 | register spill | 寄存器不够,落到 local memory(其实是 DRAM) |
| 全局内存 | global memory / HBM | 设备主显存 |
| 高带宽显存 | HBM | High Bandwidth Memory |
| 访存合并 | memory coalescing | 相邻线程访问相邻地址 |
| 软件流水线 | software pipelining | num_stages 控制 |
| 算子融合 | kernel fusion / operator fusion | 多个算子合并到一个核函数 |
| 自动调优 | autotune | 自动搜索最优 meta-parameter |
| 张量核 | Tensor Core | NVIDIA 矩阵乘加专用硬件单元 |
| 矩阵乘 | matmul / GEMM | General Matrix-Matrix Multiplication |
| 在线 softmax | online softmax | 流式归一化算法 |
| 重算 | recomputation | backward 时不存中间值,重新计算 |
| 因果掩码 | causal mask | LLM 中只能看左边 token 的下三角 mask |
| 注意力 | attention | Transformer 核心算子 |
| 占用率 | occupancy | SM 上同时驻留的 warp 数 / 最大 warp 数 |
| 计算密集 | compute-bound | 性能瓶颈在算力 |
| 访存密集 | memory-bound | 性能瓶颈在带宽 |
| 启动开销 | launch overhead | 核函数 launch 本身的固定耗时 |
| 持久化核函数 | persistent kernel | 每 SM 常驻、循环消费任务的核函数模式 |
| 异步拷贝 | async copy / cp.async | DRAM ↔ SRAM 的非阻塞拷贝 |
| Warp 专业化 | warp specialization | 不同 warp 承担不同角色(load vs compute) |
| 张量内存加速器 | TMA (Tensor Memory Accelerator) | Hopper+ 的异步拷贝硬件 |
全教程术语约定
代码标识符(add_kernel、BLOCK_SIZE、tl.program_id、grid = lambda meta: ... 等)和已成习惯的英文缩写(GEMM、SRAM、HBM、warp、SM、PTX、IR)保留英文不翻译;其余概念性词汇按本表的"中文(教程统一)"列使用。
9.7 Nsight Compute 实战 Workflow
ncu 是 GPU 性能优化的"X 光机"。会读 ncu 报告,你就能从猜测优化跨越到证据驱动优化。
9.7.1 完整命令模板
# 基础:抓所有 kernel 的完整指标(耗时较长,~30 ncu / kernel)
ncu --set full \
--target-processes all \
-o profile_full \
python my_script.py
# 生产推荐:只抓指定 kernel + 限制 launch 次数(避免超大 .ncu-rep)
ncu --set full \
--kernel-name 'matmul_kernel' \
--launch-count 5 \
--launch-skip 2 \
--target-processes all \
-o profile_matmul \
python my_script.py
# 最快:只看 SOL chart,单 kernel 单 launch
ncu --set default \
--kernel-name 'softmax_kernel' \
--launch-count 1 \
-o profile_quick \
python my_script.py关键参数:
| 参数 | 用途 |
|---|---|
--set basic | LaunchStats + Occupancy + SpeedOfLight + WorkloadDistribution(4 个 section) |
--set default | basic + ComputeWorkload + MemoryWorkload + Scheduler + WarpState |
--set full | default + Source(含 source-line correlation) + Roofline |
--set roofline | 单独跑 roofline 图 |
--launch-count N | 只抓前 N 次 launch(autotune 期间常常 launch 几十次) |
--launch-skip M | 跳过前 M 次(典型用 --launch-skip 2 跳过 warmup launch) |
--replay-mode kernel | 每个 kernel 单独 replay,比 application 快得多 |
--cache-control none | 不清 L2,更接近真实 inference 性能 |
ncu 的 replay 成本
--set full 需要 30~60 次 kernel replay 才能采全所有 counter;70B 模型 forward 一次 profile 要 20~30 分钟(H100 SXM5 spot 约 $0.55~$0.83)。生产 debug 优先用 --set default,速度快 10×。
9.7.2 关键 section 解读
在 Nsight Compute UI 打开 .ncu-rep 文件后,从上往下依次看:
Section 1: GPU Speed of Light (SOL) Chart
这是最重要的一张图。两根柱子告诉你瓶颈:
Compute (SM) Throughput: ████████░░░░░░░░ 42%
Memory Throughput: ███████████░░░░░ 68%判断规则:
| 情况 | 含义 | 该往哪个方向优化 |
|---|---|---|
| Compute > 80% | Compute-bound | 减少计算(降精度 / 用 Tensor Core) |
| Memory > 80% | Memory-bound | 减少 HBM 流量(融合 / 缓存) |
| 都 < 50% | Latency-bound | 增大 occupancy / 用 cp.async |
| Compute ≈ Memory ≈ 70% | 平衡,已经接近最优 | 微调 |
Section 2: Memory Workload Analysis
里面最关键的指标:
| 指标 | 含义 | 健康值 |
|---|---|---|
dram__bytes.sum | 总 HBM 流量 | 用来验证融合是否真的减流量了 |
l1tex__t_bytes_pipe_lsu_mem_global_op_ld.sum | global memory load 字节数 | 越接近 dram 流量越好(说明 L2 没缓存住) |
lts__t_sectors_op_read_hit_rate.pct | L2 cache 命中率 | matmul 应 ≥ 60%;不到说明 grouped ordering 没生效 |
l1tex__data_pipe_lsu_wavefronts_mem_lg_cmd_load.sum | local memory load = 寄存器溢出量 | 应该 = 0 |
smsp__inst_executed_op_global_st.sum | global store 指令数 | 太多说明输出 mask 没合并 |
一眼判断寄存器 spill
看 l1tex__data_pipe_lsu_wavefronts_mem_lg_cmd_load.sum:
- = 0 → 完美
0 → spill 了,减小 BLOCK_SIZE 或 num_warps 试试
- 比 global load 还大 → 严重 spill,性能基本崩了
Section 3: Compute Workload Analysis
| 指标 | 含义 |
|---|---|
sm__pipe_tensor_cycles_active.avg.pct_of_peak_sustained_active | Tensor Core 利用率 |
smsp__inst_executed_pipe_fma.sum.pct_of_peak_sustained_active | FMA pipeline 利用率 |
smsp__inst_executed_pipe_xu.sum.pct_of_peak_sustained_active | Special function (exp/log) pipeline 利用率 |
判断:
- Tensor Core > 70% → matmul / attention 优化到位
- FMA > 50% 且 Tensor > 50% → 算子在 fp32 fallback,没用上 Tensor Core
- XU pipeline > 80% → softmax / exp 是瓶颈,考虑 FA3 的 ping-pong
Section 4: Occupancy
Theoretical Occupancy: 66.67% (受限于 Block Size)
Achieved Occupancy: 62.13% (实际)
Achieved Active Warps: 32.05 (Per SM)理论与实际的差距 < 5% 说明 launch 充分。差距大说明 grid 太小(waves 不够)。
Section 5: Warp State Statistics(重点)
这是定位 latency 瓶颈的金钥匙。ncu 用 PC sampling 采样 warp 在每个时刻处于什么状态:
| State | 含义 | 怎么修 |
|---|---|---|
Stall Long Scoreboard | 等 HBM load | 增大 num_stages,提升 prefetch |
Stall Short Scoreboard | 等 SRAM load | 重排访问模式,减少 bank conflict |
Stall Wait | 等 __syncthreads | 减少同步点 |
Stall MIO Throttle | LSU 队列满 | 减少 load/store 指令数 |
Stall LG Throttle | local/global throttle | 寄存器溢出,必须解决 |
Stall IMC | 等指令缓存 | kernel 太大,做 inlining 优化 |
Selected | 正在执行 | 越高越好 |
经验:Stall Long Scoreboard > 30% 是 memory-bound 的铁证。
9.7.3 定位瓶颈的标准流程
1. 看 SOL Chart → 判断 compute / memory / latency 主要瓶颈
↓
2. 看 Warp State → 定位具体的 stall 原因
↓
3. 看 Source Correlation → 找到 stall 集中在哪几行代码
↓
4. 改代码 → 重新 profile → 对比 Baseline9.7.4 Triton kernel 的 source mapping 技巧
Triton kernel 在 ncu 里看到的是 PTX/SASS,不是 Python 源码。要做 source correlation:
# 1. 让 Triton 保留中间 IR
export TRITON_CACHE_DIR=./triton_cache
export TRITON_KERNEL_DUMP=1
python my_script.py
# 2. 找到对应的 .ttgir / .ptx
ls ./triton_cache/<hash>/
# 3. ncu 里的 "Source" 页签可以选 PTX 视图,
# 每条 PTX 指令上方有 .loc 注释指回原 Python 文件:line
# 例如: .loc 1 145 12 ← 表示原文件第 145 行第 12 列更直接的办法:用 Triton 自带的 Proton profiler,它原生支持 Triton 源码级 attribution:
import triton.profiler as proton
@triton.jit
def my_kernel(...): ...
with proton.scope("my_kernel"):
my_kernel[grid](...)
# 自动生成 chrome trace,里面每条 op 都指回 Python 行号9.7.5 实战案例:从 ncu 报告优化 GEMM
某 fp16 matmul kernel ncu 报告:
SOL Chart:
Compute SM Throughput: 45%
Memory Throughput: 82% ← 瓶颈
Memory Workload:
dram__bytes.sum 4.2 GB
L2 Hit Rate 31% ← 偏低
Local Memory Load (spill) 0 ✓
Warp State:
Stall Long Scoreboard 38% ← 等 HBM
Selected 22%诊断:memory-bound + L2 命中率低 + 等 HBM。问题是 grouped ordering 没生效。
修复后:
Compute SM Throughput: 72%
Memory Throughput: 78%
L2 Hit Rate 68% ← 提升 2×
Stall Long Scoreboard 18%
TFLOPS: 220 → 245 (+11%)9.8 常见性能反模式 (Anti-patterns)
下表汇总了线上踩过最多的坑:
| # | 反模式 | 症状 | 原因 | 修复 |
|---|---|---|---|---|
| 1 | 不必要的 tl.debug_barrier | kernel 比预期慢 30~50% | barrier 强制所有 warp 同步,阻塞流水 | 删掉,只在调试时加 |
| 2 | 过大的 BLOCK_SIZE 导致 spill | ncu 显示 Local Memory Load > 0 | 寄存器不够,编译器降级到 local memory(实际是 HBM) | BLOCK_SIZE 减半 或 num_warps 增大 |
| 3 | Naive reduction(多次扫描) | softmax 等 reduce 算子慢 | 写成 x.max(); x.exp(); x.sum(); 三次扫描 | 用 online algorithm 一次扫 |
| 4 | 不对齐的内存访问 | DRAM throughput 偏低(< 50%) | tensor 非 contiguous 或 BLOCK_SIZE 非 128 字节对齐 | 调用前 .contiguous();BLOCK_SIZE 选 ≥ 128/sizeof 的 2 的幂 |
| 5 | 过度融合导致寄存器爆炸 | autotune 选出来的 config 反常(很小 BLOCK) | 中间累加器太多,编译器只能压 BLOCK | 拆成两个 kernel;或减少同时存活的 fp32 临时变量 |
| 6 | 忘了 fp32 累加 | matmul 数值误差 > 1e-2 | 累加器跟输入同 dtype(fp16) | acc = tl.zeros(..., dtype=tl.float32) |
| 7 | tl.dot 之前没 cast 到 fp16 | Tensor Core 没启用,性能 1/4 | Triton 不会自动 cast,fp32 → 走 FMA pipeline | 显式 a.to(tl.float16) |
| 8 | key 没包含动态维度 | autotune 选错 config | 比如 key=['N'] 但 M 也变化 | key=['M', 'N', 'K'] 全部包含 |
| 9 | autotune 写入 kernel 没 reset_to_zero | 输出值变成真实值 ×N | autotune 多次执行,原子累加 | reset_to_zero=['output_ptr'] |
| 10 | 没用 grouped ordering 的 matmul | L2 hit rate < 40% | 朴素 pid → (m, n) 行主序 | 用 super-grouping,A100 +11% |
| 11 | 同步 torch.cuda.synchronize() 在 hot loop | CPU 端 launch 开销暴增 | 每次都强制同步等待 | 用 CUDA event 或 batched launch |
| 12 | 小 BLOCK_SIZE × 大 num_warps | 实际 occupancy 低 | 每 warp 工作量太少,调度开销吃掉收益 | BLOCK_SIZE × num_warps 至少 4096 元素 |
| 13 | kernel 内 print / device_print | 性能崩 10×+ | 每次 print 都触发隐式同步 | 调试完立刻删 |
| 14 | 指针算术里有动态步长 | 编译器无法优化 | ptr + i * stride 中 stride 不是 constexpr | 把 stride 提为 tl.constexpr(如果可能) |
| 15 | kernel 太大(> 5000 PTX 指令) | SASS 编译慢 + I-cache miss | 一个 kernel 融合太多算子 | 切分到 2~3 个 kernel |
反模式 5 的真实案例
某团队把 RMSNorm + Linear + GELU + Linear + dropout 五个算子融成一个 kernel。autotune 选出的最佳 config 是 BLOCK_SIZE=32(异常小),ncu 显示 Local Memory Load = 2.1 GB(巨量 spill)。拆成 "RMSNorm + Linear1" 和 "GELU + Linear2 + Dropout" 两个 kernel 后,性能提升 2.3×。
教训:融合不是越多越好,超过 4 个算子就要 ncu 验证寄存器使用情况。
9.9 CI/CD 中的性能回归测试
性能优化做了一遍,下次 PR 怎么保证不退化?答案是把 benchmark 加入 CI。
9.9.1 triton.testing.do_bench 的稳定性考量
do_bench 的默认参数 warmup=25ms, rep=100ms 在很多场景下不够稳定:
# 已知问题:GitHub issue #2306
# do_bench 默认 warmup=25ms 会让结果偏高 30%
# 因为 Triton kernel 编译后 PTX cache miss、L2 cache 状态未稳态CI 中推荐的参数:
from triton.testing import do_bench
# 慢但稳:warmup 100ms + 500ms 实测
ms = do_bench(
lambda: my_kernel(x, y),
warmup=100, # 必须 ≥ 100ms 才能让 SM 时钟达到 boost 频率
rep=500, # 多采样减少噪声
quantiles=(0.5, 0.2, 0.8), # 返回中位 + 20/80 分位
return_mode='median',
)
# 用 median 比 mean 更鲁棒(不受偶发尖刺影响)对极短 kernel(< 10 μs),建议改用 do_bench_proton,CPU 开销更小:
from triton.testing import do_bench_proton
ms = do_bench_proton(lambda: my_kernel(x, y), warmup=100, rep=500)9.9.2 设定性能阈值和报警策略
不要 hardcode "TFLOPS ≥ 200",而是 相对基线 阈值:
# benchmark/test_perf_regression.py
import json
import pytest
from triton.testing import do_bench
BASELINE = json.load(open('benchmark/baseline.json'))
TOLERANCE = 0.05 # 允许 5% 退化
@pytest.mark.parametrize("M,N,K", [(4096, 4096, 4096), (8192, 8192, 8192)])
def test_matmul_perf(M, N, K):
a, b = make_input(M, N, K)
ms = do_bench(lambda: my_matmul(a, b), warmup=100, rep=500)
baseline_ms = BASELINE[f'matmul_{M}_{N}_{K}']
regression = (ms - baseline_ms) / baseline_ms
assert regression < TOLERANCE, \
f"Perf regression {regression*100:.1f}% (current={ms:.3f}ms, baseline={baseline_ms:.3f}ms)"更细致的策略:
| 退化幅度 | CI 行为 |
|---|---|
| < 2% | Pass(噪声范围) |
| 2~5% | Warning(提醒 PR 作者,但不 block) |
| 5~10% | Soft fail(要求 reviewer 确认是有意为之) |
| > 10% | Hard fail(必须修复或说明原因) |
9.9.3 GitHub Actions 集成示例
.github/workflows/perf-regression.yml
name: Performance Regression
on:
pull_request:
paths:
- 'src/kernels/**'
- 'benchmark/**'
jobs:
benchmark:
runs-on: [self-hosted, gpu, h100] # 必须用 self-hosted GPU runner
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Setup
run: |
pip install -e .
# GPU clock 锁频,减少 benchmark 噪声
sudo nvidia-smi -lgc 1980,1980 # H100 boost clock
- name: Warmup compile cache
run: |
export TRITON_CACHE_DIR=/tmp/triton_cache
python benchmark/warmup.py # 预编译所有 kernel
- name: Run benchmark
run: |
export TRITON_CACHE_DIR=/tmp/triton_cache
pytest benchmark/test_perf_regression.py \
--benchmark-json=current.json \
-v
- name: Compare against main baseline
run: |
python benchmark/compare.py \
--current current.json \
--baseline benchmark/baseline.json \
--tolerance 0.05
- name: Comment results on PR
if: always()
uses: actions/github-script@v7
with:
script: |
const report = require('./benchmark/report.json');
const body = formatTable(report); // 生成 markdown 表格
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body
});
- name: Unlock GPU clock
if: always()
run: sudo nvidia-smi -rgc9.9.4 减少 CI 噪声的最佳实践
GPU benchmark 在 CI 里最大的敌人是噪声。常见的优化:
| 措施 | 效果 |
|---|---|
锁 GPU 频率:nvidia-smi -lgc <freq> | 减少 ±5% 频率波动 |
| 预热 100 ms+:让时钟和 cache 达到稳态 | 减少首次 launch 偏高 |
| 跑 ≥ 500 ms repetitions | 减少采样噪声到 ±1% |
| 专用 self-hosted runner,禁用其他 workload | 避免和其他进程抢 GPU |
| 冷启动 vs 热启动分开测:分别记录 first call 和 steady state | 暴露 autotune / 编译开销 |
| 跑 N 次取 median,记 P20/P80 | 暴露异常 |
| 每次 CI 都对比同一台 runner 的历史 baseline,而非跨硬件 | GPU 个体差异最高 10% |
9.9.5 自动更新 baseline
baseline 不应人工维护。每次 main 分支合并后自动更新:
# .github/workflows/update-baseline.yml
on:
push:
branches: [main]
jobs:
update-baseline:
runs-on: [self-hosted, gpu, h100]
steps:
- uses: actions/checkout@v4
- run: |
pytest benchmark/ --benchmark-json=new_baseline.json
# 取过去 7 天的 median 作为 baseline,抗噪
python benchmark/rollup_baseline.py \
--history-days 7 \
--output benchmark/baseline.json
- uses: peter-evans/create-pull-request@v6
with:
title: "chore: update perf baseline"
branch: bot/update-baseline本章小结
- 常用模式 就五个:1D elementwise、按行 reduce、2D 分块、持久化核函数、在线 reduce。把模板背下来,新核函数大多是套模板 + 局部修改。
- 优化清单 按顺序走:先测是不是 memory-bound → autotune → 检查 SRAM / num_stages → grouped ordering → Hopper 特性。盲目调参往往无效。
- 调试 优先用
TRITON_INTERPRET=1+print;性能问题用 Nsight Compute 看 DRAM 流量与 Tensor Core 利用率。 - Nsight Compute workflow:SOL Chart 看瓶颈方向(compute/memory/latency)→ Warp State 找 stall 原因 → Source Correlation 定位代码行;记得用
--launch-skip跳过 autotune 期间的 launch。 - 性能反模式:寄存器 spill(
Local Memory Load > 0)、过度融合、忘 fp32 累加、key没包含动态维度等 15 个常见坑要避开。 - CI 性能回归测试:
do_bench(warmup=100, rep=500, return_mode='median')起步;GPU 锁频 + self-hosted runner + 相对基线阈值 5%;baseline 走自动 PR 更新。 - 集成 PyTorch 生产环境用
torch.library.custom_op注册,研究阶段直接函数包装即可。 - 学习路径 先吃透官方 01~06 tutorial,再读 FlagGems / Liger-Kernel 等开源项目,最后挑一个细分方向深入。
写在最后
Triton 的真正威力在于 让性能优化变成可读、可改、可复用的代码。当你能用 100 行 Triton 写出朴素 PyTorch 4 倍速、CUDA 90% 性能的核函数,并且半年后还能轻松看懂、扩展、移植到新硬件——那一刻你就明白为什么 PyTorch、FlashAttention、Liger、Unsloth 这些项目都选择了 Triton。
祝写出又快又稳的核函数!
思考题
本章思考题已升级到"综合实战级"——综合本章清单和前八章学到的所有概念。
autotune 实战排查:假设你写了一个原地累加核函数(
out += compute(x)),没有配置reset_to_zero。第一次调用 autotune 会发生什么?输出还正确吗?给出完整排查思路(从现象 → 根因 → 修复),并写出修复后的装饰器写法。跨章节性能诊断:在 A100(HBM 带宽 1.5 TB/s)上,你测得自己的 RMSNorm 核函数处理
[8192, 4096]fp16 输入耗时 0.45 ms。请算出有效带宽利用率,结合第 5、6、7 章的优化清单判断当前瓶颈,并按"先 memory-bound 验证 → 再融合 → 再调 BLOCK_SIZE → 最后 Hopper 特性"给出至少 3 个具体优化方向,每个方向写出预期收益区间。生产部署的全链路决策:你已经用 Triton 写了一个融合的
fused_layer_norm_residual,准备集成进 PyTorch 训练 pipeline。请对比"直接 Python 包装" vs "torch.library.custom_op注册"两种方案,在以下场景各自的优劣(要求结合本章 9.4 节内容给出具体函数调用与陷阱):- (a) Eager 模式训练
- (b)
torch.compile(mode='max-autotune')训练 - (c)
torch.export导出到 AOTInductor 部署
教程到此完结。第 1~4 章打下基础,5~7 章引入性能武器,8~9 章把所有积木拼成实战。建议把所有示例代码亲手跑一遍、把所有思考题尝试解答——真正的理解发生在动手时,而不是阅读时。
Happy kerneling!