The working branch stays as-is so local experiments are not churn; the PR branch carries the ruff/annotation commit that CONTRIBUTING.md requires.
23 KiB
H20 / SM90 移植日志
DeepInfer 原本是 Blackwell-only 的推理框架。这份文档记录把它跑到 8×H20(SM90)上、 以及把数值从「乱码」修到「答对」的全过程:时间线、每个坑的根因和修法、完整变更清单, 最后是仍然存在的数值漂移 TODO。
配套文档:
H20-SM90-FINDINGS.md—— 硬件/环境事实、SM90 与 SM100 的能力差异分析H20-DEBUG-PLAN.md—— 分阶段排查计划(P0~P5),含每一步的判据和结果
代码位于 git@github.com:tonyh168/DeepInfer.git,两个分支:
| 分支 | 用途 |
|---|---|
feat/h20-sm90-support |
自用。能跑、验证过,但未做 ruff 规范化 |
h20-sm90-support |
提 PR 用。在自用分支基础上补了格式与类型注解,两项 ruff 检查均通过 |
| 上游 PR:https://github.com/deepseek-ai/DeepInfer/pull/180 |
改动逻辑时请改 feat/,然后 cherry-pick 到 h20-sm90-support 并重跑 ruff check --no-cache
与 ruff format --check --no-cache(CONTRIBUTING.md 的硬性要求)。
0. 结论摘要
| 项 | 状态 |
|---|---|
| 8×H20 上加载 V4.1-Flash 全量权重(78.5 GiB/卡) | 完成 |
| prefill / decode / sampling 全链路 | 完成 |
| 稀疏 MLA attention kernel(TileLang 重写) | 完成,104 TFLOP/s @ bf16 精度 |
| Engram 表查询(NVLink all_to_all 替代 RDMA) | 完成,与全表参考逐元素对齐 |
| DeepSelect top-k | 完成(torch.topk 复刻契约) |
| 语义正确性 | 首 token 起逐 token 对得上,答案正确 |
| 残余数值漂移 | 见 §6,未收敛 |
端到端对比(11-token chat prompt,greedy):
官方参考: [17607, 2793, 3139, 6127, 320] -> 中国的首都是北京。
移植后: [17607, 2793, 3139, 666, 6127, 666, 320, 1] -> 中国的首都是**北京**。
前 3 个 token 逐 token 一致,答案正确;差异是多余的 markdown 加粗 **。
1. 起点:为什么这件事不是「改个 flag」
原框架对 Blackwell 的依赖不是一处配置,而是四层:
| 层 | 依赖 | SM90 上的后果 |
|---|---|---|
| 算子库 | vendor 的 FlashMLA fork 只有 sm_100a/sm_103a,每个入口都 TORCH_CHECK(arch.is_sm100f()) |
import 就失败,或 launch 报 "no kernel image" |
| 内核实现 | DeepGEMM 的 fp8_fp4_paged_mqa_logits / fp8_fp4_sparse_mqa_logits 无 SM90 版本 |
稀疏 attention 的 indexer 无法调用 |
| 量化配方 | FP4 tensor core、packed UE8M0(4 个指数塞进 int32)、per-32 block | 三者 SM90 全没有 |
| 通信 | Engram 用 NCCL GIN(GPU-initiated RDMA) | 单机 8 卡场景无 RDMA fabric |
| 融合 kernel | mega_mhc / mega_moe / FlashAttention-4 CuTe DSL / fused norm+rope+cast |
全部 SM100-only |
SM90 的量化口径(DeepGEMM 在 Hopper 上的唯一选择):FP8 e4m3 + fp32 scale + per-128 block,
recipe (1, 128, 128),disable_ue8m0_cast=True。
策略:不去改调用点,而是复用框架已有的 patch_imports 机制(Ascend 后端在用),
把 Blackwell-only 的 from X import Y 整体替换成 SM90 实现。新增 deepinfer/h20_ops/ 包。
2. 时间线
阶段 A(本次会话之前):让它能加载、能算
已完成并记录在 H20-SM90-FINDINGS.md:
- 平台识别:新增
PlatformH20,is_sm90=True;把原来混在一起的fp8_sf_type拆成sf_pack_factor(GEMM scale 类型 vs 每元素打包几个 scale)。 - 权重量化:FP4 experts + ue8m0 dense 在加载时重量化成 FP8 e4m3 / fp32 per-128。
- 两个 FlashMLA kernel 用 TileLang 开源实现替换(
sparse_mla_sm90.py)。 - vendor 编译通过,
_C可导入,权重 8 卡 78.5 GiB/卡。 - Engram 表查询改成本地 all_to_all gather。
阶段 B:13:45 —— 澄清「prefill 跑通」的含义
用户问:「prefill 跑通是说第一个 token 是个语义正常的 token 吗?」
核查日志后发现:之前三个 run 里 OUTPUT / INPUT 计数都是 0,一句话都没生成过。
当时说的「跑通」只是「前向不崩、没有 NaN」。这个澄清很重要,它把目标从
「不报错」改成「答对」,后面所有工作都围绕后者。
定下的第一个硬判据:官方参考在这条 prompt 上输出什么,我们就得输出什么。
阶段 C:14:00-14:20 —— 打通到「能出 token」
按报错顺序修了 7 处(详见 §3.1~§3.3):
- DeepSelect topk 在 SM90 launch 失败 →
torch.topk复刻契约 - SM90 的 MQA logits 输出是 fp32(SM100 是 bf16),
candidate_block_max只吃 bf16 → 加窄化 - 共享层候选块优化(
fp8_fp4_sparse_mqa_logits)SM100-only → 关掉,走稠密路径 - 稀疏 MLA 的 0/0:padding 行
sumexp == 0,acc_o /= 0让整个 hidden state 变 NaN - paged MQA logits 的 varlen:SM90
DG_HOST_ASSERT(not is_varlen)→indices=None - paged kernel 硬要求 64-token KV page,而 cache 是 128 → decode indexer 走 torch 兜底
- ViT 兜底的
.tolist()被forbid_cuda_sync拦 →allow_cuda_sync()局部放行
结果:能出 token,但内容是 EVAL - 暂时不会 - (应为「北京」)。
顺带发现:DeepInfer 没有 --enforce-eager 命令行开关,只有环境变量
DEEPINFER_ENFORCE_EAGER=1 + DEEPINFER_DFLASH_ENFORCE_EAGER=1。打开后跳过 CUDA graph
capture,绕过了 capture 期的 not is_varlen 断言。
阶段 D:14:20 —— 固化基准,发现「对比方法本身是错的」
建立官方基准:/data/models/DeepSeek-V4.1-Flash-1017/inference/generate.py + TP8 权重
config_fp8.json,其generate()直接取 argmax,天然 greedy、可复现。
第一个方法论坑:官方 generate.py 走 encode_messages() 套 chat 模板,
所以「两侧喂同样的原始文本」并不等于同样的输入。之前的文本对比全部无效。
修法:写 scripts/ref_next_token.py(吃任意 token ids → greedy next token + top5),
和 scripts/h20_ask.py --ids 对称。拿到官方真实输入:
[0, 128803, 2056, 768, 17607, 2793, 3139, 12108, 1148, 128804, 128822]
阶段 E:14:35 —— 环境事故:磁盘满
/tmp(= docker 根)100% 满,nvcc JIT 报
fatal error: when writing out to /tmp/tmpxft_....ii: No such file or directory,
实验根本起不来。
清理:删 4 个已退出容器 + docker image prune -a → 0 → 75 GB 可用。
注意:这台机器是共享的。flagos / seed-oss-36b-flagos /
meta-llama-3-8b-instruct-nvidia-flagos1 会被同事反复重启抢走 GPU。跑 8 卡实验前
必须先 nvidia-smi + docker ps 确认;容器 stop 后 vLLM 的 EngineCore 子进程可能
残留占显存,需要手动 kill -9。
阶段 F:14:50 —— P1:prefill 因果掩码(第一个真 bug)
现象:修好 NaN 之后首 token 是 17607(中国的)——对的,但第 4 个 token 起分叉。
定性实验:把 14 个 token(官方前缀 + 17607,2793,3139)直接当 prefill 输入,
我方仍预测 666(**),官方在同一前缀下给 6127(北京)。
=> 分歧在 prefill 内,不是 decode 累积。
根因:prefill_attn_varlen_sparse 用 idx <= 行号 当因果界。但 DeepInfer 传的
kv_index 是装配后 buffer [swa cache | 当前 swa | compressed] 的绝对位置
(kv_index_cat 整体加了 total_k_swa_cached + total_k_swa_cur),行号跟它没有对应关系
=> 几乎所有 index 被 mask => sumexp=0 => acc_o/0 => 整层 NaN。
修法:is_causal=False。FlashMLA 的稀疏 API 本来就没有 causal 参数——因果关系
由 indexer(top-k 只取因果范围内)和 SWA window index 保证。
单测为什么没抓到:h20_attn_check.py 的 make_indices 按「第 t 行取 [0, t] 内」
造索引,正好满足 idx <= q_pos,把这个 bug 遮住了。已补「绝对位置」用例
(索引最大 3078,行数只有 16)。
结果:首 token 与官方一致,前 3 个 token 拼起来都是「中国的首都是」,第 4 个 token 仍分叉。
阶段 G:15:20 —— P2:分层对拍
工具:官方侧 scripts/ref_layer_dump.py(forward hook),我方侧
DEEPINFER_DUMP_LAYERS=<dir> → h20_ops/debug_dump.py,
scripts/compare_layers.py 算 cos / rel_err / 范数比。
坑 2:我方的 decoder_layer 返回的 hidden_states 只是子层输出(2D),
完整残差流在 residual(HCLazyPostData,hc_mult=4)里。不经过 hc_post 合并就比,
每层 cos 都是 0.02 —— 假结论。
坑 3:引擎在分配 KV cache 前会跑一次 dummy profile forward,token 数与真实
prompt 不同。必须用 DEEPINFER_DUMP_TOKENS=<n> 门控,否则 dump 到的是 dummy 输入。
结果(14-token 输入):
| 位置 | cos | rel_err |
|---|---|---|
| layer0 attention 子层 | 0.99897 | 4.7e-2 |
| layer0 整层 | 0.99190 | 1.27e-1 |
| layer1 整层 | 0.97707 | 2.29e-1 |
| layer11 | 0.86515 | 5.28e-1 |
结论:attention 是对的(4.7% 在 FP8 口径下属合理),误差主要来自第 0 层的 FFN/MoE, 并单调累积到把 top-1 翻掉。
阶段 H:16:40 —— P3:layer 0 的 FFN 内细分
在 mlp.py 的 MoE 内挂点,dump 路由 id / gate 权重 / shared 输出 / combined 输出。
路由(最后一个 token,top-6):
| 层 | 重合度 |
|---|---|
| L0 | 5/6(差一个:ref 370 vs mine 312,边界翻转) |
| L1 | 6/6 完全一致 |
| L2 | 6/6 完全一致 |
分支:
| 分支 | cos | rel_err |
|---|---|---|
layer0_shared_out(shared expert) |
0.99871 | 5.2e-2 |
layer0_combined(routed + shared) |
0.93949 | 3.6e-1 |
=> 路由基本无罪,shared 对,routed experts 错。
阶段 I:16:50 —— 找到根因并修复
找到了权威参考:官方 inference/convert.py::cast_e2m1fn_to_e4m3fn 就是把 FP4 转 FP8
的实现(生成 TP8 权重用的)。写 scripts/h20_expert_weight_check.py 逐元素对拍,
第一次结果:cos≈0.894、rel_err≈0.53 —— 系统性错误,不是精度损失。
根因:h20_ops/quant.py::requant_expert_fp4 用 _expand_blocks(sf, n_out, k, 32) 展开
UE8M0 scale,而这个函数对两个维度都做 repeat_interleave(32):
| scale 形状 | 该展开的维度 | |
|---|---|---|
| dense 权重 | (n_out/32, k/32) |
两个维度 |
| 专家权重 | (n_out, k/32) |
只有 k 维度 |
于是 (2304, 160) 被当成 (72, 5) 展开 —— 全部 2304 个输出通道都拿到了前 72 行的
scale,每个专家的权重被打乱。这精确解释了 P2/P3 观察到的
「shared 对、routed 错」非对称:shared 走 requant_dense_fp8(_expand_blocks 用法正确),
routed 走这条错路径。
修法:
assert tuple(sf.shape) == (n_out, k // FP4_BLOCK), f"unexpected expert scale layout: {tuple(sf.shape)}"
x = _unpack_fp4(packed) * sf.to(torch.float32).repeat_interleave(FP4_BLOCK, dim=1)
验证:
| 权重 | 修复前 cos / rel_err | 修复后 cos / rel_err |
|---|---|---|
| w1 | 0.8936 / 5.35e-1 | 1.0000 / 2.84e-2 |
| w3 | 0.8960 / 5.26e-1 | 0.9997 / 2.69e-2 |
| w2 | 0.8964 / 5.20e-1 | 0.9996 / 2.65e-2 |
端到端:中国的首都是**北京**。 —— 答案正确。
3. 踩过的坑(速查表)
按「当时现象 → 根因 → 修法」索引。前 3 个是方法论坑,如果不先排掉,后面所有 数字都是错的。
| # | 现象 | 根因 | 修法 | 位置 |
|---|---|---|---|---|
| 1 | 文本对比看起来差很远 | 官方套 chat 模板,两侧输入不同 | 用 token ids 对齐,ref_next_token.py + --ids |
工具 |
| 2 | 每层 cos 都是 0.02 | 比的是子层输出,不是残差流 | hc_post(hidden_states, residual) 后再比 |
decoder_layer.py |
| 3 | dump 出来完全不相关 | 引擎先跑 dummy profile forward | DEEPINFER_DUMP_TOKENS=<n> 门控 |
debug_dump.py |
| 4 | layer0 全 NaN | 稀疏 MLA sumexp=0 时 acc_o/0 |
eps 下限截断,空窗口返回 0 |
sparse_mla_sm90.py |
| 5 | 首 token 之后全乱 | prefill 用行号当因果界,但索引是绝对位置 | is_causal=False |
attn_sm90.py |
| 6 | DeepSelect launch 失败,无 kernel image | kernel 只编了 SM100 | torch.topk 复刻契约;vendor setup.py 打开 sm_90a |
deep_select_sm90.py |
| 7 | candidate_block_max 断言失败 |
SM90 的 MQA logits 是 fp32,kernel 只吃 bf16 | _as_bf16_block_logits 窄化 |
index_attn.py |
| 8 | 共享层候选块优化报错 | fp8_fp4_sparse_mqa_logits SM100-only |
关掉 is_candidate_topk_source / use_candidate_topk |
index_attn.py |
| 9 | decode indexer 断言 block_kv == 64 |
SM90 paged kernel 硬要求 64-token page,cache 是 128 | _decode_p_sum_sm90,torch 直接算 |
index_attn.py |
| 10 | not is_varlen 断言 |
SM90 的 paged MQA logits 不支持 varlen | 两处 indices=None |
index_attn.py |
| 11 | called a synchronizing CUDA operation |
forward 内做 host 读取 | allow_cuda_sync() 局部放行 |
多处 |
| 12 | Engram 查表失败 | EngramBuffer.fetch 需要 NCCL GIN / RDMA |
两次 all_to_all 的本地 gather |
engram_local.py |
| 13 | nvcc JIT 失败 No such file or directory |
/tmp 100% 满 |
删已退出容器 + image prune -a |
运维 |
| 14 | OOM 时显存已被别人占 | 共享机器被别人 vLLM 抢卡 | 先 nvidia-smi;容器 stop 后 kill -9 残留 EngineCore |
运维 |
| 15 | routed experts 全错,shared 全对 | _expand_blocks 对两个维度都展开,但专家 scale 是 per-channel |
只沿 contraction 轴 repeat_interleave |
quant.py |
关于 torch.float4_e2m1fn_x2:它的 cast kernel 在 SM90 和 CPU 上都没有实现
(GPU 报 device-side assert,CPU 报 copy_kernel not implemented)。
验证 FP4 逻辑只能走整数位运算 + 查表,见 h20_quant_check.py。
4. 变更清单
41 files changed, 3113 insertions(+), 59 deletions(-)
4.1 新增包 deepinfer/h20_ops/(1028 行)
| 文件 | 行数 | 职责 |
|---|---|---|
__init__.py |
41 | 注册 patch_imports 的 SM90 替换项 |
sparse_mla_sm90.py |
224 | TileLang 稀疏 MLA kernel(prefill/decode) |
attn_sm90.py |
196 | prefill_attn_varlen_sparse / decode_mla_mqa_sparse,签名与 attention_backend.py 一致 |
engram_local.py |
192 | NVLink all_to_all 替代 RDMA 查表 |
deep_select_sm90.py |
97 | torch.topk 复刻 DeepSelect 契约 |
quant.py |
93 | FP4→FP8、ue8m0→fp32 重量化 |
vit_attn.py |
86 | ViT attention 的 SDPA 兜底 |
debug_dump.py |
99 | 逐层 dump(opt-in) |
4.2 修改的框架文件(18 个)
| 文件 | 改动 |
|---|---|
deepinfer/platform.py |
新增 PlatformH20;sf_pack_factor 拆维度 |
deep_cluster/hardware/{__init__,device}.py |
识别 H20 |
deepinfer/hugging_face/config.py |
配置映射(hc_mult 等) |
deepinfer/hugging_face/weights.py |
_load_expert_params_sm90:加载时重量化专家 |
deepinfer/model_executor/layers/linear.py |
SM90 GEMM recipe (1,128,128) |
.../tilelang_ops/cache_common.py |
新增 FP8_FLOAT32_SF_INDEX_KV_FORMAT |
.../tilelang_ops/fused_transform.py |
融合 kernel 的 SM90 分支 |
deepinfer/model_executor/models/decoder_layer.py |
NaN 诊断 + dump 挂点 |
deepinfer/model_executor/models/deepseek.py |
embed / final_norm dump 挂点 |
.../v4_modules/attn.py |
平台分支 |
.../v4_modules/containers.py |
权重容器 |
.../v4_modules/engram.py |
走本地 gather |
.../v4_modules/hc.py |
Hyper-Connections 分支 |
.../v4_modules/index_attn.py |
bf16 窄化 + decode 兜底 + 关候选块优化 |
.../v4_modules/mlp.py |
MoE 兜底 + dump 挂点 |
.../parallel_utils/userbuffer.py |
SM90 分支 |
deepinfer/npu_ops/patch_imports.py |
扩展 patch 机制 |
deepinfer/worker/cache_engine.py |
KV cache 布局 |
4.3 新增脚本(11 个)
自检类(8 卡 H20 上跑):h20_attn_check.py、h20_engram_check.py、
h20_topk_check.py、h20_smoke_check.py、h20_real_requant_check.py、
h20_quant_check.py、h20_expert_weight_check.py
对拍类:h20_ask.py(单 prompt 快速验证)、ref_next_token.py(官方 next token)、
ref_layer_dump.py(官方逐层)、compare_layers.py(逐层 diff)
4.4 文档
H20-SM90-FINDINGS.md、H20-DEBUG-PLAN.md、本文。
4.5 未纳入版本管理(重要)
vendor/* 是 submodule 且本地未初始化(gitlink 未改),所以:
vendor/DeepSelect/setup.py里arch=compute_90a,code=sm_90a原本被注释掉, 需要手动打开,否则 top-k 报 "no kernel image"。- 该修改只存在于本机
/data/deepinfer-h20/DeepInfer-git,克隆本分支后必须手动处理。
5. 复现指南
环境变量(缺一不可)
export PYTHONPATH=<repo>:<repo>/examples
export DEEPINFER_ACCELERATOR=nvidia
export DEEPINFER_HF_MODEL_DIR=/data/models/DeepSeek-V4.1-Flash-1017
export DEEPINFER_ENABLE_MEGA_MOE=0 # mega_moe 是 SM100-only
export DEEPINFER_BATCH_INVARIANT=0
export EP_DISABLE_GIN=1 # 没有 RDMA
export DEEPINFER_ENFORCE_EAGER=1 # 跳过 CUDA graph capture
export DEEPINFER_DFLASH_ENFORCE_EAGER=1
冒烟测试
python scripts/h20_ask.py --ep 8 --max_tokens 8 \
--ids 0,128803,2056,768,17607,2793,3139,12108,1148,128804,128822
期望(当前):中国的首都是**北京**。
分层对拍
# 官方侧
cd /data/models/DeepSeek-V4.1-Flash-1017/inference
PYTHONPATH=$PWD torchrun --nproc-per-node 8 \
<repo>/scripts/ref_layer_dump.py \
--ckpt-path /data/models/DeepSeek-V4.1-Flash-1017-TP8 \
--config config_fp8.json --out /data/deepinfer-h20/ref \
--ids 0,128803,2056,768,17607,2793,3139,12108,1148,128804,128822,17607,2793,3139 --moe_layers 3
# 我方侧(同一条 ids,注意 14 token 门控)
DEEPINFER_DUMP_LAYERS=/data/deepinfer-h20/ref/mine8 \
DEEPINFER_DUMP_TOKENS=14 DEEPINFER_DUMP_MOE_LAYERS=3 \
python scripts/h20_ask.py --ep 8 --max_tokens 1 --ids <同上>
# 对比
python scripts/compare_layers.py \
--ref /data/deepinfer-h20/ref/ref_layers.npz \
--mine /data/deepinfer-h20/ref/mine8/deepinfer_layers.npz
权重正确性(不占 GPU)
python scripts/h20_expert_weight_check.py --layer 0 --expert 370
python scripts/h20_quant_check.py
6. 当前漂移:TODO
6.1 现象
答案已正确,但输出多了 markdown 加粗:
官方: 中国的首都是北京。
我方: 中国的首都是**北京**。
多出的 token 是 666(**)。说明还有残余数值差异,在个别位置把 top-1 推偏。
6.2 已量化的残余误差
| 环节 | rel_err | 是否异常 |
|---|---|---|
| 专家权重(vs 官方 converter) | 2.7e-2 | 否,见下 |
| layer0 attention 子层 | 4.7e-2 | 否,FP8 合理量级 |
| layer0 整层 | 1.27e-1 | 待 P3.7 复测 |
| 最终 top-1 | 个别位置翻转 | 待定位 |
那 2.7% 的性质:per-32 e8m0 → per-128 fp32 的重量化损失。FP8 e4m3 只有 3 位
尾数,理论量级就是 ~3%,所以它不是 bug。
但注意:官方 cast_e2m1fn_to_e4m3fn 是无损的 —— 它把 per-32 的相对 offset 折进
FP8 数值里,scale 只保留每 32×32 块一个(6.0 * 2^6 = 384 < 448,刚好放得下)。
也就是说这 2.7% 是我们主动引入的,不是必然的。
6.3 建议的下一步(按收益排序)
-
P3.6 改用官方无损重化 照
cast_e2m1fn_to_e4m3fn的做法:per-32 e8m0 scale 保持不变(只留块内最大 offset), 相对偏移折进 FP8 值。理论上把 2.7% 降到 0。 前置确认:SM90 DeepGEMM 是否接受 e8m0 per-32 的 expert scale —— 当前配方是 fp32 per-128,(1,128,128)recipe 可能不接受别的 grid。若不接受,则此路不通, 只能接受 2.7%。 -
P3.7 重跑 P2 分层对拍 确认 layer0 整层 rel_err 从 1.27e-1 降到 ~3e-2 量级,看误差累积曲线是否变平。 这是验证 §6.2 判断的直接手段。
-
P3.8 定位
**的具体来源 逐位置跑 greedy,找出从第几个 token 起 top-1 与官方不同,再对那个位置做单层对拍。 如果所有位置的 top-1 都对、只是官方提前 EOS,那是采样/EOS 逻辑问题,不是数值问题。 -
P3.9 复核 index KV 字节布局 SM90 用
FP8_FLOAT32_SF_INDEX_KV_FORMAT(128 fp8 + 1 f32 = 132 B/token), store kernel 是否按「整页 values 后接整页 scales」写、_decode_p_sum_sm90的读法 是否一致,从未做过 round-trip 验证。目前 attention 子层 4.7% 的误差里可能含此项。 验证方法:写入已知向量 → 读出 → 比 bf16 原值。 -
P4 decode 专项(仅当 prefill 已完全对、decode 仍偏时)
- KV cache 写入后立刻读回,比 bf16
- 同一位置 decode 路径 vs prefill 路径重算,比 logits
_decode_p_sum_sm90的数值 vs prefill 的fp8_fp4_mqa_logits
-
P5 logprob 异常 采样结果里
token_logprobs全是0.0(概率=1),不正常。 先确认是采样器 bug 还是 logits 极端(打印 logits 的 max/median)。
6.4 收尾时要复核的绕过项
这些都能跑,但都会影响数值或性能,正式发布前应逐个review:
| 绕过 | 原因 | 数值风险 |
|---|---|---|
DEEPINFER_ENFORCE_EAGER=1 |
graph capture 期断言失败 | 无(仅性能) |
DeepSelect → torch.topk |
SM90 launch 失败 | 无(契约等价,已单测) |
decode indexer → _decode_p_sum_sm90 |
page size 不符 | 有,未经对拍 |
| 关闭共享层候选块优化 | kernel SM100-only | 无(稠密等价) |
mega_mhc / mega_moe 关闭 |
SM100-only | 无(有单测) |
EP_DISABLE_GIN=1 |
无 RDMA | 无(改走 NVLink all_to_all,已单测) |
| ViT → SDPA | FA-4 是 SM100-only | 无(多模态路径) |
性能尚未系统测评。当前只测了两个点:稀疏 MLA prefill 104 TFLOP/s、decode 0.23 ms/step(batch=8, topk=1024)。端到端吞吐、vs vLLM/SGLang 的对比都还没做。
7. 方法论:下次遇到同类问题怎么少走弯路
-
先固化基准,再谈对错。 官方
generate.py是 greedy,是同卡确定性参考。 没有基准的话,「看起来像中文」会被误判成「对了」——本次一开始就吃过这个亏。 -
对齐输入比对齐输出更重要。 chat 模板这个坑让前面几小时的文本对比全部作废。
-
离散量先查。 路由 id、top-k 索引这类离散量,一次比较就能给出确定判决; 连续量(hidden state)只能告诉你「有偏差」。这也是 P3 先查路由的原因。
-
比之前先确认「比的是同一个东西」。 子层输出 vs 残差流、真实 prompt vs dummy profile —— 这两个坑都让对比结果看起来像「第 0 层就全错」,白白惊慌半天。
-
单测的输入分布要覆盖真实调用。
make_indices按[0, t]造索引,正好掩盖了 绝对位置这个 bug。写单测时要问:真实调用会传什么形状/量级? -
优先找权威参考实现。 官方
inference/convert.py里就有cast_e2m1fn_to_e4m3fn, 对拍它的成本远低于自己推导。找到它之后,那个 bug 10 分钟就定位了。 -
精度损失和逻辑错误要分开判断。 FP8 有 ~3% 的理论损失,
rel_err=0.53显然不是 精度问题;区分开之后搜索空间立刻缩小一个数量级。