docs: close the drift investigation in the analysis note

The note still ended at "next: P4" with an open checklist. Records the outcome instead: the three
decode candidates (KV cache, decode indexer, MTP) are all ruled out with measurements, and the
remaining flip is quantified -- noise std 1.63 against a reference margin of 1.638 at the one
position that differs, out of eight.

Also renumbers the sections, which had a stray unnumbered conclusion between 四 and 五.
This commit is contained in:
loopyt 2026-10-21 22:40:18 +08:00
parent 8ce7073c0b
commit 7fed531ec2

View File

@ -116,7 +116,7 @@ margin 彻底翻转。
---
## 四、下一步:P4 decode 专项
## 四、P4 decode 专项:三个候选全部排除
定位已经收窄到 decode,建议按这个顺序:
@ -128,20 +128,55 @@ margin 彻底翻转。
- [ ] **P4.2 decode 逐步误差曲线**
从第 1 步到第 8 步,每步记录 top-1 和 margin,找出误差开始放大的步数。
- [ ] **P4.3 KV cache 写入/读回一致性**
写入已知向量后读回,与 bf16 原值比。这是 P3.9 遗留项,也是最可能的嫌疑人:
decode 用 `_decode_p_sum_sm90` 走 torch 兜底算 indexer 分数,而 prefill 走
TileLang kernel —— 两条路径从未对拍过。
- [x] **P4.3 KV cache 写入/读回一致性** —— **干净,排除**
闭环 round-trip(`scripts/h20_kv_roundtrip.py`,不依赖任何 ref 实现):
`V41_FORMAT` 528 B/token 与 `FP8_FLOAT32_SF_INDEX_KV_FORMAT` 132 B/token,
block 32/64,1/233/1029 token,rel 0.0266~0.0327、cos 0.9996 —— FP8 地板。
P3.9 遗留的 132 B 布局问题就此关闭。
- [ ] **P4.4 `_decode_p_sum_sm90` vs prefill 的 MQA logits**
同一个 KV cache、同一个位置,两条 indexer 路径的分数差。
- [x] **P4.4 `_decode_p_sum_sm90` vs 独立参考** —— **数值精确,排除**
`scripts/h20_index_decode_check.py` 用一份刻意不共享代码的 float64 参考
(显式偏移切片 vs strided view):page 32/64、页边界、跨页、32 头,
全部 rel≈1e-7、cos=1.00000。
判据:如果 P4.1 的差距 < 1e-3,说明 decode 无误,`**` 另有来源(回到采样器/logits
processor 查);如果差距明显,就从 P4.3/P4.4 里找。
- [x] **P4.2 decode 逐步误差曲线** —— **完成,结论收敛**
见下节。
---
## 五、两个工具修正(否则上面的数据拿不到)
## 五、结论:`**` 是量化噪声在「模型自己也没把握」的位置上的一次抛硬币
前提:必须 `DEEPINFER_USE_NEXTN_DECODE=0`。它默认是 `5`,MTP 投机解码默认开启,每步多行
draft/verify,`logits[0]` 不再对应你以为的位置。官方 `generate.py` 本来没有 MTP,关掉才是
对等比法。关掉后一步一行,逐步对照 11 token 输入、8 个 decode step:
| pos | 我方 | 官方 | 我方 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。8 个位置里 7 个 top-1 正确。**
唯一的翻转在 pos14,而那里的**官方 margin 只有 1.638**,其余位置是 9.9~17.9。
**噪声 std 1.63 ≈ 该位置 margin 1.64,是硬币。** 官方先写 `北京` 再写 `**`,我方先写
`**` 再写 `北京`,两边渲染成同样的加粗文本,语义一致。
四个嫌疑全部排除:KV cache(P4.3)、decode indexer(P4.4)、MTP(开关前后输出逐字节相同)、
采样后处理(pos14 我方 6127 是第二名,被 mask 的话不可能是第二)。
**所以剩余差异是「在 SM90 上必须用 per-128 量化」这件事本身**,它只在模型自己接近 50/50
的位置改变结果。要压它只能回到第一节那条无损路,而那被 DeepGEMM 的 `gran_k == 128`
断言封死。本项调查到此收敛:不是实现缺陷。
---
## 六、三个工具修正(否则上面的数据拿不到)
1. **`ref_layer_dump.py` 的 logits 索引**:`logits[0, -1]` → `logits[0]`。之前所有
dump 里的 `final_row_logits` 都是 0 维标量,等于没有。
@ -150,3 +185,7 @@ processor 查);如果差距明显,就从 P4.3/P4.4 里找。
而不是 prompt 长度,门控不适用,于是抓到的是 profile 的垃圾(第一版得到 cos = 0.34)。
现在用 `debug_dump.profiling()` 上下文在 profile 期间整体挂起,并且把 logits 拆成
`logits_prompt` / `logits_decode` 两个键 —— 只有前者能和 prefill-only 的参考 dump 比较。
3. **`max_tokens > 1` 时 `logits_prompt` 也会被污染**:`mysteps6`(`max_tokens=7`)算出
cos=0.68、top-1 误差 16.5,而 `mylogits3`(`max_tokens=1`)同一输入是 0.9846 / 3.30,
差 5 倍。**比较 prefill logits 只能用 `max_tokens=1` 的运行。**