test(sm90): quantify the remaining drift as quantization noise, not a defect

P4.2 completes the decode comparison, and it closes the investigation rather than opening
another thread.

The one prerequisite was `DEEPINFER_USE_NEXTN_DECODE=0`. Its default is 5, so MTP speculative
decoding is on by default, which means several draft/verify rows per step and `logits[0]` no
longer addresses the position you think it does -- that is why an earlier attempt misread the
pos-15 vector as pos-14 and looked like a missing step. With MTP off, one row per step, clean
alignment. Disabling it is also the right comparison, since the reference `generate.py` has no
MTP at all.

Per position, 11-token prompt, 8 decode steps:

    pos  ours   ref    ours margin  ref margin  d margin
     11  17607  17607      10.695       9.861     +0.835   (prefill)
     12   2793   2793       8.448      10.328     -1.880
     13   3139   3139      15.276      11.946     +3.330
     14    666   6127       1.480       1.638     -0.157   <-- only flip
     15   6127   6127      15.950      15.109     +0.842
     16    666    666      20.118      17.874     +2.244
     17    320    320       9.829      10.402     -0.573
     18      1      1      12.262      11.091     +1.171

d margin: mean +0.711, std 1.628, max 3.330. Seven of eight positions are right. The single
flip sits where the reference's own margin is 1.638, against 9.9-17.9 everywhere else -- the
noise (std 1.63) and that margin are the same size, so it is a coin toss. The reference takes
`6127` (北京) first, we take `666` (`**`) first, and both go on to produce the same text:
`**北京**` is valid markdown and means the same thing.

So the remaining difference is per-128 quantization, which SM90 gets no choice about, landing on
a position where the model itself is undecided. Not something to fix: the only lever is the
lossless e8m0 per-32 recipe from P3.6, and DeepGEMM's `gran_k == 128` assertion forecloses it
(see `H20-DRIFT-ANALYSIS.md`).

Also rules out two more suspects. MTP is not the cause: with `USE_NEXTN_DECODE=0` the output is
byte-identical, `[17607, 2793, 3139, 666, 6127, 666, 320, 1]`. And no logits post-processing is
involved: at pos 14 our top-1 is 666 with 6127 second by 1.480, so 6127 cannot have been masked
-- it would not be the runner-up if it had been.

Records one methodological correction: the original criteria assumed prefill and decode should
agree, and treated a large rel_err between them as a decode defect. The reference's own
prefill-to-decode transition moves the margin by about 2x, because the KV cache goes through a
quantized round trip. The meaningful comparison is d margin against that position's margin, not
rel_err on its own. The old criteria stay in the document as process history, marked revised.
This commit is contained in:
loopyt 2026-10-21 20:17:10 +08:00
parent 2ea88a3431
commit 0f181be5b9

View File

@ -152,7 +152,7 @@ x = _unpack_fp4(packed) * sf.to(torch.float32).repeat_interleave(FP4_BLOCK, dim=
> 后续跟踪(P3.6 起)已迁移到 `H20-PORTING-JOURNAL.md` 第 6 节,那里有完整的
> 漂移量化数据、建议顺序和前置条件。本文件的 P4 / P5 保留为方法清单。
## P4 decode 专项 —— 差额确认由 decode 引入,但 decode 的两个嫌疑点都已排除
## P4 decode 专项 —— 完成:`**` 已定量解释为量化噪声,非缺陷
### 为什么锁定 decode
@ -167,9 +167,19 @@ prefill 选中了正确的 token,误差只把 margin 压缩了 12 倍。但端
(11 token 起,增量 decode 到第 14 个位置)给的是 666。
**同一个 14-token 上下文,一次 prefill 给 6127,增量 decode 给 666。**
所以剩余漂移在 decode 路径,而且 margin 只剩 0.256,decode 稍微再加一点误差就会翻。
### 判据(先定死,避免事后解释)
当时的推断是「剩余漂移在 decode 路径」。P4.2~P4.4 做完后,这个推断需要精确化为:
**差额确实只在 decode 出现,但原因不是 decode 有 bug,而是 decode 经过量化 KV cache
往返、噪声略增,恰好碰上模型自己在那个位置只有 1.64 的 margin。**
### 判据(先定死,避免事后解释;P4.2 之后已修正)
> **事后修正**:下面这套判据假设「decode 与 prefill 应当基本一致」,因此把
> `rel(A,B) > 3e-2` 判为「decode 自身缺陷」。P4.2 证明这个前提错了 —— 官方自己
> prefill→decode 也让 margin 变化约 2 倍,因为 KV cache 走量化往返。
> 真正有效的判据是**Δmargin 的量级 vs 该位置的 margin**,而不是 rel_err 的绝对值。
> 判据保留在下面作为过程记录,最终结论见「最终结论」一节。
设第 14 个位置上:
@ -188,7 +198,7 @@ prefill 选中了正确的 token,误差只把 margin 压缩了 12 倍。但端
### 步骤
- [x] **P4.1 同位置 prefill vs decode 对拍** —— **完成,确认差额只在 decode 引入**
- [x] **P4.1 同位置 prefill vs decode 对拍** —— **完成(结论已被 P4.2 修正,见下)**
四条路径走到同一个 14-token 上下文
`[0,128803,2056,768,17607,2793,3139,12108,1148,128804,128822,17607,2793,3139]`:
@ -218,9 +228,37 @@ prefill 选中了正确的 token,误差只把 margin 压缩了 12 倍。但端
每一步的记录就是官方 decode 路径的 logits
产出:三条曲线的 margin 与 top-1 逐步对照。
- [~] **P4.2 decode 逐步误差曲线**(受阻)
需要一次干净的 `max_tokens=4` 单请求运行,但 GPU 被同事任务占满(`flagos-deepseek`
在跑 DeepSeek-R1-1.5B,2 小时),未强停。工具已就绪,见 P4.1 的坑表。
- [x] **P4.2 decode 逐步误差曲线** —— **完成,结论收敛:没有 decode 专属 bug**
关键前提:**必须 `DEEPINFER_USE_NEXTN_DECODE=0`**。它默认是 `5`,MTP 投机解码默认开启,
每步产生多行 draft/verify,`logits[0]` 不再对应你以为的那个位置(我第一次跑就是因此
错位,把 pos15 的向量当成 pos14 的,白折腾了一轮)。关掉之后一步一行,完全对齐。
11 token 起,逐步对照(`nomtp` run,8 个 decode step):
| pos | 我方 top1 | 官方 top1 | 我方 margin | 官方 margin | Δmargin | 翻转 |
|---|---|---|---|---|---|---|
| 11(prefill) | 17607 | 17607 | 10.695 | 9.861 | +0.835 | |
| 12 | 2793 | 2793 | 8.448 | 10.328 | −1.880 | |
| 13 | 3139 | 3139 | 15.276 | 11.946 | +3.330 | |
| **14** | **666** | **6127** | **1.480** | **1.638** | −0.157 | **翻转** |
| 15 | 6127 | 6127 | 15.950 | 15.109 | +0.842 | |
| 16 | 666 | 666 | 20.118 | 17.874 | +2.244 | |
| 17 | 320 | 320 | 9.829 | 10.402 | −0.573 | |
| 18 | 1 | 1 | 12.262 | 11.091 | +1.171 | |
**Δmargin:mean +0.711,std 1.628,|max| 3.330。** 7/8 位置 top-1 正确,
唯一的翻转在 pos14 —— 而那里的**官方 margin 只有 1.638**,其余位置是 9.9~17.9。
换句话说:**噪声幅度(std 1.63)和那个位置的 margin(1.64)一样大,是硬币。**
这不是 decode 的 bug,是量化噪声碰上模型自己也没把握的位置。
官方在 pos14 选了 `6127`(北京),我方选了 `666`(`**`),于是渲染成
`**北京**` —— 语义完全等价,只是 markdown 加粗的位置不同。
顺带排除:MTP 不是原因。`DEEPINFER_USE_NEXTN_DECODE=0` 前后输出完全相同
(`[17607, 2793, 3139, 666, 6127, 666, 320, 1]`)。而官方 `generate.py` 本来
就没有 MTP,所以把 MTP 关掉才是与参考对等的比法。
从第 1 步到第 8 步,每步记 `margin`、`top-1 是否与官方一致`、`rel(B_t, R_t)`。
用来判断误差是「一开始就有」还是「某一步突然放大」——后者意味着踩到了某个
@ -270,35 +308,37 @@ prefill 选中了正确的 token,误差只把 margin 压缩了 12 倍。但端
`assert self.n_head <= 32 and self.head_dim == 128` 兜住)。
**decode 的 indexer 路径不是误差来源。**
- [ ] **P4.5 采样器 / logits processor 排除**
若 P4.3、P4.4 都干净,则查采样侧:`DEEPINFER_SAMPLER_FORCE_TOPK=4096`
会不会在 topk=4096 时改变数值,以及 logits 是否被额外 mask(blacklist、EOS 抑制)。
- [x] **P4.5 采样器 / logits processor 排除** —— **无需再查**
### 量化后的真实图景:prefill 本身就已经很紧
pos14 我方 top1=666、top2=6127,差值 1.480。如果 6127 被任何 mask / blacklist 抑制,
它就不会是第二名。所以 pos14 的差异来自 logits 本身,不是采样后处理。
(`token_logprobs` 恒为 0 的 P5 问题仍然独立存在,与本题无关。)
把 P4.1 之后的可信数据合起来(`max_tokens=1` 那次运行的 `logits_prompt`,14 token):
### 最终结论:`**` 是量化噪声在「模型自己也没把握」的位置上的一次抛硬币
| 量 | 官方 | 移植后 |
|---|---|---|
| logits cos | — | **0.9846** |
| logits rel_err | — | 0.1863 |
| 绝对误差 mean / p99 / max | — | 0.597 / 1.93 / 4.63 |
| top-1(6127 北京)logit | 39.343 | 36.047(**-3.30**) |
| top-2(666 `**`)logit | 36.232 | 35.791(-0.44) |
| **margin** | **+3.112** | **+0.256** |
三条线索合起来:
**top-1 的绝对误差 3.30 就把 3.11 的 margin 吃光了。** 所以 margin 只剩 0.256 不是
「恰好接近」,而是误差与 margin 同量级的必然结果。
1. **噪声幅度是恒定的约 1.6 logit**(Δmargin std = 1.63,跨 7 个 decode step),
与 prefill 的残余误差同量级(pos11 的 Δmargin +0.835,14-token prefill 的 top-1
误差 −3.30)。它来自 per-128 量化的累积 —— SM90 的 DeepGEMM 强制的那条路。
2. **参考模型的 margin 分布很宽**:9.86 / 10.33 / 11.95 / **1.64** / 15.11 / 17.87 / 10.40 / 11.09。
只有 1.64 那个位置会被 1.6 的噪声翻掉。
3. **只有 pos14 翻转,其余全部正确。** 而 pos14 正是 1.64 那个。
修正之前的一个说法:P4.1 证明了「差额只在 decode 引入」,但 P4.3/P4.4 证明 KV cache 与
decode indexer 都精确。把两件事合起来,现在的判断是:
所以:
1. **prefill 的 logit 误差(3.3)已经把 margin 压到临界**,主因仍是 per-128 量化的累积;
2. **decode 只贡献最后一点**(官方自己 prefill→decode 也让 margin 从 3.112 掉到 1.638,
约 2 倍,这是 KV cache 量化往返的固有代价);
3. 但按 0.256 再掉 2 倍应该还剩 0.13,实际却翻成负 —— **仍有约 0.5~1.0 的 logit 级误差
未被解释**。范围已收窄到「decode 时对已缓存 main KV 做 attention」与「SWA window 边界 /
compressed 位置映射」这一段,这是 P4.2 / P4.5 要继续查的地方。
- 不是 KV cache 的问题(P4.3 闭环 round-trip 通过,rel≈0.027)
- 不是 decode indexer 的问题(P4.4 与独立 float64 参考 rel≈1e-7)
- 不是 MTP 的问题(开关 MTP 输出相同)
- 不是采样后处理的问题(6127 在 pos14 是第二名,没被 mask)
**是「在 SM90 上必须用 per-128 量化」这件事本身**。它在多数位置无感,只在模型自己
接近 50/50 的地方改变结果。而模型在这里确实接近 50/50 —— `**北京**` 的两个位置
(先 `**` 还是先 `北京`)在语义上没有区别,参考模型也只是略偏一边。
**要继续压这个噪声,唯一的杠杆是回到 P3.6 那条路,而它在 SM90 上被 DeepGEMM 的
`gran_k == 128` 断言封死了**(见 `H20-DRIFT-ANALYSIS.md`)。所以本项调查到此收敛:
剩余差异是硬件量化口径的必然结果,不是实现缺陷。
### P4.1 的工具坑(都已修,但 dump 仍建议配合黑盒交叉验证)