AI Compiler Drills/ 提示与讲解
L5 提示与讲解 · Track L · Luminal(graph compiler) · 对照 solutions/l5_differential.py 逐行读

L5 讲解:一个能抓住真 bug 的差分测试

把三个“后端风格”的 softmax bug 依次放进参考解的差分测试:tail 被 n=97 当场抓住(误差 0.54);fp16 累加只差 4e-5,靠 fp32 的紧容差才抓到;没减最大值的版本在 N(0,3) 输入下永远通过,直到输入放大到 40 倍。再把容差公式拆开讲,说明“理由”那一行该怎么写。

怎么读这一页

对照 drillsl5_differential.py。device_softmax(drills41–64 行)是一个分块、在线 max/sum 的“设备实现”,bug 参数打开三种错误。

① 测试的骨架

一个 case 的生命周期悬停节点查看连接

关键一步是 先舍入再分发:两边看到的是同一组 fp16 / bf16 数,差异只来自计算,不来自输入表示。忘了这一步,fp16 的 case 会因为“参考用的是没舍入的输入”而大面积误报。

② 容差:每一项都有来源

drillstolerance_for(67–77 行):

rtol = 2·eps(dtype)            ← 输出舍入到存储 dtype(半个 ulp,留一倍余量)
     + 4·eps32·√n              ← 长度 n 的 fp32 求和(随机游走)
atol = max(eps(dtype) / n, 1e-7)   ← softmax 平均值是 1/n,接近 0 的概率看绝对误差
dtype n = 65 n = 1024
float32 rtol 4.1e-6,atol 1e-7 rtol 1.5e-5,atol 1e-7
float16 rtol 1.96e-3,atol 1.5e-5 rtol 1.97e-3,atol 9.5e-7
bfloat16 rtol 1.56e-2,atol 1.2e-4 rtol 1.56e-2,atol 7.6e-6

低精度时第一项占绝对主导;fp32 时第二项才是主角,n 越长越宽。这张表本身就是 PR 里“tolerance rationale”的好素材。

类比 · 考试的允许误差

允许误差 = 尺子的刻度(输出 dtype 的 eps)+ 量的段数带来的累积误差(√n)。对一份用毫米刻度的尺子量了 1024 段的报告,你不会因为差了 0.5 mm 就判它错;但对一份用 0.01 mm 刻度、只量了 1 段的报告,差 0.5 mm 就该追查了。

③ 三个 bug,三种结果

bug 机制 第一次被抓 / 数量(60 个 case,seed 0) 最小失败
tail 只对完整的 64 块求和,丢掉 n % 64 列 26 个失败 n = 97,fp32,max_abs 0.54
fp16_acc 求和累加器用存储 dtype 11 个失败,max_abs 都 < 1e-2 n = 63,fp32 输入,4e-5
no_max_sub 直接 exp(x),不先减最大值 scale = 3:0 个;scale = 40:44 个 溢出 → 0 / NaN

④ 输出:一个 Rust 测试骨架

drillsemit_rust_test(108–124 行) 把最小失败(1 × 97、fp32、seed)和容差理由写进注释:

#[test]
fn softmax_matches_reference_float32_1x97() {
    // minimized by l5_differential; input seed = ...
    // tolerance: float32: output rounding 2*eps=2.38e-07 + fp32 sum over n=97: ...
    let mut cx = Graph::new();
    let x = cx.tensor(vec![1, 97], DType::F32);
    let y = x.softmax(1).output();
    ...
    assert_close(&device, &reference, /*rtol=*/..., /*atol=*/...);
}

random_vec_seeded、run_reference、run_cuda、assert_close 是占位名,提交前要换成 crates/luminal_cuda_lite/tests/ 里实际使用的 helper(先读 luminalcublaslt_precision_matches.rs 这类文件学写法)。

⑤ Luminal 里的两个 runtime

ReferenceRuntime CUDA(luminal_cuda_lite)
角色 定义语义 被测对象
窄浮点 luminalnarrow_float.rs 精确模拟 硬件
TF32 luminaltf32.rs 模拟 cuBLASLt 可能用 TF32
调试 — LUMINAL_CUDA_CHECK_NONFINITE_INTERNAL=1 检查中间 buffer 的 NaN / inf

Reference 模拟了低精度,所以对拍时可以用比“float64 参考”更紧的容差——这是 Luminal 测试设计里值得学的一点。

⑥ 源码与资料索引

⑦ 自测

为什么 fp16_acc 的最小失败是 fp32 输入,而不是 fp16 输入?

因为容差跟着 dtype 走:fp16 输入的 case 容差约 2e-3,fp16 累加带来的 4e-5 级误差完全在容差内;fp32 输入的 case 容差约 4e-6,同样的误差就超了。这提醒我们:最容易暴露精度 bug 的是高精度的测试用例。

如果 CUDA 上 1% 的元素超出容差,但都是概率接近 0 的位置,算 bug 吗?

先看 atol:如果 atol 比这些元素的绝对误差还小,可能是 atol 定得太严(softmax 里 1e-30 的概率差 1e-35 没有意义)。但也可能是真问题,比如 underflow 处理不同(flush-to-zero)。正确做法是在报告里给出这些元素的值和误差,说明为什么调整 atol 是合理的——而不是悄悄放宽容差让测试变绿。