我在一份 Megatron 转换流程的检查清单上看到”MTP round-trip”这一项,完全不知道它在说什么。两个缩写,一个连字符,而且显然重要到被人专门列成了一条待验证项。
搞明白它的过程把我带到了一个没预料到的地方。真正有意思的不是 MTP,而是投机解码为什么能成立——它建立在一个我原本理解反了的硬件事实上。
TL;DR
- 生成文字慢,是因为它严格串行:每出一个 token,就要完整跑一遍模型。
- 但一次前向处理五个 token,和处理一个,耗时差不多。 瓶颈在搬运权重,不在算术。
- 投机解码就是吃这个差价:用便宜的东西先猜 k 个,大模型一次前向把这 k 个全验掉。
- 它是精确的,不是近似。输出分布和正常解码完全一致。
- MTP(Multi-Token Prediction)是生产这些草稿的一种办法——一个训进模型内部的小模块。
- MTP 有两条命:训练期的辅助 loss(可以扔),和推理期的草稿头(不能扔)。
为什么生成文字慢
要产出第 N+1 个 token,模型必须先有第 N 个。这个顺序绕不开,”语言模型”就是这个意思。
所以生成 100 个 token,就是把整个网络完整跑 100 遍。对 GLM-5.2 这种模型来说,是 78 层,跑 100 次。
看起来的结论是:生成比读 prompt 贵 100 倍。这个结论是错的,而它错的方式正是全部重点。
让我卡住的那一点
一次前向处理一个 token,和一次前向处理五个 token,实际耗时差不多。
我一直以为计算量随 token 数线性增长。并不是,因为计算根本不是瓶颈。
每跑一次前向,都得把模型权重从显存搬到计算单元里。那是几百 GB 的数据穿过内存总线,而且不管你处理的是 1 个 token 还是 50 个,这趟搬运都要走完。至于搬过来之后在这几个 token 上做的那点算术,跟搬运的开销比起来是零头。
所以账是这么算的:
| 权重搬运次数 | |
|---|---|
| 5 个 token,一个一个出 | 5 次 |
| 5 个 token,一次前向出 | 1 次 |
同样的 token,同样的计算量,五倍的内存流量——差别纯粹来自顺序。
这个比值就是奖金。如果你能一次处理五个 token,就能快大约五倍。但你不能,因为第 3 个依赖第 2 个。
除非你猜。
投机解码:先猜,再并行验
诀窍是把生成拆成两个角色。
- 某个便宜的东西先往前猜 k 个 token。这些是草稿。
- 大模型跑一次前向,同时覆盖这 k 个位置,逐个检查。
- 从头开始,对的收下,遇到第一个错的就停。
- 重复。
第 2 步是整个技巧的支点,而它依赖一个容易忽略的性质:生成是串行的,但验证是并行的。
串行依赖之所以存在,是因为你没产出第 2 个 token 就不知道第 3 个是什么。但在验证阶段,候选的 1 到 k 个 token 已经摆在那了——草稿生成器刚交上来。所以可以把它们全部铺开,一次前向查完,没有任何需要等待的东西。
猜对三个,你就用一次权重搬运产出了四个 token。第一个就猜错,那就退回去出一个 token——反正本来也是这个结果。
它是精确的,不是近似
这是我以为一定有猫腻、结果没有的部分。
投机解码产出的输出分布,和普通解码完全一致。它不是拿质量换速度。最终输出里的每一个 token,都是大模型自己认可的——草稿生成器的猜测始终只是建议,任何大模型自己不会给出的建议都会被拒绝掉。
接受判据的设计保证了:活下来的 token,其分布和大模型独自生成时严格相同。所以这里没有精度旋钮可调,也没有质量回退要盯。要么更快,要么不更快,仅此而已。
草稿生成器不好的唯一代价,是白干了打草稿的活。所以真正要看的指标是接受率——猜出去的东西有多大比例活下来。
草稿从哪来
有好几种办法,MTP 到这里才登场。
| 草稿来源 | 怎么回事 | 代价 |
|---|---|---|
| 另一个小模型 | 拿 1B 给 70B 打草稿 | 要额外训练、部署、维护;两边 tokenizer 还得一致 |
| N-gram / 查表 | 直接从上文里抄候选 | 免费,但只在输出大量重复输入时管用——改代码、做摘要 |
| MTP 头 | 训进主模型内部的小模块 | 多一层 |
用独立小模型是最早的做法,但有个别扭之处:两个模型各训各的,想法不一定一致。一旦草稿生成器的直觉和大模型分道扬镳,接受率就掉;而一个猜什么都被拒的草稿生成器,是纯粹的额外开销。
这就引出了把草稿生成器做进模型里的动机。
MTP 到底是什么
Multi-Token Prediction,多 token 预测。 GLM-5.2 的配置里写着:
1 | "num_hidden_layers": 78, |
78 层主干,外加一个模块,专职预测”下下个 token”。这个模块就是 MTP。
两个性质让它成为好的草稿生成器:
它和主模型想法一致,因为它是和主模型一起训出来的,并且直接架在同一套内部表示上。它猜的东西,就是主模型大概率也会给出的东西——而这恰恰是接受率奖励的。DeepSeek-V3 用的是同一套设计,论文报告下一个 token 的接受率在 85–90%,端到端吞吐提升约 1.8 倍。
它很便宜,因为它复用了主模型已经算好的中间结果。打一次草稿的开销是多跑一层,而不是多跑一整个模型的前向。
而且没有第二个 checkpoint 需要做版本管理、部署和同步。草稿生成器就装在模型里面。
MTP 有两条互不相干的命
这是我不去查就一定会搞错的地方。
训练期,MTP 是一个辅助 loss。逼模型不只预测下一个 token、还要预测下下个,会推着它学出携带更多前瞻信息的表示,主模型本身因此变强。这种用法下 MTP 是一次性的——训练结束可以直接把模块扔掉,收益留在主模型里。
推理期,MTP 是草稿头。这种用法下必须留着,扔了就没有加速。
同一份权重,两个毫不相干的在意它的理由。你需不需要 MTP 活着穿过你的流程,完全取决于你要的是哪一个。
回到那份检查清单
到这里才说得清”MTP round-trip”为什么值得单列一条。
训练流程要在格式之间来回转换:HuggingFace → Megatron 去训练,训完 Megatron → HuggingFace 去上线。round-trip 就是这一圈,而问题是:MTP 模块能不能完好地从另一头出来。
它需要单独一条检查项,是因为丢了它是静默的。MTP 不在主前向路径上——它不影响模型说什么,只影响说得多快。于是:
- 转换不报错
- 前向对拍通过
- 训练跑完,loss 曲线正常
- 导出成功
- 上线能用,吞吐减半,日志里什么都没有
我去看了引出这一切的那个验证脚本。Step 5 的做法是:把 HuggingFace 和 Megatron 两个实现喂同一份输入、比较输出,来验证权重映射写对了没有——cosine 0.9936,干净通过。而脚本里还有这么一行:
1 | if hasattr(hc, "num_nextn_predict_layers"): hc.num_nextn_predict_layers = 0 |
测试时 MTP 被关掉了。 这对那个测试本身是正确的做法——关掉旁支才能隔离主干,出问题时指向明确。但后果是:那份被验证过的映射,对 MTP 只字未提;而且就算 MTP 真的错了,前向对拍也抓不到,因为 MTP 压根不碰被比较的那条路径。
真正该做的验证是另一种类型的:不是”模型行为还对不对”,而是”每一块权重还在不在”。把转换前后的张量列出来,比名字、比形状,并且要求数值完全相等——格式转换不做任何算术,不存在浮点误差可以容忍。这更接近盘点存货,而不是跑测试。
我从中拿到的
我进去的时候以为 MTP 是主题、投机解码是背景。其实是反过来的:投机解码才是那个想法,MTP 只是它其中一个零件的一种实现。
但真正值得带走的,是底下那个硬件事实:一次前向的开销,几乎不取决于里面装了几个 token。 想通这一点之后,投机解码就不再像个巧妙的把戏,而变得理所当然——车上反正还空着四个座位,那不如猜猜谁要上车。
这件事和我在给 vLLM 做多副本扩缩容时撞见的问题也是一个韵脚:直觉里的成本模型是错的,而这个错误只有在你真去看硬件在干什么的时候才会暴露。
English version: Speculative Decoding and MTP: Why Guessing Is Free