`DEEPINFER_ENABLE_FLAGGEMS=1` turns it on. Verified on all 8 DP ranks: exit=0, 8/8 ranks agreeing, no kernel fallbacks, `hard failures: none`, correct answers. 19 ops actually dispatched, 152 call sites, 43 registrations from 37 requested names. The default is `flag_gems.only_enable(include=[...])`, not `enable()`, and that is the whole design: * allow-list -- every intermediate state is safe (unproven ops stay on PyTorch, which is correct by construction); an incomplete list costs throughput, not correctness. * deny-list -- I tried it first. Five full engine runs, one new defect each: `cat` (e8m0 dtype), `mul` (broadcast shape), `topk` (compile blowup), `rms_norm` (signature), `mm` (compile assert). The method requires being bitten before it can exclude, and every state along the way is unsafe. That matches the instruction this was built to: use what works, leave the rest. Where it is called is not incidental. `import flag_gems` initialises a CUDA context -- measured, `torch.cuda.is_initialized()` flips on the import alone, before `enable()`. A `sitecustomize` injector therefore put all 8 ranks on device 0 and NCCL refused with "Multiple Ranks are using the same GPU". It now runs inside `create_llm`, after `setup_cuda_visible_devices`, which is the same hazard already documented there for the profiler. Defects found and documented rather than worked around, because each one is worth knowing: * `mul` returns the *first operand's shape* when broadcasting expands it: (1,64,8)*(4,1,8) gives (1,64,8), and (64,1)*(1,128) gives (64,1). Silent, no error, fewer elements computed. It made `engram_hash_ref` disagree with PyTorch during a real run. add/sub/div/maximum/minimum all broadcast correctly, so it is mul's own bug. * `cumsum` drifts by up to 1.0 over a 512-element bf16 row (tolerance ~0.17) -- bf16 accumulation. * `topk`'s compile time explodes with k: 4.8 s at k=8, 19 s at k=64, >180 s at k=512, and the sampler asks for 4096. py-spy put all 8 ranks at 100% CPU inside `make_llir` with every GPU idle. * `cat` raises `KeyError: 'float8_e8m0fnu'` on the model's UE8M0 scale tensors during weight load. * `mm` passes every isolated probe yet fails inside the engine at `general_mm`; trigger unknown, so GEMMs stay on cuBLAS, which is faster anyway. Two methodological errors in my own measurements, both fixed and both recorded: 1. After `enable()`, `torch.mul` *is* `flag_gems.mul`, so the first version of the mul A/B compared FlagGems with itself and reported a perfect score. All references are now computed before enable. 2. "ok" did not mean FlagGems ran. `torch.bitwise_xor` has no FlagGems registration at all, so that "match" was PyTorch against PyTorch. `screen_ops.py` now checks registration first and reports such cases as `skip` rather than pass. Name mismatch is its own trap: FlagGems registers `add` bare but `eq.Scalar`, `where.self`, `repeat_interleave.self_int`, `_softmax` (not `softmax`), `div.true_divide`. Asking for `softmax` silently registers nothing. The bootstrap now expands names against `flag_gems._FULL_CONFIG` and warns on anything unresolved -- which is how `softmax` was found to need `_softmax`. Output is not token-identical to the no-FlagGems build (diverges at the first token), but two runs of the same FlagGems config are bit-identical, so the difference is Triton-versus-eager accumulation order, not nondeterminism. Answers stay correct and coherent.
11 KiB
FlagGems 全面接入:计划与已完成的 de-risk
分支 h20-sm90-flaggems,基于 h20-sm90-sparse-mla-triton(6d337a8)。
截止 2026-10-25 14:40。
起点:三个已经实测的事实
不是推测,脚本在 scripts/flaggems_port/,都可复跑。
1. import flag_gems 没有全局副作用 ✅
ab_import_side_effects.py:对 aten::rms_norm / silu / softmax / topk / lerp 的
torch._C._dispatch_dump_table 做内容哈希,import 前后全部相同。
这推翻了我上一轮的说法(当时写在 vendor/flashmla_sparse.py 头部和 README 里,说
「import 会进程级替换 ATen 算子」)。实际情况:__init__ 只创建 aten_lib 句柄并 import 各算子模块,
注册发生在 enable() / only_enable() 被显式调用时。已在两处文档里改正并注明是修正。
2. 可以按白名单注册 ✅
only_enable(include=["rms_norm"]) → 实测只有 aten::rms_norm 变化,其余不动。
这是「受控接入」的机制,也是整个计划能成立的前提。
3. 首要候选已数值验证:逐位相同 ✅
ab_quant.py:FlagGems per_token_group_quant_fp8(scale_ue8m0=True, column_major_scales=True)
vs DeepInfer per_token_cast(fmt="e4m3", 128, round_sf=True, use_packed_ue8m0=False, use_tma_aligned_col_major_sf=True),5 个形状:
| 形状 | 量化值 | scale | 反量化 |
|---|---|---|---|
| 512×4096 | bitwise 相同 | 0 误差 | 0 误差 |
| 1×4096 | bitwise 相同 | 0 | 0 |
| 64×128 | bitwise 相同 | 0 | 0 |
| 7×2048 | bitwise 相同 | 0 | 0 |
| 128×1024 | bitwise 相同 | 0 | 0 |
两边 round_sf 写法不同但是同一个函数:DeepInfer 是 (int + 0x7FFFFF) & 0x7F800000(加满尾数进位
到指数域 = 向上取整到 2 的幂),FlagGems 是 exp2(ceil(log2(s)))。所以能逐位相同,不是「接近」。
算子是三层,不是一个清单
tile_kernels(DeepInfer 自己的融合算子库)和 flag_gems(通用算子库)不是替代关系。
混为一谈会导致白做很多工。实际是:
| 层 | 是什么 | FlagGems 能做什么 |
|---|---|---|
| A. 通用算子 | rms_norm / softmax / silu / rotary / topk … |
可替代,且这是 flag_gems 的主场(251 个 ops) |
| B. DeepSeek 专有融合算子 | mhc_*(9 个)、engram_*、swiglu+per_token_cast 五合一、batched_transpose_weight_sf |
部分可替代,但签名几乎都不 1:1 |
| C. 引擎 kernel 后端 | tilelang_ops/ 那 29 个 |
无关,那条线已经做完(Triton 17/29,默认路径无回退) |
逐项判断(基于实际签名比对)
A 层:值得做,风险低
| 算子 | flag_gems | DeepInfer 对应 | 判断 |
|---|---|---|---|
| per-token FP8 quant | per_token_group_quant_fp8 |
tk_compat.quant.per_token_cast |
✅ 已验证逐位相同,可直接接 |
| rotary | apply_rotary_pos_emb |
tk_compat.transform.apply_rotary |
待 A/B。已知 DeepInfer 版有 1 bf16 ULP 差异的旧账 |
| rms_norm | rms_norm / fused_add_rms_norm |
引擎内多处 | 待查调用点;若引擎已用 torch 原生则可直接 only_enable |
| grouped topk | grouped_topk |
tk_compat.moe 内部 |
待 A/B。MoE 路由,影响面大 |
| swiglu | swiglu(input, quantizer=None) |
swiglu_forward_and_per_token_cast |
⚠️ 部分:激活对得上,但 MoE 的 psum_num_tokens_per_expert / topk_weights / routed_scaling_factor 记账没有。不能整体替换 |
B 层:部分可做,但每个都要单独适配
| 算子 | flag_gems | 判断 |
|---|---|---|
mhc_pre(五路融合) |
mhc_pre(residual, fn, hc_scale, hc_base, eps×3, hc_post_mult_value, sinkhorn_repeat, n_splits) |
对应 mhc_pre_big_fuse_fwd。返回值风格不同(DeepInfer 写 out 参数、返回 None;flag_gems 返回 tuple) |
mhc_post |
mhc_post(x, residual, post_layer_mix, comb_res_mix) -> out |
对应 mhc_post_fwd(唯一返回值的那个),最接近 |
hc_split_sinkhorn |
hc_split_sinkhorn + torch ref |
对应 mhc_sinkhorn_fwd |
expand / head_compute_mix / pre_apply_mix / pre_split_mixes / normw_merge |
flag_gems 没有 | 这几个是纯 broadcast/multiply,本来就不需要 kernel,保持现状 |
reduce_partials_and_rmsnorm |
flag_gems 没有 | 保持 torch 参考 |
batched_transpose_weight_sf |
flag_gems 没有 | 这是一个未结的疑点(可能解释 token 差异),别在这里动 |
engram_* |
flag_gems 没有 | 保持现状 |
dequant / cast_back |
flag_gems 没有 | 保持 torch 参考 |
C 层:不动
tilelang_ops/ 已经有 Triton 后端,DEEPINFER_OPS_BACKEND=triton 切换,与 flag_gems 无关。
端到端已跑通(2026-10-26):白名单模式
DEEPINFER_ENABLE_FLAGGEMS=1 → deepinfer/flaggems_bootstrap.py 在 create_llm 里、
setup_cuda_visible_devices() 之后调 flag_gems.only_enable(include=[...])。
结果:
| 值 | |
|---|---|
| exit code | 0 |
| 后端 | sparse MLA = triton,FlagGems mode=allow |
| DP 一致 | 8/8 |
| kernel 回退 | 无 |
| hard failures | none |
| 回答 | 「中国的首都→北京」「日本的首都→东京」,带分段格式 |
| 实际派发算子 | 19 个、152 处调用点 |
| 注册 | 37 个请求名 → 96 个变体 → 43 个实际注册 |
| 警告 | 0 |
实际在跑的是 abs/amax/bincount/clamp(3变体)/cos/exp/gather/index_select/neg/nonzero/ reciprocal/relu/rsqrt/sigmoid/sin/sqrt/stack 这些——点状与 reduction 算子,正是 eager
PyTorch 一次一个 launch 吃亏、Triton 能合并的地方。
为什么改成白名单,而不是「全开 + 排除坏的」
我先按「全开+排除」做了 5 轮,每轮都在完整引擎里撞出一个新问题,每轮约 5 分钟:
cat(e8m0 dtype) → mul(广播形状) → topk(编译爆炸) → rms_norm(签名) → mm(编译断言)。
这条路的性质是「必须先被坑到才能修」,且中途任何状态都是不安全的。
白名单相反:任何中间状态都是安全的(没验证的算子留在 PyTorch),不完整只是性能问题, 不是正确性问题。这正好对应「能用就用、不能用就拉到」。
踩到的两个方法论错误(都已修)
torch.mul被替换后不能再当参考。第一版ab_mul_dtype.py在enable()之后同时用了torch.mul和flag_gems.mul,于是「bf16 mul 0 差异」是自比自。现在所有参考值都在enable()之前算好并 clone。- 「ok」可能根本没走 FlagGems。
torch.bitwise_xor在 FlagGems 里没有实现,所以 「bitwise_xor 通过」是 PyTorch 比 PyTorch。screen_ops.py现在先判断该算子是否真的被注册, 否则标skip并说明「PyTorch answered」。square同理(FlagGems 用别的名字)。
名字对不上是坑
FlagGems 的注册名是混合风格:add/mul 是裸名,但 eq.Scalar、lt.Tensor、where.self、
repeat_interleave.self_int、_softmax(不是 softmax)、div.true_divide、pow.pow_scalar
是 aten 风格的。只写 softmax 会静默什么都不注册。现在 _expand() 对着
flag_gems._FULL_CONFIG 自己做前缀展开,并在匹配不到时打印警告(这次就是靠它发现 softmax
应写 _softmax)。
仍未解决
mm家族:隔离探测(M/N/K 扫描、NT 布局、batch、addmm)全部正确,但引擎里仍在general_mm撞accumulator shape is incompatible。触发条件没定位,因此 GEMM 继续走 cuBLAS (本来也更快)。- token 级不一致,但是确定性的:与不用 FlagGems 的版本对比,第 1 个 token 就分叉(上一版 sparse-MLA-only 是前 15 个一致)。但同配置连跑两次逐 token 完全相同,所以差异来自 Triton kernel 与 eager PyTorch 的累加顺序,不是不确定性。回答仍然正确连贯,与 SM90 已知 2.7% logits 漂移同源,被贪心解码放大。
- 性能完全没测。白名单里只有 19 个算子真被用到,收益未知。
建议的推进顺序
每一步都是「先 A/B 单测、通过才接线」,沿用 sparse MLA 那次的规矩。
- per-token quant 接入(唯一已验证的)
- 在
tk_compat/quant.py加一个DEEPINFER_USE_FLAGGEMS分支 - 单测:直接复用
ab_quant.py的 5 个形状 + 加 Engine 真实形状 - 端到端:
DEEPINFER_SIMULATE_NO_TILELANG=1 DEEPINFER_USE_FLAGGEMS=1
- 在
- rotary + rms_norm:先 A/B,再决定接哪些。rotary 有 1 ULP 旧账,必须先看清是 flag_gems 更准还是更差
- grouped_topk:A/B 再做。MoE 路由错了会直接影响输出分布,容错度最低
- mhc_post / hc_split_sinkhorn:签名最接近的两个,先做这两个而不是
mhc_pre - mhc_pre:最后做,因为签名差异最大(out 参数 → 返回值)
必须先回答的两个问题
这两个不解决,后面的收益无法判断,甚至会白改:
- 性能到底有没有提升? 现在全盘没有一次性能测量。
tile_kernels的 torch 参考是纯 torch 实现, 换成 Triton kernel 理论上会快,但没有数字就是猜。建议在第 1 步接入后立刻测一次 prefill/decode 的 attention 与 quant 耗时占比。 batched_transpose_weight_sf到底对不对? 它是路径上唯一没 A/B 的算子,作用于所有权重的 scale factor,也是 token 差异的第一嫌疑。如果它本身有 bug,先修它比接 flag_gems 收益大得多。 补测要用hugging_face/parameters.py的真实 3-D(num_groups, -1, n)布局。
反向清单:明确不做的
- 不调用
enable()(无参版本)。实测它会注册全部算子,等于把引擎的数值行为整体换掉, 出了问题无法归因。只用only_enable(include=[...]),或者干脆只 import 具体模块的函数。 - 不替换
tilelang_ops。那条线已经完成。 - 不为了「统一」而替换纯 broadcast 的小算子(
expand/normw_merge等)。它们本来就不是瓶颈, 换过去只增加一层出错可能。 - 不引入
flagcx依赖。importflag_gems会拉 Triton TLE → dlopenlibflagcx, 需要LD_LIBRARY_PATH里能找到libcudart。当前部署本来就满足这一点,但不应该把这条 隐性依赖扩大化(这也是 sparse MLA 那个 kernel 内联而不依赖包的原因之一)。
环境事实(复跑用)
# flag_gems 5.0.2 已装进构建 venv
source /data/ftbuild/venv/bin/activate
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/root/.triton/nvidia/cudart/cuda_cudart-linux-x86_64-12.8.57-archive/lib
export PYTHONPATH=/data/ftbuild/repo CUDA_VISIBLE_DEVICES=3
python scripts/flaggems_port/ab_import_side_effects.py
python scripts/flaggems_port/ab_quant.py
注意:宿主机 /tmp 与容器内 /tmp 是两套,脚本要经 stdin 或 docker cp 进容器。