DeepInfer Part2 —— The adaptive verification of Dspark
DeepInfer Part2—— DSpark的动态验证调度
简单来说,DeepInfer的设计算法是 一个以GPU有效吞吐为目标的全局预算分配算法
核心的思考来说,就是可以参考这篇文章的一个观察:
Speculative Decoding: Performance or Illusion?](https://arxiv.org/abs/2601.11580)
想必看到这里的读者,其实都明白一个道理
所谓投机解码,就是一种利用GPU的多余算力,做一些额外的计算和工作,从而让核心的路径达到更高的计算速度的一种方式。Drafter做了很多额外的proposal tokens,其中只有相当一部分最终能被接受。
想必敏锐的读者到现在已经发现问题所在了。
投机解码的技术能够在单卡单请求上面得到不错的加速比例(一般是2~5x), 其根本原因是因为一般来说我们说Decode阶段是 Memory-bound的。而投机解码 激进的计算能够提高计算强度。从而达到加速。而现在我们的目光到了serving阶段,多个请求一般来说能够直接给我们的Compute打满。到了这种时候,我们再用投机解码,被拒绝掉的token就是真正的浪费掉了、本来可以用于加速的计算能力了。
因此,如何良好的去进行验证,提高acceptance,就是speculative decoding 在 serving utilization的一个关键点。
看到这里,想必你对动机有了一个清晰的理解。
1. 先从一轮Decode batch入手。
现在我们假设有32条亲求,每个token都已经有5个DSpark生成的draft tokens,那么我们就需要一次验证192个token,并且每个token的接受率并不相同。
给一个高接受率的token验证全部6 tokens, and it maybe still worth it !
但是对于一个不好的draft,预测第一个可能都没用。
1.1 数学细节
假设confidence head给出了三个drafter tokens的置信度,我们在预测一个token是否值得接受的时候,应该采用前缀积的方式,否则可能某处自己的概率很高,但是其前面很容易被拒绝。
1 | void update_draft_state( |
上述代码去除了错误处理等。可以看到核心的执行逻辑就是那个for
1.2 验证数的确定
假设我们已经准备好了累积概率。
DeepInfer会把所有的累积概率降序排序,选择最好的k个额外token。所以其实际的近似目标可以被考虑为是:
argmax ( B + k / T )
1 |
|
其中,estimated_forward_ms可以视作一个已有的函数,不用去细究里面的细节。
粗略的来讲,里面包含一条启动profiling得到的拟合曲线,从而根据已有的数据进行拟合,估计其具体时间。
1.3 怎么放到GPU上面?
我们刚刚得到了 启发式最佳的Q(本轮verification token数目)
但是在实际上,如何把他放进去去运行,也是一个很难的事情。
首先第一个困难,就是异步调度带来的数据依赖问题。
1 | 传统模式下: |
而Deepinfer希望做到的是,GPU在计算N轮的时候,CPU可以提前安排N +1 轮。
但此时N轮的Confidence 和 Acceptance还没有返回CPU。
所以,他们把决策拆开
- CPU: 选择全局总预算: 根据更早的历史Acceptance 估计Q
- GPU: 重新分配各个请求的验证长度,用GPU上面比较新的数据把Q分配个每个 小q
我们应该明晰一个设计: 总计算量允许我们根据历史预测去提前决定,但是实际的计算量分配应该尽可能地使用最新的信息
我们可先看GPU侧的代码
1 |
|
GPU不只是修正Seqlens_q ,因为这个地方有一个额外的细节,上一轮的acceptance也会影响KV 长度,所以在调度的时候需要注意这些信息的同步,不然很有可能导致 Attention Positions 和 KV cache Length的mismatch
2. Where gose Verification?
1 |
|
Confidence Head 用于调度,不会负责决定接受和拒绝,因此,预测的置信度不准确可能会影响性能,但是他不会因为某个token的confidence太低就直接判断其错误。
最后在验证结束之后,调用链会回到 LLMEngine
Decode step 提交以下内容
- SamplerOutput: 接受的Draft Tokens , 最终采样Token, 下一轮Draft,confidence
- Process_MTPInfo: 更新每个亲求的Draft 状态和概率估计
- Scheduler.process_outputs() 提交输出Token,更新Sequence 和 KV states
根据它们的性能证据,他们给出:
| 并发请求数 | 固定验证长度 | 动态平均验证长度 | 动态吞吐增益 |
|---|---|---|---|
| 16 | 5.00 | 4.44 | -1.5% |
| 32 | 5.00 | 3.65 | +1.6% |
| 60 | 5.00 | 3.58 | +5.4% |
| 96 | 5.00 | 3.24 | +11.0% |
这些数据给我们以下的观察
- 动态验证长度不是无条件有效。
- 越接近高并发的地方,减少低价值Verification通常就越有意义。
- 这个优化的收益没有2x \ 3x 那么夸张。










