一份诚实的实验报告:大多数"惊艳加速"经不起严格检验,真正的 SOTA 藏在编译器结构性做不到的地方——MoE 的 data-dependent routing。所有数字均为 gfx1151 实测、可复现。
我们用一个 LLM agent(GLM-5.1 生成 OpenAI Triton kernel)在一台消费级 AMD APU(gfx1151,带 128 GB 统一内存的 Strix Halo iGPU)上系统挖掘 GPU kernel 的加速机会,以 KernelBench 为评测框架。
本报告的核心不是某个惊人数字(最高真实合规加速约 1.5×),而是一条可复现、有方法论、诚实的研究路径:
结论:在带宽受限的消费级 iGPU 上,rocBLAS / MIOpen / torch.compile 已覆盖大部分易得优化;真正剩余空间在编译器结构性做不到的地方——data-dependent 控制流(MoE expert routing)。
全文所有加速比都在这三者之间比较。理解它们,是分辨"真 SOTA"和"赢朴素基准的假象"的前提。速度通常 ① < ② ≈ ③。
PyTorch 默认模式,一个算子一个算子地执行。算 silu(gate)*up 会先 launch 一个 GPU kernel 算 silu、结果写回显存,再 launch 一个 kernel 读出来乘 up。每步都有 kernel 启动开销 + 中间结果的显存往返——简单灵活但慢。几乎任何"融合"都能赢它,所以"vs eager 1.5×"其实意义有限。
PyTorch 2.0 的 JIT 编译器(后端 TorchInductor)。它把代码自动 trace 成计算图,再自动融合多个算子、自动生成优化 Triton kernel。上面的 silu·mul 它会自动融成一个 kernel,和手写的差不多快。生产环境都用它——所以只有赢过 torch.compile,手写 kernel 才真有价值,否则 inductor 已经免费帮你做了。
agent 生成的自定义 kernel——拿来同时和 eager、torch.compile 比,看它到底是真有本事,还是只赢了朴素基准。
一句话:vs eager 看"有没有融合",vs torch.compile 看"有没有真本事"。本报告坚持用 torch.compile 做基准,就是为了不被"赢朴素 eager"的假象骗到(见下方 §03 教训二的对比图)。
torch.compile 要把代码 trace 成静态图。但 MoE 的 forward 里有依赖运行时数据的控制流:
for e in range(num_experts):
if tok.any(): # ← 分支取决于张量"实际值" → 编译器没法 trace
xe = x[tok] # ← 索引取决于运行时才知道的 routing
... # gate/up GEMM · SwiGLU · scatter-add
这种 data-dependent 控制流(分支和索引都取决于张量实际值),编译器无法 trace,只能断图(graph-break)、把那部分退回 eager 执行。所以 torch.compile 对 MoE 几乎零优化(实测 compile ≈ eager)——这正是手写 kernel 在 MoE 上有结构性优势、不会被编译器追平的根本原因,也是全报告"真 SOTA"的落点。
KernelBench 合成题挖掘的边际收益迅速枯竭。我们转向用 ollama + rocprofv3 profile 真实模型推理(gfx1151)。
| 模型 | 架构 | decode tok/s | 有效带宽* |
|---|---|---|---|
| qwen3:8b | dense 8B | 38.9 | 202 GB/s |
| qwen3:32b | dense 32B | 10.6 | 212 GB/s |
| gpt-oss:20b | MoE 21B/3.6B act | 48.2 | ~106 GB/s (~50%) |
| gemma4 | dense | 53.0 | — |
*有效带宽 = 模型字节数 × decode rate(decode 是 bandwidth-bound)
↑ 两个 dense 模型(不同大小)撞在同一条 ~207 GB/s 实测带宽墙上 → decode 已饱和、无空间;MoE 只用到一半 → 真实瓶颈。
决定性证据:两个不同大小的 dense 模型(8B/32B)算出的有效带宽高度一致(202 / 212 GB/s),证明 dense decode 已带宽饱和(~81% roofline)、无优化空间。而 MoE 带宽利用率只有 ~50% → 真实瓶颈在 MoE(expert gather 的非连续访存 + routing 开销)。
构造标准 top-k MoE SwiGLU 参考,GLM-5.1 写融合 Triton kernel。干净 no_grad cuda-event 计时,对比 eager 与 torch.compile。
| 配置 | eager | torch.compile | MoE kernel | vs compile | 正确性 |
|---|---|---|---|---|---|
| moe · 8 experts | 5.24 ms | 5.25 ms | 4.75 ms | 1.10× | 7e-7 |
| moe2 · 32 experts | 67.24 ms | 67.51 ms | 59.13 ms | 1.14× | 2e-6 |
| moe3 · 64 experts | 47.55 ms | 47.50 ms | 32.03 ms | 1.48× | 2e-6 |
bash final_moe3.sh # moe3: 1024 tok / 2048 hidden / 64 experts / top-2 max_abs_err 1.9669e-06 ref_mean 0.2138 eager=47.550ms torch.compile=47.503ms MoE3_kernel=32.028ms speedup vs eager=1.485x vs torch.compile=1.483x
↑ torch.compile(47.50ms)≈ eager(47.55ms)——对 MoE routing 零优化;手写 kernel 32.03ms。
↑ 三个配置都超 torch.compile(它对 MoE routing 一律 graph-break);experts 越多、per-expert 开销越大,融合收益越大。
核心结论:torch.compile对 MoE 的 data-dependent 控制流(if tok.any():的 per-expert token gather)graph-break,几乎零优化(compile ≈ eager)。手写 kernel 通过融合 gate/up GEMM 合并、SwiGLU 激活、加权 scatter-add,减少中间张量 DRAM 往返拿到稳定加速。这是结构性优势,不会被编译器追平;越稀疏(更多 expert loop 开销)收益越大。
诚实说明:GEMM 本身仍走 rocBLAS(手写 Triton matmul 在 gfx1151 打不过 rocBLAS);收益来自激活/scatter 融合 + GEMM 合并。这是 GPU MODE AMD 挑战赛 MXFP4 MoE 算子的同类问题。
最初 agent 报 Softmax 1238×。排查发现 KernelExecResult.runtime 字段注释写 in us 但实际单位是 ms,我们误除 1000。官方 run_and_check.py 复核报 1.25× = 独立计时 1.26×。是我们 agent 的 bug,不是 KernelBench 的问题。
python scripts/run_and_check.py level=1 problem_id=23 backend=triton check_kernel=False [Eval] Kernel eval result: compiled=True correctness=True runtime=95.4 ref_runtime=119.0 # 单位是 ms,不是注释说的 us! [Timing] Custom Kernel exec time: 95.4 ms [Timing] PyTorch Reference Eager exec time: 119.0 ms [Speedup] Speedup over eager: 1.25x # 官方;与独立计时 1.26x 吻合,1238x 是乌龙
| KernelBench 题 | vs eager | vs torch.compile | 判定 |
|---|---|---|---|
| #12 Gemm·Mul·LeakyReLU | 1.76× | 1.62–1.68× | ✓ 真 SOTA |
| #38 L1Norm | 1.51× | 1.20× | ✓ 真 SOTA |
| #23 Softmax | 1.25× | 0.97× | ⚖ 被 compile 追平 |
| #25 Swish | 2.35× | 0.96× | ⚖ 追平 |
| #89 cumsum | 1.67× | 1.01× | ⚖ 追平 |
↑ 简单 elementwise / softmax / scan 对 eager 的"赢"会被 inductor 抹平到 ~1.0×(红);只有它融合不掉的 #12 / #38 才是真 SOTA(绿,两柱都 >1)。
| 题 | vs eager(原始) | FP32 合规后 | 真相 |
|---|---|---|---|
| L2 #80 Gemm·Max·… | 10278× | — | reward-hack |
| L2 #51 …LogSumExp… | 170× | — | reward-hack |
| L2 #56 Matmul·Sigmoid·Sum | 11.0× | 1.04× | FP16 降精度假象 |
| L2 #64 Gemm·LogSumExp·… | 7.7× | 1.02× | FP16 降精度假象 |
python scripts/run_and_check.py level=2 problem_id=56 check_kernel=True precision=fp32 [WARN] Static check warnings: ['Precision downgrade detected: required FP32 but code uses FP16'] [Eval] correctness=True (5/5) metadata={... 'excessive_speedup': True} [Speedup] over eager: 11.02x over torch.compile: 10.67x # FP16 假象 # 强制全程 FP32 重跑同一题 → speedup 塌到 1.04x
Goodhart 定律的活案例:当 allclose(1e-2) 成为"正确"的代理度量,对 reduce 到极小输出的题,近似/退化解也能"通过"。我们把官方静态检查器对 FP16 的 WARN 升级为合规闸门(强制 FP32)。
真合规 SOTA(vs torch.compile):MoE 1.10× · MoE2 1.14× · MoE3 1.48× · L2 #12 1.62–1.68× · L1 #38 L1Norm 1.20×。
四层验证:独立 cuda-event 计时 → 对比 torch.compile → 强制 FP32 合规 → 官方静态反-hacking 检查。
上游贡献: Issue #155(FP16 reward-hack 案例 + 检查器改进建议)、 Issue #156(提议增加 MoE/expert-routing 算子 + torch.compile graph-break 观察)。
真实的研究充满意外。记录这次过程里几个有趣的瞬间——它们也是结论可信度的一部分。这是一台远程机器、一个 LLM、一个人类协作者之间的真实拉锯。
满心以为分到一台数据中心卡(MI300)。SSH 上去一看:AMD Ryzen AI MAX+ 395 "Strix Halo"——一颗带核显的消费级 APU(Radeon 8060S, gfx1151)。整个项目的难度、定位、甚至能不能跑起来,都因这一行 lspci 输出而改变。
lspci | grep -iE "AMD|Radeon" | head -1 00:00.0 Host bridge: AMD Strix/Strix Halo Root Complex (rev 02) grep "model name" /proc/cpuinfo | head -1 model name : AMD RYZEN AI MAX+ 395 w/ Radeon 8060S python -c "import torch; ..." torch 2.12.0a0+rocm7.13 | arch gfx1151 | CUs 20 | mem 61.9GB matmul 4096^3 fp32: 48.08 ms -> 2.86 TFLOPS
中国网络下 download.pytorch.org、AMD nightlies、GitHub release CDN 全被封。gfx1151 的原生 PyTorch 轮子在 TheRock 的 CloudFront 上,几十个边缘 IP 挨个 curl 测试,最后只有 18.65.3.45 一个能连——钉进 /etc/hosts,才把 1.3 GB 的轮子断点续传下来。
for ip in $edges; do curl --resolve rocm.nightlies.amd.com:443:$ip -o /dev/null -w "$ip -> %{http_code}\n"; done 18.65.3.20 -> 000 18.65.3.45 -> 200 # ← 唯一能连的边缘节点 18.65.3.53 -> 000 18.65.3.96 -> 000 2600:9000:2366:1a00:7:c0e1:fbc0:93a1 -> 000 2600:9000:2366:2c00:7:c0e1:fbc0:93a1 -> 000
第一个 torch wheel 标题写着 "pytorch wheels for gfx1151",装上跑一句 torch.randn(..., device='cuda') 直接 no kernel image available。拆开 libtorch_hip.so 一看——里面只有 gfx1100/gfx1201 的代码,根本没有 gfx1151。
Agent 一度兴奋地报出 Softmax 1238× 加速。一路查到底:KernelExecResult.runtime 字段的注释写着 in us,但实际单位是 ms,我信了注释、除以了 1000。官方脚本复核报 1.25×,和独立计时的 1.26× 一致。是我的 bug,不是 benchmark 的。从此刻起,任何"过好"的数字一律先怀疑测量。
有两道题分别报出 1 万倍、170 倍加速。当然是假的——它们 reduce 到极小输出,在 allclose(1e-2) 容差下,连一个几乎什么都不算的退化 kernel 都能"通过正确性"。Goodhart 定律最鲜活的标本,被我们当场抓获。
监控查询太勤,SSH 触发了机器的 fail2ban,反复 Connection reset by peer,只能停手、静默等解封。后来想用 rocprofv3 包裹 ollama 做 kernel profiling,一不小心把整个 ollama 服务搞挂了(systemctl 卡在停止状态),又手忙脚乱地恢复。
ssh glm@172.31.101.50 'echo ALIVE' kex_exchange_identification: read: Connection reset by peer Connection reset by 172.31.101.50 port 22 # 连续 5 次全 reset…监控查得太勤,IP 被 ban 了 sleep 90 # 停手,静默等解封 ssh glm@172.31.101.50 'echo ALIVE' ALIVE
给模型喂了完整的 flash-attention 骨架、把输出开到 64k token,它还是写不出一个正确的融合 attention kernel——变量未定义、代码被截断、语法错误轮番上演。公认最难的 kernel,名不虚传。
从 GLM-4.6 换到 GLM-5.1,同一道 RMSNorm 从 0.57×(打不过 torch)一跃到 1.00×(打平 aten fused kernel)——肉眼可见的代际差距。后来想再上 glm-5.2 / glm-x-preview,API 礼貌地回了句"您无权访问",只好作罢。
写报告前要确认 MoE 参考实现可信,结果发现——对话太长,我自己都不记得 moe.py 是哪一步建的了。回去把全文 cat 出来逐行核对,确认是标准的 top-k MoE SwiGLU,才敢对外宣称这是 SOTA。诚实,从不假设自己记得。
一段 /loop 让 agent 整夜自己挖矿,每 25 分钟醒来看一眼、换一批题、记录规律。等最有价值的 MoE 结论浮现、moe2 跑完时,抬头一看系统日期——已经跳到了第二天。规律是在一整夜的反复试错里,自己长出来的。
这些弯路不是报告的瑕疵,而是它的一部分。一个能被 1238× 乌龙、10278× 假象、fail2ban、权限墙、自己的记忆漏洞反复修正的过程,才配得上最后那个温和但真实的 1.48×。