课程 14 · 投机与确定性

猜五个,验证几个?

投机解码的老问题是「猜多了浪费、猜少了没收益」。DeepInfer 的 DSpark 用一个小型置信度头预测每个 draft token 的接受概率,再据此决定验证多长——同时还要保证结果与 batch 组成无关。

约 55 分钟 源码:model_executor/models/mtp/dflash.py · environ.py 前置:11、13

2026-11-08 源码校正

本页已按本地固定commit修正相关描述。三框架对照 · 带行号的离线证据 · 核查与勘误详情。

1DFlash:一次猜 5 个

DeepInfer 的投机解码来自模型自带的 MTP(Multi-Token Prediction)层:每一层预测「再下一个 token」。默认最多一次猜 5 个:

environ.py · L136DEEPINFER_USE_NEXTN_DECODE = EnvVar.int(default=5)
dflash.py · L82–110class DFlash(nn.Module):
    ...
    self.max_MTP_value = envs.DEEPINFER_USE_NEXTN_DECODE   # 每个 seq 生成的 propose 个数
    self.logprobs_dim = self.vocab_size if self.force_topk == 0 else self.force_topk

force_topk改变草稿候选logprobs的存储和支持集合,需与下游验证/残差采样一起理解;不能用“低概率token没有意义”跳过分布正确性检查。

2置信度头:预测「这个 draft 会被接受吗」

dflash.py · L46–79class ConfidenceHead(nn.Module):
    """Predict the acceptance probability of each draft token."""
    def __init__(self, n_embed, markov_rank, num_steps, weight):
        self.proj = ReplicatedLinear(n_embed + markov_rank, 1, weight=weight)
        temperature = envs.DEEPINFER_DFLASH_CONFIDENCE_HEAD_TEMPERATURE
        ...

它是一个极小的线性层:输入是隐藏状态(n_embed)加上 markov 嵌入(markov_rank),输出一个标量——该位置 draft token 的接受概率估计。

dflash.py · L302–319confidence_rates = (... if self.enable_confidence else None)
...
if confidence_rates is not None:
    self.confidence_head(hidden, markov_emb, out=confidence_rates[:, step_i], step_i=step_i)

每个 draft 位置一个置信度,存进 MTPPredictInfo(token_ids, confidence_rates),和 token 一起交给调度器。

3为什么需要置信度

普通投机解码是「固定猜 k 个,全部验证」:

置信度让验证长度变成可变的:

置信度:  draft1 0.98   draft2 0.95   draft3 0.41   draft4 0.12
累乘:    0.98 → 0.931 → 0.38171 → 0.0458052
决策:    还需其它请求的候选与graph耗时曲线,不能只凭本行确定长度

环境变量 DEEPINFER_ENABLE_MTP_VARLEN=1 打开可变长度;DEEPINFER_MAX_BACKTRACKING_TOKENS(默认 ≥ 6)限制最坏情况下要回退多少。

核心思想

真实实现先累乘前缀接受率,再跨请求排序,结合graph-size的预估耗时选择有效token/耗时最佳的总query预算。逐token的固定成本收益只适合独立的玩具练习。

4SWA 与「有界重放」

这里的bounded replay针对YOCO consumer层的SWA状态重建,目标是减少decoder prefill工作;不能概括成模型所有层都只保留同一个窗口。部署QA明确称它为近似重建。

compact模式移除早期query,只重放有界后缀;mask模式保留query但限制可见域。保留长度至少覆盖SWA window + max_backtracking。下面是README的启用示例;默认模式off,keep默认0。

DEEPINFER_YOCO_PREFILL_TRUNCATION_MODE=compact
DEEPINFER_YOCO_PREFILL_KEEP_TOKENS=140      # README推荐的V4.1启用示例

这个配置能减少重放工作,并保留回退所需窗口;但不保证与全量prefill逐位或严格数学等价。见部署QA的approximate说明。

5Batch invariance:同样的输入,同样的输出

这是 DeepInfer 最反直觉的特性。浮点运算的归约顺序可能改变结果,尤其是浮点加法不满足结合律:

(a + b) + c  ≠  a + (b + c)      # 浮点加法不满足结合律

同一请求在不同batch下可能因kernel归约和随机流变化得到不同结果。确定性有利于回归、调试与需要复现轨迹的训练流程,但少量浮点差异并不自动意味着梯度错误;是否要求bitwise一致需结合算法目标。

DEEPINFER_BATCH_INVARIANT=1        # 不同 batch 组成下结果完全一致(仅 NVIDIA)
DEEPINFER_ENABLE_MTP_VARLEN=0      # 确定性的前提下关闭可变长验证
+ 传给 SamplingParams 一个固定的 seq_seed

三者组合,才能得到 README 承诺的「deterministic sampling」:相同输入 token + 相同采样参数 → 相同输出。

目标需要打开代价
batch-invariantDEEPINFER_BATCH_INVARIANT=1只能用特定 kernel 路径(NVIDIA only),可能略慢
确定性采样上述 + MTP_VARLEN=0 + 固定 seq_seed失去可变长投机带来的加速
性能调优固定负载对比配置默认配置不保证吞吐最优
为什么不能「全都要」

该提交README明确给出NVIDIA上的确定性配置:batch-invariant路径、关闭MTP_VARLEN、固定seq_seed。它是一组可核对的前提,不是跨硬件、模型与版本无条件相同的承诺。

6练习 A · 算一次验证的期望收益

假设验证一个 draft token 的边际成本是 1 单位,接受它能省下 3 单位(不用单独跑一步 decode)。四个 draft 的接受概率分别是 0.9、0.6、0.3、0.1。应该验证到第几个?

展开:答案

验证第 i 个的期望净收益 ≈ p_i × 3 − 1:

① 0.9×3−1 = +1.7;② 0.6×3−1 = +0.8;③ 0.3×3−1 = −0.1;④ 0.1×3−1 = −0.7。

以上只是假设独立边际收益的玩具模型,不能推出真实DSpark阈值。真实实现对confidence累乘,结合跨请求排序与graph耗时曲线选择预计有效token/耗时最大的总预算,再分配各请求长度;详见32课。

7练习 B · 闭卷测验

8面试复述卡

「为什么推理引擎要做 batch invariance?」

展开:30 秒版本

「batch组成可改变归约与随机数消费。DeepInfer为支持的NVIDIA路径提供batch-invariant配置,再关闭variable-length verification并固定每请求seq_seed来满足其确定性采样条件。是否值得采用以及性能代价应按用途与负载验证。」

到这儿,整条主线走完了。 你可以回去任选一课重读,或者告诉我:哪一块你想做成能跑的实验?我可以据此加一节实战课(比如在 4070 Ti 上复现投机解码的接受率曲线)。