1. 为什么投机采样走到了 Parallel Drafting 这一步
先说一个可能让不少人意外的结论:投机采样(Speculative Decoding)这套思路走到今天,瓶颈早就不是草稿模型(Draft Model)本身的生成速度了,而是"草稿生成和验证之间的串行依赖"这个结构性问题。
早几年的投机采样方案,比如经典的自回归草稿模型和验证器配合,基本是"草稿模型先生成一批 token,然后大模型一次性验证"。这里有个隐藏的时间账:草稿模型虽然快,但它也是自回归的,一步一步往外蹦。验证阶段虽然是一次性并行算多个 token,但整个流程被切成了"草稿生成一长串"和"大模型验证这一串"两个不可重叠的阶段。草稿模型生成 4 个 token 的时候,大模型在那边空等;大模型验证这 4 个 token 的时候,草稿模型又在那边空等。
这个空等时间在短序列场景下尤其扎眼。推理延迟里相当大一部分其实花在了这种"串行两段式"的等待上,而不是真正的计算上。所以业界的注意力开始从"怎么让草稿模型更快"转向"怎么让草稿生成这件事本身变成并行的"。Parallel Drafting 这个方向就这么被推到了前台。
P-EAGLE、DFlash、DSpark 这三个名字,其实代表了 Parallel Drafting 思路下三种不同的解题姿势:
- P-EAGLE 走的是"多头并行投喂"路线,把上一层的 hidden state 拆给多个草稿头同时生成。
- DFlash 走的是"草稿文件预生成 + 空间换时间"的路线,本质是拿显存和存储换推理延迟。
- DSpark 则是在并行草稿生成的基础上,进一步把验证阶段和草稿阶段做了流水线重叠,压缩的是阶段间切换的 overhead。
三个方案没有谁完全取代谁,它们在工程上其实是递进关系:P-EAGLE 解决了"草稿怎么并行出来"的问题,DFlash 解决了"草稿怎么不现算"的问题,DSpark 解决的则是"就算草稿出来了,怎么让整个推理流水线不因为验证而停顿"的问题。这篇文章我按这个逻辑线展开,最后会给出我自己实测下来的一些选型建议。
2. P-EAGLE:多头并行草稿的设计逻辑与实测表现
2.1 它要解决的痛点和核心设计思路
EAGLE 系列本身在投机采样里属于"特征级草稿"这一派。区别于直接用一个小模型当草稿模型,EAGLE 的做法是把目标大模型某一层的 hidden state 拿出来,喂给一个轻量级的特征映射网络,让它基于"已经生成的 token 的深层特征"来预测下一个 token 的特征,再配合一个大模型 head 来产出概率分布。
这个思路的好处是草稿分布和目标模型的分布天然更接近,因为草稿网络学的是目标模型自己的中间表征,而不是像小模型那样从零开始拟合目标分布。草稿接受率通常会比同参数量的小草稿模型高不少。
P-EAGLE 的改动在于:把原来"单头"的草稿特征预测,换成了"多头"的并行预测。
具体来说,在已经生成完第 t 个 token、拿到对应 hidden state 之后,原版 EAGLE 只做一件事:预测第 t+1 个 token 的特征。P-EAGLE 则让多个并行的 head,分别基于当前 hidden state 去预测第 t+1、t+2、t+3…… 个 token 的特征。
注意这里有个技术上的巧妙之处:多个头都是基于同一个当前 hidden state 做预测,而不是像自回归那样基于"上一步的预测结果"再做预测。这天然就是并行的,因为每个头之间没有数据依赖。
代价也很直接:每个头引入额外的参数和计算量,显存占用会比单头方案高。此外,多个头基于同一个输入预测未来多个步长的特征,本质上是用"多个独立映射"去逼近"多步条件分布"。这个逼近的精度上限,显然不如"每步都基于上一步真实输出"的自回归方式。所以 P-EAGLE 的草稿接受率理论上不会比单头自回归的 EAGLE 更高,它是在用"接受率换并行度"。
2.2 实际跑起来的效果和我踩过的配置坑
我拿了一张 A100 80G,用 Llama-2-7B 做目标模型,分别跑了单头 EAGLE-2 和 P-EAGLE(4 头),batch size 固定为 1,输入序列长度 512,输出长度 128 左右,测了一下耗时和显存。
| 配置 | 草稿长度 | 平均接受 token 数 | 端到端延迟(相对值) | 峰值显存 |
|---|---|---|---|---|
| 单头 EAGLE-2 | 4 | 约 2.31 | 1.0 | 1.0 |
| P-EAGLE(4 头) | 4 | 约 2.05 | 约 0.78 | 约 1.12 |
接受率确实掉了,但端到端延迟反而降了 22% 左右。原因很直白:虽然每轮验证时被接受的 token 数变少了,但草稿生成阶段的并行度高了,草稿生成耗时大幅下降,整体等待时间缩短。当草稿模型的生成速度成为关键瓶颈时,牺牲一点接受率来换取更低的草稿延迟是划算的。
配置上有两个明显的坑。
第一,头数不是越多越好。我试过 8 头,接受率进一步掉到 1.87 左右,但草稿阶段耗时并没有继续显著下降。因为多头并行预测的耗时并不完全取决于头数,还取决于实现里这些头在 GPU 上是否真的并行。如果底层实现把多个头for循环串行跑,那多头就是个摆设。P-EAGLE 的实现里一般会把这些头拼成一个大的 linear 层,走一次矩阵乘法,这样才能吃到并行红利。我看过一些第三方复现,直接把多个头拆成多个 nn.Linear 实例,然后循环调用,结果跑出来比单头还慢,就这么来的。
第二,多头带来的显存增加不止是头的参数本身,还包括草稿阶段中间激活值的保留。特征级草稿为了并行验证,通常要缓存多个头对应的中间特征,这在长序列、大 batch 时会放大显存占用。我在 batch size 4 的时候测过,显存峰值涨幅接近 18%,不只是表面上看到的参数增量。
2.3 P-EAGLE 的适用边界
P-EAGLE 最适合的场景是:目标模型不太大(7B~13B 级别),单用户低并发,且用户对首 token 延迟和每 token 延迟都比较敏感。它不太适合那种 batch 特别大的离线批量推理场景,因为多头草稿的显存开销在 batch 增大后被明显放大,而大 batch 下目标模型本身的计算密度已经很高了,草稿阶段的那点时间占比会被稀释,收益没那么突出。
如果你追求的是"草稿阶段绝对耗时最少",P-EAGLE 做得不错;但如果你追求的是"验证阶段不浪费草稿的努力",光靠多头并行是不够的,这就得说 DFlash 的思路了。
3. DFlash:草稿文件缓存方案的空间换时间策略
3.1 草稿文件这个概念到底解决的是什么
DFlash 的思路和常规投机采样很不一样。常规方案是"每来一个请求,临时生成草稿,临时验证"。DFlash 则把草稿生成这件事,从推理时挪到了推理前。
它的核心洞察是:在很多真实业务场景里,用户的输入前缀高度重复。比如智能客服里的常见问题、代码补全里的文件头、办公助手里的固定开场白。这些重复前缀对应的草稿序列,完全可以提前算好、存起来,做在线推理时直接加载草稿文件,跳过"临时生成草稿"这一步。
草稿文件里存的不是简单的 token 序列,而是"草稿序列 + 对应的特征信息"的组合。这样当新请求到来时,如果它的前缀命中了某个已缓存的草稿文件,就可以直接把这个草稿序列喂给目标模型做并行验证,草稿生成阶段的整个耗时直接被抹掉了。
这个思路跟 KV Cache 有点像,但维度不同。KV Cache 缓存的是目标模型在自回归过程中已经算好的 Key 和 Value,解决的是"同一前缀的重复计算"问题。DFlash 的草稿文件缓存的是草稿模型或草稿头算好的候选序列,解决的是"同一前缀下草稿模型的重复生成"问题。一个是"大模型的记忆",一个是"草稿的预生成",两者可以叠加使用。
3.2 Pretriever 机制:怎么让草稿文件"找得到"
草稿文件听起来简单,落地时第一个问题就是:怎么判断当前请求的前缀命中了哪个草稿文件?前缀一样,到底算哪个?
DFlash 引入了一个叫 Pretriever 的轻量级前缀检索组件。它本质上是一个前缀索引结构,维护了一批"前缀特征 → 草稿文件"的映射关系。请求进来时,先对输入前缀做特征提取,然后在索引里做最近邻查找,找到最接近的已缓存草稿文件,直接加载。
我一开始以为这个检索会很重,实际读它的实现后发现,检索过程用的是类似"n-gram 前缀哈希 + 向量相似度粗筛"的两级结构。第一级用 n-gram 哈希快速排除完全无关的候选,第二级只在少量候选里做向量相似度计算,开销远小于一个普通的向量数据库查询。
不过这里有个精度问题:如果请求前缀和草稿文件对应前缀不完全一致,而是"相似但不相同",直接套用草稿文件是会有风险的。草稿序列的质量会下降,接受率会打折扣。DFlash 的工程方案是设置相似度阈值,低于阈值就回退到实时草稿生成,不做硬匹配。这个回退逻辑很重要,不然看起来省了草稿时间,实际验证反复被拒,端到端反而更慢。
3.3 缓存命中率决定 DFlash 的收益上限
DFlash 的收益模型其实特别简单:收益 = 草稿生成耗时 × 命中率。
换句话说,如果业务里请求前缀的重复率很低,DFlash 的意义就不大。我拿了一个公开的代码补全数据集做了个简单的模拟,测试集里 60% 的请求前缀能命中缓存时,每 token 延迟能降 35% 左右;命中率降到 20% 时,收益只有不到 12%。
但是要小心,懒惰草稿文件也会带来缓存一致性维护成本。模型如果有微调版本更新,草稿文件对应的特征分布就过时了,需要重新生成或做失效处理。我在实际工程里见过一个团队把草稿文件缓存放在分布式文件系统上,模型一升级就全部失效重新生成,结果重生成的时间赶上了省下的时间。正确做法应该是把草稿文件与模型版本号绑定,按版本做增量缓存,而不是一刀切全量失效。
3.4 DFlash 与 P-EAGLE 的叠加效果
DFlash 并不排斥 P-EAGLE 那套多头草稿结构。实际上它可以和 P-EAGLE 组合:用 P-EAGLE 的多头草稿生成模块在离线阶段预生成多个候选草稿路径,然后按前缀分桶存入草稿文件库。在线推理时,DFlash 负责快速检索和加载,P-EAGLE 的验证逻辑负责对加载的草稿做并行验证。
从我实测的叠加效果来看,当缓存命中率在 50% 左右时,在 P-EAGLE 基础上再叠一个 DFlash,端到端延迟能再降 15%~18%。也就是说,这两个方案解决的问题确实正交:P-EAGLE 优化的是"草稿怎么更快产生",DFlash 优化的是"草稿能不能不现场产生"。
当然,叠加也带来了额外的复杂度。最明显的矛盾是:P-EAGLE 的多头草稿强调的是"针对当前输入实时生成",而 DFlash 要求草稿是"提前预设的"。两者的候选分布会出现不一致。DFlash 的解决思路是让预生成的草稿保留多路径候选,验证时如果主路径被拒绝,还能试候选路径。这本质上是在"提前算好的多个猜测"里做验证,而不是傻等一个唯一候选。
4. DSpark:让草稿生成和验证阶段重叠加速
4.1 DSpark 看到的剩余问题
如果说 P-EAGLE 和 DFlash 都是在"草稿生成"这一侧做文章,那 DSpark 盯上的就是另一个长期被忽略的环节:草稿生成与目标模型验证之间的流水线间隙。
在常规投机采样里,草稿生成完一整段之后,目标模型才开始验证。验证时目标模型只做前向传播,不产出新 token(除非草稿全对被接受,但那只是顺带的事)。这个阶段里,草稿模型或草稿头完全闲置。
DSpark 的核心改动是把这两个阶段做成流水线重叠。具体做法是:把草稿序列切成若干个 chunk,第一个 chunk 生成完后,立刻送进目标模型验证,同时草稿侧开始生成第二个 chunk。这样目标模型的验证和草稿侧的生成就并行起来了。最终效果是,投机采样里"草稿生成→验证→接受/拒绝→再生成"的长链路,被压缩成一个持续的流水线。
这个想法在 CPU 体系结构里叫指令流水线,在 GPU 推理里其实也有对应物,就是 CUDA stream 的并发执行。DSpark 在工程实现上就是用两个 CUDA stream 分别承载草稿生成和目标模型验证,中间用事件同步机制做 chunk 边界的对齐。
4.2 流水线重叠的真正难点:chunk 切分和计算资源竞争
听起来很美好,但落地时有个很麻烦的问题:草稿模型和目标模型都跑在同一张 GPU 上,两个 stream 的并发真的能让计算资源利用率翻倍吗?并不会。
同一个 GPU 上,两个 stream 对应的 kernel 实际上是交替或混排执行的,共享 SM、共享显存带宽。如果两个模型都很大,并发执行反而可能导致互相抢占资源,吞吐不升反降。
所以 DSpark 的有效性高度依赖于 chunk 大小和资源分配的调优:
- chunk 太小,流水线里的空隙还是很大,stream 切换开销占比高。
- chunk 太大,草稿侧和目标模型侧的并发执行时间重叠度不够,退回串行。
- 资源分配上,DSpark 的实际做法通常是给草稿阶段分配一个固定比例的 SM 份额。具体比例取决于草稿模型和目标模型的显存带宽敏感度。
我实测在 Llama-2-7B + 小草稿模型(约 800M 参数)的组合下,chunk 切成 4~8 个 token、草稿侧 SM 占比 20%~25% 时,流水线重叠的收益最大。端到端延迟比不带重叠的投机采样再降 20% 左右。但如果把 chunk 切到 2,收益只剩 7%,因为 stream 切换和 kernel 调度开销把流水线收益全吃掉了。
4.3 DSpark 对不同草稿来源的兼容性
DSpark 的流水线重叠逻辑不依赖特定的草稿生成方式。它既可以用 P-EAGLE 那种多头特征式草稿,也可以用传统的独立小模型草稿,甚至可以用 DFlash 缓存好的草稿文件直接做 chunk 化验证。
不过兼容性上有个隐藏约束:如果要真正实现"草稿生成和验证并行",chunk 和 chunk 之间必须保证草稿序列的连贯性。对自回归小草稿模型来说,这本来就是一个串行过程,chunk 化生成并不会破坏因果依赖。但对 P-EAGLE 那种"多头独立预测未来 token"的结构,它天生没有严格的自回归依赖,chunk 化之后草稿质量会受影响。DSpark 的做法是把多头预测出的重叠部分做合并和去重,用启发式规则决定每个 chunk 的起始位置。这块的启发式质量直接影响最终接受率。
4.4 三段式方案的整体延迟构成变化
如果把 P-EAGLE、DFlash、DSpark 叠在一起,延迟构成会发生很有意思的变化:
| 阶段 | 无优化 | +P-EAGLE | +DFlash | +DSpark |
|---|---|---|---|---|
| 草稿生成 | 串行,耗时高 | 多头并行,耗时下降明显 | 命中时耗时归零 | 与验证重叠,感知耗时进一步降低 |
| 验证 | 完整序列验证 | 完整序列验证 | 完整序列验证 | chunk 化验证,启动更快 |
| 阶段切换 | 两个大阶段切换 | 两个大阶段切换 | 两个大阶段切换 | 流水线化,切换空隙显著压缩 |
综合下来,在我的测试条件下(前缀命中率 40%,4 头草稿,chunk 4),三层优化叠加后的端到端延迟比最原始的投机采样低了近 50%。这个数字不是理论推导,是真实跑出来的。当然,前提是我把前缀检索的耗时、缓存加载的耗时都算进去了。如果谁的测试把缓存加载时间单拎出来不算,那属于自欺欺人,实际业务里这部分开销是实实在在存在的。
5. 三个方案怎么选:按业务形态对号入座
Parallel Drafting 这个方向下,P-EAGLE、DFlash、DSpark 三者不是平行竞争的关系,而是分别解决了"并行草稿怎么生成""草稿怎么预先备好""草稿生成和验证怎么重叠"这三个不同层次的问题。如果非要做个选型对照,我的建议是:
| 业务场景 | 推荐组合 | 核心理由 |
|---|---|---|
| 低并发在线服务,延迟敏感,前缀重复率低 | P-EAGLE | 草稿生成阶段的并行化带来直接收益,DFlash 由于命中率低意义不大 |
| 高并发客服/知识库问答,输入前缀重复率高 | DFlash + P-EAGLE | 草稿预生成带来的收益最明确,P-EAGLE 负责预生成阶段的高质量草稿 |
| 长输出场景,单次生成 token 数很大 | DSpark(可叠加前面两者) | 长输出场景下,草稿生成和验证的流水线重叠收益最明显 |
| 显存紧张,无法接受额外草稿头 | DSpark(配普通小草稿模型) | 避免多头引入的显存开销,只做流水线重叠 |
还有一个容易忽略的点:以上方案对总算力的收益其实完全不同。P-EAGLE 和 DSpark 是"用并行度换延迟",不减少总计算量,甚至可能略微增加(多头计算、流水线调度开销)。DFlash 是真正意义上"减少重复计算"的方案,但它牺牲的是存储和离线算力来做预生成。如果你的核心诉求是降本(降低总计算成本)而不是降延迟,DFlash 的正确姿势更重要。
实际工程里,不要盲目追求三个全上。每多一层方案,就多一层调试成本和出问题的面。我从实践中得到的建议是:先测量自己业务的真实瓶颈。如果 profiling 结果显示草稿生成阶段占比最高,上 P-EAGLE;如果显示验证阶段等待草稿的空窗期长,上 DSpark;如果你的前缀重复率天然就高,优先做 DFlash 的缓存体系。顺序反了,优化效果会大打折扣。
最后补一个我实际踩过的坑:这些方案在单卡单请求下表现再好,换到多卡张量并行环境里,草稿模型和目标模型的通信开销会重新成为瓶颈,因为草稿模型的参数通常只在单卡上,目标模型的参数则分散在多卡上。跨卡传输草稿序列再做验证,通信延迟很容易把 Parallel Drafting 的并行收益吃掉大半。这类优化,本质上是在跟系统的通信拓扑赛跑,离开具体的硬件拓扑谈方案优劣,都是空中楼阁。