做推理加速的人,几乎都会碰到同一个尴尬:GPU 很贵、显存很大,但生成 token 的速度就是上不去。自回归解码一步只出一个字,剩下的时间基本都耗在把几百亿参数从显存搬到计算单元的路上。投机采样(Speculative Decoding)算是少数立竿见影的手段,而 Parallel Drafting 则是这个方向上最容易被误解、也最值得搞清楚的一套思路。这篇文章想从 P-EAGLE、DFlash 到 DSpark 这三条典型路线,把投机采样里的并行草稿机制拆开讲透,顺带把哪些环节容易踩坑、哪些结论和直觉相反,都摊开聊聊。
1. 为什么投机采样能加速推理:几十毫秒背后的算账逻辑
1.1 大模型推理的瓶颈:访存带宽与自回归的本质
先说一个基本事实:大模型解码时,速度瓶颈通常不是计算量,而是显存带宽。一个 70 亿参数的模型,FP16 精度下光参数就有约 14GB,每次生成一个 token,理论上至少要把这些参数从显存读一遍。假设一张 GPU 的显存带宽是 2TB/s,单次前向传播光是读参数就要 7 毫秒左右,算上 KV cache 读写和中间激活,实际单步延迟至少在 10~20 毫秒。一块几万块的卡,跑起来每秒只能吐几十个 token,用户体验当然不跟手。
自回归的本质决定了这一步一个 token 的串行节奏。当前 token 依赖前面所有 token,GPU 的并行计算能力在这个环节里被严重浪费。算力利用率往往不到 5%,大部分时间都在等数据搬运。正因为这种"算力富余、带宽受限"的状态,投机采样才有了操作空间:与其逼着大模型一步步自回归,不如让一个大模型一次算一批候选 token,把所有候选一次验证掉。
1.2 投机采样的核心原理:草稿与验证的两阶段流水
投机采样(Speculative Decoding)的思路其实很朴素:先用一个更轻量的草稿模型快速生成 n 个候选 token,再用目标大模型对这 n 个 token 做一次前向验证。验证过程的细节不展开,核心是它不能直接贪心地接受全部候选,而要按一定的拒绝采样规则逐个确认。目标模型会把草稿模型给出的每个 token 与自己的预测分布做对比,概率足够一致就接受,不一致就从当前位置用目标模型自己的输出替换。这样得到的分布严格等价于目标模型直接自回归的分布,不会因为投机而改变模型本身的生成质量。
加速比能不能赚到,直接看两个数:草稿模型的生成速度和候选 token 的接受率。可如果草稿模型本身也需要自回归地跑 n 步,那它节省的只是"每步前向传播的参数量变小"——总步数没怎么少,草稿阶段的时间占比很快会从"可忽略"变成"吃掉了大部分收益"。
1.3 投机采样的"隐含税":草稿生成本身成了新瓶颈
我实际测过一些轻量草稿模型方案,印象很深:在 7B 目标模型上,一个 100M 左右的草稿模型,每步延迟大约是目标模型的 15% 左右。粗看很划算,但草稿模型同样要吃显存带宽,只是吃得更少。当草稿长度加到 8 到 16 步时,草稿阶段累计延迟就能到目标模型单步延迟的 1.2 到 2.4 倍。这个"隐含税"会直接吞掉投机采样原本想节省的时间,最终加速比远低于理论值。
Parallel Drafting 就是冲着这个税来的。它不是去压缩草稿模型的每一步耗时,而是把草稿生成从串行变成并行:一次并行算出 n 个候选 token,草稿阶段的总耗时近似等于单个候选的耗时,而不是 n 倍。这听起来有点反直觉,因为生成 token 明明存在依赖关系。想真正理解它,得先搞清楚这几条路线分别用什么方法绕开依赖。
2. Parallel Drafting 的核心思路:把串行草稿换成并行草稿
2.1 从"逐步生成"到"独立候选":打破自回归依赖
并行草稿的关键不是"让自回归模型一次输出多个 token",而是重新定义草稿的生成方式:让 n 个候选 token 互不依赖。这样它们就可以在同一个前向过程里被一次性算出来,甚至可以直接查表取出来。代价是候选质量不再像自回归那样逐 token 动态调整,n 个 token 之间的一致性变弱。
这里要澄清一个常见误解:并行草稿不等于"一步解码出 n 个 token 并直接当作答案"。它只是提供了 n 个独立的候选,真正是否被接受,仍然由目标大模型验证。验证阶段还是自回归的,只不过不是逐 token 验证,而是用一个"对未来 n 个位置的并行预测矩阵"来一次验证全部候选。目标模型的前向计算被复用了,所以总计算量大致等于跑一次 n 步的预填充,而不是 n 次自回归。
2.2 接受率与期望加速比的关系:为什么质量比速度重要
投机采样的期望加速比大致可以用这个式子估算:
期望加速比 ≈ (1 - α^(γ+1)) / ((1 - α)(γ + Δt_draft / Δt_target))
其中 α 是平均接受率,γ 是草稿长度,Δt_draft 是生成草稿的耗时,Δt_target 是目标模型单步前向耗时。式子里的核心信号是:即使草稿生成时间降到接近 0,在接受率低于某个阈值时,加速比依然可能小于 1。比如 γ=8 时,α 掉到 0.5 以下,理论加速比就很不乐观了。
Parallel Drafting 最微妙的地方也在这:它的草稿生成时间可以压得很低,但候选之间的独立性会让接受率天然弱于自回归草稿模型。P-EAGLE 这类特征层外推方案,实际上是在努力让并行候选也保留足够的条件概率信息;DFlash 这类查表方案则在另一个方向上把草稿耗时压缩到极限,赌的是被预测文本有较强的重复模式。
2.3 三类并行草稿路线的整体对比
为了后面讲起来不乱,先给一个总览表,把三条路线的关键差异摆出来。
| 维度 | P-EAGLE(特征外推) | DFlash(表驱动/草稿文件) | DSpark(系统级推测执行) |
|---|---|---|---|
| 草稿生成方式 | 轻量解码器在特征空间外推 | 哈希/表查询直接取候选 | 多草稿源统一调度 |
| 草稿依赖关系 | 并行外推,弱依赖 | 完全无依赖 | 混合,按策略配置 |
| 草稿耗时增长 | 随草稿长度增长较慢 | 几乎不随长度增长 | 取决于组合策略 |
| 接受率天花板 | 中到高 | 中低,取决于文本重复度 | 可动态调整策略 |
| 显存成本 | 轻量解码器参数 | 草稿文件本身 | 各策略的叠加 |
| 适合场景 | 开放生成、通用对话 | 代码补全、模板文本 | 高吞吐服务化场景 |
后面三个章节就围绕这个表展开,分别说清楚每条路线是怎么实现的、边界在哪、实际落地时该注意什么。
3. P-EAGLE:在特征层外推的并行草稿
3.1 为什么选择特征层做外推
先回顾一下 EAGLE 系列的基本思路。EAGLE 系列的洞察是:与其让草稿模型从头到尾跑一遍完整网络,不如直接在目标模型的高层特征上做轻量外推。Transformer 每一层输出的特征向量,已经编码了丰富的语义信息;已知当前位置的最终隐藏状态,去预测下一个位置的隐藏状态,理论上比从 token 级硬猜要容易得多。因为特征空间的平滑性通常比词表空间好得多,词表里一个词的变化可能对应完全不同的语义,而特征向量的变化是渐进的。
P-EAGLE 把 EAGLE 的这种外推过程做了并行化改造。传统 EAGLE 草稿依然是一个轻量自回归过程:拿到第 t 步特征后,预测第 t+1 步特征,再预测第 t+2 步特征,一步步往后推。P-EAGLE 的做法是用一个额外的轻量解码器,一次性地基于当前位置特征输出未来 n 个位置的特征估计。这个解码器可以是几层 MLP,也可以是一个小型 Transformer 解码器;关键在于预测目标从"下一步 token"变成了"未来 n 步的特征序列"。
3.2 P-EAGLE 的并行化改造:一次解码、批量验证
具体实操时可以这样理解 P-EAGLE 的流程:
- 目标模型执行正常前向,得到当前位置的 hidden state。
- 轻量解码器以这个 hidden state 为输入,一次输出 n 个未来位置的特征向量。
- 这 n 个特征向量各自通过目标模型的输出头(LM Head),映射成 n 个词表上的概率分布。
- 从每个概率分布里采样出一个候选 token,组成 n 个草稿 token。
- 目标模型把这 n 个 token 拼接在已有序列后面,做一次并行前向验证,按投机采样规则接受或修正。
第 2 步就是"并行草稿"的关键。轻量解码器一次输出 n 个特征,不需要循环 n 次。得到的 n 个特征虽然来自同一个当前状态,但彼此没有自回归更新,所以是并行的。代价是第 3 步的概率分布少了真实历史条件,候选 token 之间可能出现"前言不搭后语"的情况,接受率会有一定损失。
在实现层面,P-EAGLE 的一个常见工程处理是让轻量解码器输出多个低维特征,再把它们一次性映射到词表。因为特征维度远小于词表维度(比如隐藏维度 4096,词表 128k),这个映射在计算量上比完整 transformer 层便宜得多。我做过的最优实现里,草稿阶段延迟可以控制在目标模型单步延迟的 5%~10% 左右,这比传统轻量草稿模型动辄 15%~20% 的开销要友好不少。
3.3 P-EAGLE 的工程落地心得
如果你想动手集成 P-EAGLE,有几个地方比想象中容易出问题。
第一个是采样一致性。投机采样必须保证草稿分布和目标模型验证时的分布"对齐",否则接受率会非常难看。我在早期版本里犯过错误:草稿阶段用了 top-p=0.8,验证阶段用了 top-p=0.95,结果两个分布偏差很大,几乎所有候选都被拒绝,加速比直接崩到 0.7。排查了很久才发现不是模型问题,而是采样参数不一致。
第二个是草稿长度设置。P-EAGLE 的接受率一般不是均匀分布的:前 1~2 个 token 接受率很高,越往后越低。动态草稿长度在这里非常有用。可以按接受率实时调整下一轮草稿长度,接受率高就加长,连续几轮低就缩短。固定长度不是不能用,只是浪费了最优区间。
第三个是 KV cache 的维护。并行草稿阶段虽然没有自回归依赖,但目标模型验证时是并行前向,这部分计算量的 KV cache 要单独管理,不能和主序列的 cache 混在一起。尤其在多 batch 场景下,不同序列的已接受草稿长度不同,KV cache 的拼接逻辑稍不注意就会索引错位。
4. DFlash:表驱动草稿与"草稿文件"机制
4.1 DFlash 的核心逻辑:草稿从推理变成查表
DFlash 走了一条完全不同的路。它不打算让任何神经网络去"预测"草稿 token,而是把草稿生成变成一次查表操作。这里的核心资产是一个预先构建好的草稿文件(draft file),里面存的是某种上下文特征到候选 token 序列的映射关系。在线推理时,根据当前已有的上下文计算出查询键,直接在表里取出一组候选 token,连神经网络的前向都不需要。
这正是热词里"dflash 草稿文件"指向的东西。草稿文件的构建通常离线完成:在大量语料或领域数据上统计 n-gram 频次、聚类上下文特征、或者训练一个小型的哈希映射模型。然后把这些规则固化成一张查找表,推理时加载进内存。因为查表操作没有循环依赖,n 个候选 token 可以一次全部取出,并行性天然成立,而且没有任何神经网络执行开销。
从硬件角度看,DFlash 也更讨巧。查表的延迟主要看哈希计算和访存,这些操作在 CPU 或 GPU 上都非常容易并行。做得好时,取 n 个候选的耗时只有几十微秒,和目标模型动辄十几毫秒的单步前向相比完全不是一个量级。把草稿阶段的开销压到接近零,这是 DFlash 相比 P-EAGLE 最直接的优势。
4.2 草稿文件的构造与缓存友好性
草稿文件的具体形态可以有很多种,但常见的做法是紧凑哈希表或前缀树。我见过一种比较实用的设计:把上下文窗口里的最近 k 个 token 作为键,使用类似 Bloom Filter 或 locality-sensitive hashing 的方法映射到表项,每个表项里存一组候选 token 序列和对应的置信度。查询时先做一次哈希定位,再把候选读出来,整个流程可以控制在几微秒级。
这里有一个工程细节值得注意:草稿文件要放到访存友好的位置。如果表很大,GPU 显存放不下,要考虑放到主存并用 RDMA 或统一内存访问。如果表不大,尽量放 GPU 共享内存或 L2 cache 附近,避免每次查询都走一次 PCIe。实测下来,把高频表项放在 GPU 端、低频部分留在 CPU 端的分片方案,效果通常比单一大表更好,因为热点很集中。
草稿文件不是越大越好。表太大会稀释命中率,表太小则覆盖率不足。构建时需要在数据分布上做权衡:对代码模型,常见 token 序列的重复度很高,表可以做小一点;对开放式长文本生成,上下文变化太快,表命中率会下降。我会在构建阶段先统计验证集上的 top-k 2-gram/3-gram 覆盖度,再决定表的大小,而不是凭感觉拍一个值。
4.3 DFlash 与 P-EAGLE 的对比:适用场景分明
DFlash 的强项是草稿耗时极低、实现简单、可解释性强;弱项是候选 token 之间没有任何语义建模,接受率上限比较有限。在重复模式多的场景里,比如代码补全、JSON 生成、SQL 生成、模板化文案,DFlash 效果能非常好。我做过一个代码补全的对比实验,DFlash 在这类场景的接受率可以到 60% 以上,加速比甚至超过 P-EAGLE。但换到开放式对话里,接受率可能掉到 20%~30%,加速比就不太理想了。
P-EAGLE 的优势在于候选质量更高,因为它确实在建一个特征层面的条件模型,对开放文本的适应能力更强。它的弱势是草稿阶段仍然有一些神经网络计算,虽然比目标模型轻得多,但终究不是纯粹查表。
选型建议上,我的经验是看任务类型:如果文本模式规律性强,优先 DFlash,简单直接;如果偏开放生成,优先 P-EAGLE 这类质量优先的方案。但也别把两者对立起来,实际框架里完全可以把它们叠加在一起作为两个草稿源,用 DFlash 的高频候选打底、用 P-EAGLE 的动态候选补足长尾。
5. DSpark:把并行草稿变成推理框架的原生能力
5.1 DSpark 想解决的问题:系统级开销
P-EAGLE 和 DFlash 解决的是"草稿怎么生成",DSpark 想解决的是"草稿怎么被系统高效调度"。在真实服务场景里,一个请求可能同时存在多个草稿源、多个验证批次、动态序列长度、不同优先级,这些乱七八糟的状态如果管理不好,草稿阶段省下的时间会被调度开销和内存碎片重新吃掉。
DSpark 本质上是一个推测执行引擎,它不是某一种单一的草稿策略,而是把多个草稿策略统一管理起来。你可以把它理解成一个调度层:每个序列进入解码阶段后,DSpark 会根据当前上下文和系统负载情况,动态选择使用 P-EAGLE 外推、DFlash 查表、还是两者混合,然后统一调度到目标模型的验证前向里。
5.2 推测执行引擎的设计
从我接触过的类似系统来看,一个可用的 DSpark 类引擎至少要包含四个部分:
第一个是草稿源管理。每种草稿策略被包装成一个独立的草稿源,暴露统一的接口:给定当前上下文,返回候选 token 列表和置信度。新加一种草稿策略就是新增一个源,不用改动验证主链路。P-EAGLE 可以是一个源,DFlash 可以是另一个源,后面如果再出现更轻的草稿模型,同样挂进来。
第二个是策略选择器。它决定当前请求该用哪个草稿源、草稿长度该设多少。最简单的是基于规则:文本重复度高的走 DFlash,否则走 P-EAGLE。更进一步的可以做一个轻量决策模型,把请求类型、最近接受率、当前 batch 大小都作为特征,动态调整草稿长度和策略。
第三个是统一验证器。目标模型的前向验证是所有草稿源共用的。无论候选来自哪里,最终都要拼接到主序列后面做一次并行验证。验证器需要支持不同长度的候选对齐、合理的 attention mask 设置,以及 KV cache 的高效更新。这里的实现质量直接决定实际加速比能兑现多少。
第四个是性能反馈回路。系统每轮验证后统计每个草稿源的接受率、草稿耗时、真实加速比,反馈给策略选择器。这个闭环非常关键,因为没有一套策略能适配所有流量,靠动态反馈才能长期稳定。
5.3 实测效果与选型建议
我参考了一些公开的性能数据和自己的对比测试,把 DSpark 这类系统级方案和单独使用某一种并行草稿策略做了对比。单独用 P-EAGLE 时,理想情况下的端到端加速比大约在 1.8~2.5 倍;单独用 DFlash 在代码类任务上也可能到 1.8 倍左右,但在对话任务上可能只有 1.1~1.3 倍。DSpark 这类调度引擎的价值不是拉高单任务的峰值,而是保证不同任务混流时的整体吞吐,实测混流场景里它能比单一策略稳定高出 20% 到 40% 的吞吐,代价是工程复杂度明显上升。
如果你的场景是单一任务、单一模型、追求极致延迟,我建议先单独验证 P-EAGLE 或 DFlash,没必要一上来就上一个调度引擎。如果你的场景是不同请求混在一起,比如一个 API 网关同时面对代码补全和对话生成,那 DSpark 的系统级调度会更值得投入。说到底,并行草稿不是一个算法问题,而是一整套工程取舍问题,前期花一小时做数据分析和方案选型,能省下后面几天的调试时间。
6. 工程落地中的常见问题与排查技巧
6.1 草稿接受率异常低的排查思路
接受率掉到很离谱的水平时,先别急着怀疑模型实现,按下面这几项排查,大概率能找到问题。
采样参数是否一致是第一个要查的。temperature、top-p、top-k 任何一个不一致,草稿分布和目标模型验证分布都会偏差。建议把采样参数抽成全局配置,草稿阶段和验证阶段读同一份配置,避免复制错。
位置编码是否匹配是第二个容易踩坑的点。有些实现里草稿模型或草稿源拿到的上下文位置偏移和目标模型差了 1 位,尤其是并行草稿拼接验证的瞬间,RoPE 位置很容易对不上。这个问题非常隐蔽,因为只有验证位置的 token 会出现系统性低接受率,其他位置正常。
第三个是 OOV 或特殊 token 处理不一致。草稿阶段如果对部分 token 做了过滤,而验证阶段没有,两边分布会有系统性偏差。建议统一 tokenizer 和词表过滤逻辑,最好单独跑一次工具函数,把草稿分布和目标分布打印出来做对比,别全靠肉眼猜。
6.2 草稿文件/轻量模型带来的显存代价
投机采样的显存开销经常被低估。P-EAGLE 的轻量解码器参数量不大,但要让草稿阶段跑得快,还得考虑它自己的 KV cache;DFlash 的草稿文件如果到了几百 MB,也不是完全无感。在显存紧张的环境里,建议把草稿资源压缩到最小可用水平,或者直接做显存外的映射访问。
我踩过的具体坑是 DFlash 草稿文件默认加载到 GPU 显存,结果 7B 模型加上 512MB 草稿文件直接 OOM。后来改成冷热分片,把热点表放 GPU、冷数据放 CPU,OOM 问题就消失了,命中率只损失了几个百分点。这个做法值得一试。
6.3 测量加速比时的常见误区
另一个常见问题是加速比测不准。很多人直接对比开了投机采样前后的整体 wall-clock 时延,忽略了几件事:输入长度不同、输出长度不同、预热次数不足、GPU 频率波动。更靠谱的做法是固定输入输出长度,用相同的随机种子跑多轮,取中位数或多次平均值对比。
还要注意 batch 效应。投机采样在 batch size 很小的场景收益明显,随着 batch 增大,目标模型的前向计算越来越饱和,草稿阶段省下的比例会变小。我见过有人在 batch=1 时测出 2 倍加速,结果到生产环境 batch=32 时加速比掉到 1.2 倍,这不一定代表实现有问题,而是场景变了。建议在目标 batch 范围里做几组对比,别只看单点值。
6.4 动态草稿长度与批处理对齐的一个实用技巧
最后分享一个关于动态草稿长度的经验。投机采样在批处理场景里很容易被"最长草稿长度"拖累:一个请求想要的草稿长度是 8,另一个是 2,系统为了对齐往往让所有人跑 8,这纯属浪费。一个比较实用的做法是做"截止时间感知调度":每一轮验证前统计各序列当前的接受率,接受率低的序列提前截断,接受率高的序列继续加长,然后用一个最大公倍数或分组方式把同批次序列按草稿长度相近的分到一组。这样浪费的 padding 最少,验证矩阵也不算稀疏。
我实测过这个做法在混流场景下能多出 10% 左右的吞吐,虽然没有理论加速比那么惊艳,但在贵价 GPU 上这 10% 已经是实打实的成本节省。如果你正在做一个高吞吐推理服务,这个点值得优先优化,比盲目调大草稿长度更有效。