15 KiB
DeepSeek-V2-Lite 路由的官方模板敏感性审计
状态:真实官方 BF16 权重、本机 RTX 5090、layer 0–6 连续 forward。 固定模型 revision:
604d5664dddd88a0433dbae533b7fe9472482de0。 研究单位:同一批 128 条公开 source prompt 的三种输入协议。 结论身份:协议敏感性探针,不是能力评测、训练分布、线上流量或专家语义。
1. 这轮究竟要排除什么混杂因素
上一轮已经证明:自然长度不同会改变路由集中度,因此我们不能把所有域间差异都 解释成“内容语义”。
但即使内容和长度都固定,输入模型之前还有一层协议:
纯文本
↓ tokenizer
BOS + content
单轮对话
↓ official chat template + tokenizer
BOS + "User: " + content + "\n\n" + optional "Assistant:"
角色前缀会:
- 增加 wrapper token;
- 改变内容 token 的绝对位置;
- 为每个内容 token 增加可见的左侧上下文;
- 可能让第一个内容 token 在 BPE 边界处重新切分;
- 改变“整段输入统计”的分母。
所以本轮不是问“chat 模型强不强”,而是问:
同一段内容送入同一个 base checkpoint,仅改变官方输入包装时,前六个 MoE 层的路由统计会怎样变化?
2. 官方模板不是自行编写的教学字符串
固定 revision 的 tokenizer_config.json 给出:
{{ bos_token }}
{% for message in messages %}
user -> "User: " + content + "\n\n"
assistant -> "Assistant: " + content + eos_token
system -> content + "\n\n"
{% endfor %}
{% if add_generation_prompt %}"Assistant:"{% endif %}
完整模板字符串 SHA-256:
8aeba567270fa9a8d5372caf4b04affea947c4421d96adc01e3ff94021b0ae8e
特殊 token:
| 角色 | 文本 | ID |
|---|---|---|
| BOS | <|begin▁of▁sentence|> |
100000 |
| EOS / PAD | <|end▁of▁sentence|> |
100001 |
实验通过 Transformers 4.41.2 的 apply_chat_template 生成字符串和 token IDs;
随后再次对渲染字符串编码,并要求两条路径的 token IDs 完全一致。
3. 三个条件如何构成“实验 + 负对照”
每条 source prompt 都渲染三遍:
RAW
[BOS] [content]
USER
[BOS] [User:] [content] [\n\n]
GENERATION
[BOS] [User:] [content] [\n\n] [Assistant:]
比较分两种:
3.1 RAW → USER
这是主要敏感性实验。
- 内容字符前缀相同;
- checkpoint、权重、batch、层、dtype 相同;
- 改变的是左侧角色前缀和尾部换行;
- 尾部换行不能反向影响更早的内容 token;
User:前缀可以通过因果注意力影响后续内容 token。
它能描述“输入协议敏感性”,但不能单独证明训练中的角色语义或能力变化。
3.2 USER → GENERATION
这是因果负对照。
GENERATION 只在 USER 末尾追加 Assistant:。在严格因果 attention 中,
未来 suffix 不应改变此前共享前缀的 hidden state 或路由。
因此它同时验证:
- causal mask;
- batching;
- token prefix;
- route 捕获;
- 分析管线。
若这个负对照失败,RAW → USER 的任何解释都不可信。
4. 固定 cohort 与内容长度合同
四个公开域仍为:
| 域 | 来源 | 使用字段 | 数量 |
|---|---|---|---|
| 英文百科 | WikiText-2 raw validation | text |
32 |
| 中文新闻 | CLUE TNEWS public test | sentence |
32 |
| Python 代码 | OpenAI HumanEval | prompt |
32 |
| 小学数学 | OpenAI GSM8K test | question |
32 |
答案不输入,HumanEval 代码不执行。
4.1 为什么固定 23 个 content tokens
上一轮 24-token matched cohort 的总长包括 BOS:
1 BOS + 23 source-content tokens = 24 raw input tokens
本轮直接把内容合同写清楚为 23 个 regular-tokenizer content tokens。 这样 RAW 条件固定为 24 tokens,同时保留足够多的 TNEWS 候选。
4.2 字符前缀如何重建
BPE 对“完整字符串”和“截断后的字符串”可能在最后一个 token 发生不同合并。 因此不能粗暴地取完整编码的前 23 个 IDs 再假设可逆。
脚本采用:
- 读取 fast tokenizer offset mapping;
- 在目标 token 边界附近搜索字符终点;
- 重新编码字符前缀;
- 只接受恰好得到 23 tokens 的最长前缀。
选样排序固定为:
ascending SHA256(
"llm-atlas-deepseek-routing-template-control-v1"
| domain
| source_id
)
候选与选中来源长度:
| 域 | 可用候选 | 选中 | source tokens min / mean / max |
|---|---|---|---|
| 英文百科 | 1,655 | 32 | 29 / 142.84 / 280 |
| 中文新闻 | 1,609 | 32 | 23 / 24.66 / 30 |
| Python 代码 | 164 | 32 | 42 / 141.78 / 352 |
| 小学数学 | 1,319 | 32 | 31 / 59.88 / 131 |
TNEWS 在完成选中前有 1 条无法构造精确 23-token 字符前缀,脚本显式记录并继续按 哈希顺序选择下一条;其余域为 0。
5. 为什么必须分“整段输入”和“对齐内容”
若直接把 wrapper token 与 content token 全部求和,得到的是部署协议问题:
这一整段实际输入会把 token 路由到哪里?
若只研究内容,需要保证比较的是同一 token,而不是“都在内容字符范围内”就算相同。
本轮为三种条件中的每个 token 记录:
(相对内容字符起点, 相对内容字符终点, token ID)
然后只保留三个条件的精确交集。
5.1 Token 账
| 条件 | 输入 tokens | 对齐内容 tokens | 六层真实 top-6 routes |
|---|---|---|---|
| RAW | 3,072 | 2,874 | 110,592 |
| USER | 3,642 | 2,874 | 131,112 |
| GENERATION | 3,898 | 2,874 | 140,328 |
| 合计 | 10,612 | — | 382,032 |
RAW 原始内容范围共有 2,944 tokens;由于 User: 后的 BPE 边界,70 个首 token
不能以相同 (span, token ID) 对齐,因此从三个条件中同时剔除。
按域的对齐覆盖:
| 域 | aligned / RAW content | 覆盖率 |
|---|---|---|
| 英文百科 | 706 / 736 | 95.9% |
| 中文新闻 | 735 / 736 | 99.9% |
| Python 代码 | 733 / 736 | 99.6% |
| 小学数学 | 700 / 736 | 95.1% |
这意味着 content_only 比较中的每一条 route 都对应相同字符段与相同 token ID。
6. 统计合同
与长度实验相同,抽样单位仍是 source prompt,而不是 token。
每个 layer × scope × mode × domain:
- 32 条 prompt;
- 有放回重采样 2,000 次;
- before / after 使用完全相同的 prompt indices;
- 固定 seed
20260729; - 报告 percentile 95% 区间;
- 不进行假设检验;
- 不做多重比较校正。
聚合口径:
prompt_balanced:每条 prompt 先归一,再让 32 条 prompt 等权;token_weighted:先汇总选中 token routes,再整体归一。
主要正文使用 prompt_balanced,避免少量 tokenization 长度差异改变 prompt 权重。
7. 负对照:21,852 个共享前缀 token 全部 exact
每个 MoE 层:
| Layer | USER 共享前缀 tokens | ordered top-6 exact | 违规 prompt |
|---|---|---|---|
| L1 | 3,642 | 3,642 | 0 |
| L2 | 3,642 | 3,642 | 0 |
| L3 | 3,642 | 3,642 | 0 |
| L4 | 3,642 | 3,642 | 0 |
| L5 | 3,642 | 3,642 | 0 |
| L6 | 3,642 | 3,642 | 0 |
| 合计 | 21,852 | 21,852 | 0 |
content_only 的 USER→GENERATION 在 6 层 × 4 域共 24 个比较中:
CV Δ = 0
total variation = 0
JSD = 0
但若看 full_input,因为 GENERATION 确实新增了 Assistant: token,prompt-balanced
total variation 为 0.037–0.051,平均 0.0445。
这组结果正好说明:
suffix 没有改写过去;整段统计改变,是因为统计对象加入了新 token。
8. 主结果:同一个 User: 前缀在不同深度方向不同
以下使用:
- scope:三个条件精确对齐的 content tokens;
- mode:prompt-balanced;
- Δ:
CV(USER) − CV(RAW)。
| Layer | English | Chinese | Code | Math |
|---|---|---|---|---|
| L1 | 0.633→0.593, −0.040 [−0.063, −0.017] | 0.670→0.621, −0.049 [−0.068, −0.028] | 0.901→0.882, −0.019 [−0.039, 0.001] | 0.817→0.796, −0.021 [−0.049, 0.006] |
| L2 | 0.404→0.405, 0.001 [−0.014, 0.016] | 0.383→0.353, −0.030 [−0.041, −0.016] | 0.684→0.617, −0.067 [−0.084, −0.047] | 0.453→0.455, 0.002 [−0.016, 0.016] |
| L3 | 0.405→0.422, 0.017 [−0.002, 0.033] | 0.415→0.471, 0.057 [0.033, 0.075] | 0.824→0.812, −0.011 [−0.023, 0.001] | 0.541→0.547, 0.006 [−0.011, 0.023] |
| L4 | 0.530→0.555, 0.025 [−0.002, 0.049] | 0.897→0.848, −0.048 [−0.067, −0.030] | 1.064→1.038, −0.027 [−0.039, −0.016] | 0.668→0.649, −0.020 [−0.040, 0.000] |
| L5 | 0.446→0.451, 0.005 [−0.015, 0.021] | 0.478→0.504, 0.026 [0.006, 0.042] | 0.808→0.850, 0.043 [0.032, 0.055] | 0.770→0.779, 0.008 [−0.005, 0.021] |
| L6 | 0.502→0.562, 0.060 [0.036, 0.076] | 0.509→0.539, 0.030 [0.009, 0.043] | 0.851→0.909, 0.058 [0.046, 0.068] | 0.738→0.775, 0.037 [0.013, 0.058] |
8.1 初学者应该怎样读
CV 越高,64 个 routed experts 的份额越不平。
- L1:英文、中文的对齐内容变得更平;
- L2:中文、代码变得更平;
- L3:中文反而更集中;
- L4:中文、代码又变平;
- L5:中文、代码更集中;
- L6:四域全部更集中。
因此不应写成:
Chat template 会让路由更均衡。
也不应写成:
Chat template 会让路由更集中。
更准确的说法是:
在这个 23-token 固定探针中,角色前缀对路由集中度的影响随层和域改变方向; 低层与高层不能用一个单调结论概括。
9. “整段输入”会给出另一幅图
若把 BOS、User:、换行与 content 全部计算,Δ CV 为:
| Layer | English | Chinese | Code | Math |
|---|---|---|---|---|
| L1 | 0.061 [0.041, 0.077] | 0.093 [0.062, 0.112] | 0.014 [−0.009, 0.035] | 0.058 [0.029, 0.083] |
| L2 | 0.093 [0.069, 0.106] | 0.138 [0.106, 0.151] | 0.038 [0.020, 0.052] | 0.098 [0.065, 0.122] |
| L3 | 0.100 [0.072, 0.110] | 0.140 [0.096, 0.159] | 0.078 [0.058, 0.092] | 0.058 [0.031, 0.076] |
| L4 | 0.131 [0.093, 0.152] | −0.012 [−0.033, 0.003] | 0.051 [0.035, 0.063] | 0.066 [0.042, 0.083] |
| L5 | 0.083 [0.050, 0.094] | 0.036 [−0.003, 0.054] | 0.092 [0.070, 0.112] | −0.053 [−0.075, −0.036] |
| L6 | 0.134 [0.090, 0.155] | 0.046 [0.000, 0.069] | 0.103 [0.079, 0.118] | 0.027 [−0.004, 0.047] |
多数格子是正值,与对齐内容的混合方向明显不同。
原因不是统计错误,而是问题不同:
full_input包含重复出现的 wrapper token;content_only只比较相同字符段和 token ID;- wrapper 自己可能有很强、很稳定的路由偏好;
- wrapper 数量占短输入的比例不小。
这也是为什么评测协议、chat template 和 tokenizer 必须进入实验账本,而不能只写模型名。
10. 逐 token top-6 有多稳定
对 RAW 与 USER 中能按 (relative span, token ID) 对齐的内容 token,报告:
set:top-6 专家集合完全相同的比例;J:top-6 专家集合的平均 Jaccard。
| Layer | English | Chinese | Code | Math |
|---|---|---|---|---|
| L1 | set 0.552 · J 0.854 | set 0.600 · J 0.867 | set 0.625 · J 0.867 | set 0.459 · J 0.822 |
| L2 | set 0.606 · J 0.874 | set 0.604 · J 0.869 | set 0.562 · J 0.846 | set 0.577 · J 0.860 |
| L3 | set 0.625 · J 0.881 | set 0.634 · J 0.875 | set 0.570 · J 0.864 | set 0.644 · J 0.890 |
| L4 | set 0.572 · J 0.865 | set 0.590 · J 0.869 | set 0.562 · J 0.831 | set 0.593 · J 0.873 |
| L5 | set 0.540 · J 0.849 | set 0.550 · J 0.841 | set 0.551 · J 0.824 | set 0.610 · J 0.872 |
| L6 | set 0.581 · J 0.859 | set 0.558 · J 0.839 | set 0.529 · J 0.814 | set 0.544 · J 0.846 |
两个数字可以同时成立:
- 只有约 46%–64% 的 token 保持完全相同的 top-6 集合;
- 但平均 Jaccard 仍为 0.814–0.890。
直觉上,这说明很多变化不是“六个专家全部换掉”,而是 top-6 边缘的一两个专家发生替换。 它仍不能告诉我们专家“负责什么语义”。
11. 分布距离不大,不等于逐 token 没变化
RAW→USER 的对齐内容、prompt-balanced:
- total variation 点估计约为 0.024–0.067;
- JSD 点估计约为 0.001–0.008 nats。
聚合分布看起来接近,但上一节仍有大量 token 的 top-6 集合不完全相同。
这是一个重要的测量层级区别:
aggregate distribution close
≠
every token took the same route
相反,也不能从 token route 变化推出输出答案或任务分数必然变化;本实验没有执行完整模型生成。
12. 独立复跑
正式输出:
src/data/deepseek-v2-lite-routing-template.json
src/data/deepseek-v2-lite-routing-template-repro.json
两份完整 JSON:
SHA-256
da1f10333b2fa269e64f9716a0ca6c1a656d23d3e70b8a25d6e59f8a5b3bc1b9
cmp byte-exact。
复现命令:
PYTHONPATH=/path/to/transformers-4.41.2-deps:/usr/lib/python3/dist-packages \
python -B experiments/deepseek/v2_lite_routing_template_probe.py \
--artifact-dir /path/to/deepseek-v2-lite \
--human-eval /path/to/HumanEval.jsonl.gz \
--gsm8k /path/to/gsm8k/test.jsonl \
--tnews /path/to/tnews/test.json \
--tnews-archive /path/to/tnews_public.zip \
--wikitext /path/to/wikitext-validation.parquet \
--output /path/to/template-probe.json \
--per-domain 32 \
--content-tokens 23 \
--batch-prompts 8 \
--layers 7 \
--bootstrap 2000 \
--seed 20260729 \
--captured-at 2026-07-29T08:30:00+00:00
Jinja2 是 apply_chat_template 的运行依赖;正式环境记录为 3.1.6。
13. 可以说什么,不能说什么
可以说
- 固定官方 revision 的 chat template 会改变输入 token 序列;
- 相同字符段、相同 token ID 的内容路由对角色前缀敏感;
- 敏感性随层和域改变方向;
- 追加在未来的
Assistant:不改变此前共享前缀,六层 21,852 / 21,852 exact; - wrapper-inclusive 与 aligned-content 统计回答不同问题;
- 正式运行与独立复跑 byte-exact。
不能说
User:专门激活某类“对话专家”;- top-6 ID 的变化等于专家语义变化;
- 路由更平或更集中等于模型能力更强;
- 这个 base checkpoint 的 route probe 等于 chat model 的线上流量;
- 前六层等于完整 27 层;
- 23-token 前缀代表长对话;
- 不生成答案的实验可以给出任务准确率结论;
- 24 个 layer×domain 区间是已校正的显著性检验。
14. 下一道闸门
- 下载并执行完整 layer 0–26,检查“低层变平、高层变集中”的模式是否延续;
- 加入 system role、few-shot turn 与换行边界的正交扰动;
- 把模板敏感性接到完整生成与输出指标,仍保持 route / output 两张账;
- 在支持环境执行 FlashMLA optimized kernel;
- 继续 FP8 / pipeline traces 与 R1-like 小模型复现。