Multi-token prediction

多 token 预测

深入阅读 · 第 11 章 Sampling——模型如何在一次前向中掏出不止一个 token

上一章讲的 sampling,把一行 logits 变成一个 token。要产出下一个 token,你得把它接回提示词,再把整个模型重新跑一遍——全部 24 层、每一个权重。一段 200 token 的回复就是 200 次完整前向,严格一个接一个,因为每个 token 都依赖前一个。这就是自回归税,也是生成感觉慢的最大单一原因。

让它真正难受的,是每一次前向把时间花在哪。产出一个 token 会把模型里每个权重恰好碰一次,所以一个解码步是访存受限的:GPU 这一步的大部分时间只是在把那十亿来个参数从内存里读出来,而那一个 token 实际要做的数学,相比之下只是个零头。芯片大多在闲着、等内存。于是有一个诱人的问题:既然我们反正要付出把这些权重全都搬进来的代价,能不能为这同一笔钱拿到不止一个 token

这个赌注:先 draft,再 verify

推测式解码(speculative decoding)就是那个说「能」的把戏。思路是先廉价地出接下来几个 token,再让真正的模型在一次前向里把所有猜测一次性查完。查 k 个猜测,开销和正常产出一个 token 差不多——都是过一遍权重——所以每个活下来的猜测,都是你白赚的一个 token。猜得好,一次前向就吐出好几个 token;猜得差,你也只是浪费了一点点廉价的草稿,仅此而已。

传统上你需要第二个、更小的「draft 模型」来做猜测——一个额外的模型,要训练、要加载、还要保持同步。多 token 预测(multi-token prediction,MTP)去掉了这笔成本:模型为它自己打草稿。Qwen3.5 随身带着一个小小的额外模块——MTP 头——它唯一的活,就是在主模型还在收尾当前 token 时,先提议下一个 token。

Qwen3.5 随身带的 MTP 头

它刻意做得很小,因为几乎所有东西都从主模型那里借。它接收两个输入:主模型在当前位置的最后一个 hidden state,以及刚刚吐出的那个 token 的 embedding——关键在于,这个 embedding 来自主模型用的同一个 embedding 矩阵(mtp_use_dedicated_embeddings: false)。它把两者都归一化,用一次投影把它们融合,过一层transformer,再通过主模型用的同一个 tied LM head 读出一个 token。整个头就这么多:

一个 MTP 模块的内部
刚生成的 token → embedding1024-d用的是主模型的 embedding 矩阵last hidden state h1024-d主模型在当前位置的最终 hiddenRMSNormpre_fc_norm_embeddingRMSNormpre_fc_norm_hiddenconcat([emb_norm, hidden_norm])2048 = 2 × 1024fc · Linear 2048 → 1024无 bias一个 full-attention 解码层self-attention + 门控 MLP
一个 FULL-attention 层——绝不是 GatedDeltaNet;它不触碰循环状态
RMSNorm最终共享 LM head(权重绑定)草稿 token · 即「下一个之后」的那个

复用主模型的 embedding 与 LM head,所以这个头很小——一个 transformer 层、三个 norm、一个投影。

虚线框与主模型共享——本身没有新增权重。

config.json: mtp_num_hidden_layers: 1 · mtp_use_dedicated_embeddings: false

有两个细节对后面很重要。第一,这个头的那一层是全量注意力层——不是 18 个 GatedDeltaNet 层里的某一层——而且它自带一个小小的注意力 cache;它从不碰主模型的循环 state。第二,因为 embedding 和输出头是共享的,唯一的新权重就是三个 RMSNorm、一次投影,以及那单独一层。在配置里这就是一行——mtp_num_hidden_layers: 1——一个预测下一个之外再多一个 token 的单一模块。

这个循环:draft 个、verify 一次、接受匹配的

现在让这个头干活。一个解码步变成三个动作。Draft(打草稿):廉价的 MTP 头一个接一个地提议D 个候选 token(Qwen3.5 的默认深度是 D = 1,不过引擎可以把它调高)。Verify(核验):完整模型把窗口[最后提交的 token, d₁, …, d_D]一次批处理前向里跑完,一口气在每个位置都给出它自己的意见。Accept(接受):从左到右走过草稿,保留每一个与完整模型本会选出的相符的猜测;在第一个不匹配处,改吐出一个修正 token,然后停下——在 temperature 0 时,这个修正就是完整模型的 top token;而带采样时,它是从一个由两个模型的概率共同构建的 调整后分布中抽取的,而不是简单地取 target 自己选定的那个 token。一步步看:

speculative decoding——draft、verify、accept
提交
draft
verify
accept
统计
已提交MTP draftofFranceis

示意 token——讲概念,并非本模型的真实输出。

无损 · 与普通解码分布一致最好情况:D+1 = 4 token / 遍 · 最坏情况:1 个 token + 一点点草稿开销
本遍 token:3
遍数:0 · token: 0 / 遍

目前已经提交了 5 个 token。普通解码现在会把全模型跑一遍,恰好再得到 一个 token。speculative decoding 试图一次拿到好几个。

在本项目原生引擎中实测:约 1.06–1.46× 加速,temperature 0 时每次 verify 约 1.3–1.46 个 token——增益取决于文本有多可预测。Qwen3.5 的默认 draft depth 为 1。

算一算收益。如果 D 个草稿里有 K 个被接受,这一步就吐出 K + 1 个 token——K 个好猜测,加上 verify 这趟白赚的一个 token(第一处失误处的修正,或者当所有草稿都对时、末尾再多送的一个 token)。所以过一遍权重,能产出 1 个 token(全被拒——和朴素解码跑同样一趟完整模型前向,只是多花了草稿那一点点开销)一直到 D + 1 个之间的任意数量。

为什么它恰好是无损的

下面这部分,正是让推测式解码诚实、而非以质量换速度的关键:在 temperature 0 时,一个草稿只有在它与完整模型自己在那个位置本会选出的顶选相符时,才会被保留。带采样时,接受判据换了一套,但同样严格——它比较 target 和 draft 对那个具体草稿 token 各自给出的概率,并以概率 接受,被拒时则从一个修正性的残差分布里重新抽样。不管哪种情况,verify 这趟用的都是真模型的真分布;MTP 头永远说不了最后一句话。配上恰当的接受规则——Leviathan 等人提出、Chen 等人独立提出的推测式采样/解码算法,两者都发表于 2023 年——你吐出的 token 流在分布上与朴素的逐个解码完全一致——每一步的概率、以及 temperature 行为都相同(在 temperature 0 时就是完全相同的文本;带采样时则是完全相同的概率),只是用更少的前向产出。MTP 头永远改不了答案——最坏只是付一点点草稿税,最好则能省下好几趟前向。

这也是为什么猜错很便宜:一个被拒的草稿,只花掉产出它的那一点点 MTP 计算。昂贵的 verify 那趟无论如何都要发生——它就是朴素解码本会跑来产出那一个 token 的同一次前向。推测式解码,最坏不过是带一点点草稿税的朴素解码;最好则是好几倍的速度。

这个想法从哪来

MTP 不是一出场就是成品。我们走到这里用了三步,Qwen3.5 所采用的是第三步的设计:

MTP 谱系——三个里程碑
Meta 2024并行
  • n = 4 个并行 head
  • 彼此独立,相互之间没有因果链
  • 训练:作为辅助任务
  • 推理:自我推测式(self-speculative),最高快 3×
  • 在 13B 上 +12% HumanEval / +17% MBPP
arXiv 2404.19737
DeepSeek-V3 2024串联
  • D = 1(一个额外 token)
  • 串联——保持因果链
  • 共享 embedding 与 output head
  • 训练损失 λ·(1/D)·ΣLᵏ,λ:0.3→0.1
  • 推理:丢弃,或用于 speculative decoding
arXiv 2412.19437
Qwen3.5 / Qwen3-Next 2025串联
本课程使用的模型
  • 一个共享的串联 module,depth 为 1
  • mtp_num_hidden_layers: 1
  • 共享绑定的 embedding(mtp_use_dedicated_embeddings: false)
  • 同时提升预训练效率与推理速度
  • 可在 vLLM / SGLang / 本项目的原生引擎中运行

这个领域从并行的独立 head(Meta)转向一个保持因果链的串联 module(DeepSeek → Qwen)。

Meta 2024 年的版本在一个共享主干上栓了四个并行的头,每个同时预测一个不同的未来 token——作为训练信号很棒,但这些头彼此独立,所以没法互相作为条件。DeepSeek-V3 把形状改成了单个串行模块,它预测多出来的一个 token,同时保住因果链——预测 t+2这个 token 的草稿,能看到已经选定的 t+1。Qwen3.5(建立在 Qwen3-Next 架构之上)采用的正是这种串行、权重共享的设计:一个模块,深度为一。

它把自己的饭钱挣了两次

同一个头在两个截然不同的时刻都有回报。在训练时,要求每个位置去预测接下来两个 token 而不是一个,是一个更稠密的学习信号——每一个 token 的数据里都有更多可学的东西。Meta 报告说他们的多 token 模型在 13B 规模下多解出了 12% 的 HumanEval 和 17% 的 MBPP 题;DeepSeek-V3 把一个 MTP 损失折进预训练,其权重从 起步,在最后一段 token 上降到

Qwen 把自己的 MTP 描述为同时提升预训练效率和推理速度。在推理时,同样的这些权重就变成了上面那个循环里白送的自我 draft 者。一个模块,两份工作:一个训练得更好的模型,外加一个更快的模型。

时间都花在了哪里

前面我们说过,解码步是访存受限的——芯片大多在闲着、等权重。只有亲眼看到这一点,speculative decoding 才真正讲得通。下面就是 GPU profiler 会画出来的那幅图:同一条时钟上的两条时间线,上面是朴素解码,下面是 draft → verify → accept。每次前向画成 ≈20 ms 宽,因为这大致就是这个标签页自己的朴素 decode 实测出来的数字(M 系列 Mac 上约 50 tok/s)——而 verify 前向和朴素前向一样宽,因为不管哪种,都只是把权重过一遍。把 draft 深度调高、把文本换得更难猜,看看 trace 会发生什么:

时间都花在了哪里——一条示意 GPU 时间线
batch = 1——你就在这里

这里的脚本结果:6 个 draft 位置里有 3 个通过 verify——单个位置的几率约 55%,但一个位置只有在它前面的全部存活时才算数

D = 1 是 Qwen3.5 自己的 MTP 深度;更大的 D 是推理引擎调高后的示意设置。

朴素自回归——每个 token 一次 ≈20 ms 的前向1 tok1 tok1 tok1 tok1 tok1 tokspeculative——每个 draft token ≈1 ms + 一次 ≈20 ms 的 verify 前向2 tok1 tok2 tok2 tok1 tok1 tok020406080100120140ms——示意时间轴,锚定在每次前向 ≈20 ms把一次前向放大——这 ≈20 ms 究竟花在哪朴素前向——只给 1 个位置打分访存流式读入 ~1.6 GB 的 bf16 权重——整个前向都在忙算力大多在闲着——那点数学只是零头verify 前向——在同一条权重流上给 2 个位置打分访存流式读入 ~1.6 GB 的 bf16 权重——整个前向都在忙算力

两条一样宽是故意的:核验 D + 1 个位置仍然只是把权重过一遍,代价和产出一个 token 差不太多。

verify 前向(把全部权重流一遍)draft——MTP 头,每个 draft token ≈1 ms被 accept 的 draft 位置浪费的位置——算完即弃白赚的 token(来自被 accept 的 draft)这次前向本来就会产出的那个 token

两块 GPU,同一份活。按播放——或单步——扫过播放头,看 token 一个个落地。

朴素那条
6 个 token / 120 ms ≈ 50 tok/s
speculative 那条
9 个 token / 126 ms ≈ 71 tok/s
加速比(这条脚本化 trace)
×1.43
真实测量:D = 1 时约 1.06–1.46×

3 个 draft 位置算完就被丢掉——但它们搭的是反正要付钱的那几次前向 · 草稿税:126 ms 里的 6 ms

一次前向 ≈ 20 ms——不管给 1 个还是 D + 1 个位置打分,权重反正都要整个流一遍。
D 个 draft 里有 K 个被 accept,这一步就吐出 K + 1 个 token;最差情形也有 1 个。
被拒的位置不增加一毫秒墙钟时间——真正多花的只有 draft 本身:每个 draft token ≈1 ms。
这里的 accept 判决是脚本化的(greedy、相当于 temperature 0);temperature 越高,接受率越低。

这趟白车之所以存在,是因为 batch = 1 时上面那条算力行几乎是空的。batch 一大芯片本来就忙——服务器就会把 speculation 关掉。

示意 trace,时间轴是合成的——判决是脚本,不是真实的 profiler 抓取,也不是这个标签页:浏览器里的 WebGPU 解码循环就是朴素自回归。

锚点:这个标签页的朴素 WebGPU decode 在 M 系列 Mac 上实测约 50 tok/s——每次前向约 20 ms,就是聊天 HUD 显示的那个数(随机器与批次波动)。本项目唯一实测过的 speculative 加速是 ~1.06–1.46×(原生 Metal 引擎,Qwen3.5 默认深度 D = 1);这个小部件能摆出的更大倍数,来自脚本化的、偏乐观的接受率——在 D 大于 1 时,还叠加了引擎调高的深度。

有两件事值得注意。第一,D 的收益是递减的:第四个 draft 只有在前三个全部存活时才算数,所以每多猜一个,价值都比上一个低。第二,可预测性卡住一切——在难猜的文本上,speculative 那条只剩残羹:付完草稿税后只比朴素解码快一点点,而且 draft 得越深税越重(看看「难猜」下的 D = 2 反而落在 D = 1 后面)。再看看红色小格对时间线做了什么:什么也没做。被拒的位置从不会把一次前向拉宽——浪费搭的是一趟你反正要付钱的访存。这也正是为什么这个把戏在 batch = 1 时划算、在打满的服务器上会被关掉:白车只有在算力那一行还空着的时候才存在。

一座 drafter 动物园

上面所有内容都把 MTP 头当作那份廉价 draft 的来源。但 draft → verify → accept这副骨架并不在乎猜测从哪来——它只在乎猜测够廉价、以及 verify 那趟守住无损保证。换掉 drafter,你就得到一整个 speculative decoding家族,它们全都共享你刚刚走过的那同一趟 verify。

draft-target 方案(由第二个更小的模型来写 draft——最容易加装,但你现在要跑两个模型);Medusa(在模型自身上长出几个额外的预测 head,不需要独立模型);EAGLE(一个专门打造、读取 target hidden states 的微型模型——通用对话的首选);以及 n-gram / lookahead解码(完全不用 draft model——只是查一下通常接什么,在代码和可预测语法上收益巨大)。你刚认识的 MTP 头,就是 Qwen3.5 随身带的那一种。挑一个 drafter,比一比什么变了、什么没变:

挑选你的 drafter——speculative decoding 家族
you are here · Qwen3.5
已提交廉价 drafterD 个 draft tokenisMTP headParis,a全模型 verify · 一遍只有 verify 接受才保留(T=0 时保证严格匹配)

骨架每次都一样——只有 drafter 这个盒子在变。

draft 来源
模型自带的 MTP head
典型 draft 长度
1 个 token(Qwen3.5 默认 depth)
最适合
本课程已经讲过的部分

模型通过自带的 MTP head 为自己起草——没有第二个模型,也无需训练。这正是本课程已经走过的 draft → verify → accept 循环,也是本项目原生引擎真正运行的那一种。

对每一种 drafter 都成立
  • 全部无损。在 T=0 时,verify 只有在 draft 与真实模型自己的顶选一致时才保留;带采样时,它跑的是同一套概率比接受/重采样判据,所以每一种变体只改变速度,绝不改变答案
  • 低 batch 时收益最大(有富余算力去 verify);在高 batch 时会被动态关闭——GPU 已经被占满了。
  • temperature 越高 → token 越难预测 → 接受率越低

浏览器内的演示这些都不跑——它只做普通的自回归解码,每遍一个 token。本项目的原生引擎运行的是 MTP 变体(在那里约 1.06–1.46×)。请把这个选择器看作一台生产服务器会从中挑选的菜单——代码用 n-gram,通用对话用 EAGLE——它们共享着课程已经证明过的同一套无损 verify

有三条规则对整座动物园都成立。它们全部无损——在 temperature 0 时,verify 只有在 draft 与真实模型自己的顶选一致时才保留;带采样时,它跑的是上面那同一套概率比接受/重采样判据。所以每一种变体只改变速度,绝不改变答案。它们都在低 batch 时收益最大,此时 GPU 有富余算力去白跑那趟 verify;在高 batch 时芯片已经被占满(batching 子章节会讲为什么),于是服务器会把推测关掉。而 temperature 越高,下一个 token 越难预测,接受率就越低。空闲且文本可预测时,推测为你买来速度;反之,它会安静地让到一边。

看真实模型做 verify——就在一次前向里

上面的一切都是示意——预设的 token、写好的剧本。下面这个是现场版:你可以把真正的 Qwen3.5-0.8B 加载进这个标签页,让它在一次真实的前向里 verify 一份廉价的 draft,每个判定都来自模型自己的 logits。这份 draft 来自 prompt-lookup——就是上面动物园里那个 n-gram drafter,这里是真实现的:拿前缀末尾的几个 token 去前缀更早的位置查一查,当时紧跟其后的内容就成了猜测——或者干脆你自己输入一段续写。它不是来自 Qwen 的 MTP head:这份 checkpoint 里没有任何 mtp.* tensor,下一节会讲清楚。还有一句诚实声明:前缀是以原始文本喂给模型的,不套 chat template——这是一次原始语言模型续写,不是一轮对话。

Verify 现场版——真实模型,一次前向
在你的浏览器里运行
已提交的前缀(原始文本——不套 chat template)
The capital of France is Paris. The capital of Italy is Rome. The capital of France is
提议的 draft(不来自模型)
Paris.Thecapital

互动演示——页面启动后加载。

有三点值得注意。这里的 verify 是 greedy、temperature 0——一个 draft token 被接受,当且仅当它就是模型在那个位置自己的 argmax,没有任何放水。被拒时,接着往下写的是模型自己的选择(绿色的纠正 chip)——这正是推测式解码无损的原因:输出就是朴素解码本来也会产出的东西。而这整个检查——每个位置、以及上面每一份可以展开的 top-K 分布——只花了一次过权重的前向。这一次前向,就是这个子章节一直在讲的全部速度故事。(这个小部件是专为演示接出来的一次性 verify 探针——浏览器实际的聊天解码循环仍是朴素自回归,下一节会说明。)

你浏览器里的 Qwen 用它吗?

没有——而且照例,诚实的答案才是有意思的那个。架构是真的:这个具体模型的配置写着mtp_num_hidden_layers: 1,所以一份完整的 Qwen3.5 checkpoint 确实定义了你刚看到的那个头。但有两件事让它在这个演示里没上路。浏览器下载的那份 bf16 checkpoint 在转换时没带上 MTP 权重——它的 474 个 tensor 里,零个mtp.*——而这里的 WebGPU 解码循环是朴素自回归的,一次前向一个 token,完全没有 draft/verify 那套机制。(你不孤单:原版的 Hugging Face Transformers 也会丢掉 MTP 权重;如今主要是 vLLM、SGLang,以及本项目自己的原生 Metal 引擎这类推理引擎,才真的去跑那个循环——在那里实测大约快1.06–1.46×,取决于文本有多好预测。)

所以多 token 预测,和视觉编码器以及完整 checkpoint 里其他「架构有定义、但本次浏览器之旅不会走到」的部分坐在一起:在 Qwen3.5 里是真的,在这里是有意关掉的。你甚至能在架构图里看到它被置灰——那个「MTP head」节点,就在一个 token 在本演示里真正会走的路线之外。