课程 14 · 投机与确定性
投机解码的老问题是「猜多了浪费、猜少了没收益」。DeepInfer 的 DSpark 用一个小型置信度头预测每个 draft token 的接受概率,再据此决定验证多长——同时还要保证结果与 batch 组成无关。
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没有意义”跳过分布正确性检查。
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 一起交给调度器。
普通投机解码是「固定猜 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的固定成本收益只适合独立的玩具练习。
这里的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说明。
这是 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-invariant | DEEPINFER_BATCH_INVARIANT=1 | 只能用特定 kernel 路径(NVIDIA only),可能略慢 |
| 确定性采样 | 上述 + MTP_VARLEN=0 + 固定 seq_seed | 失去可变长投机带来的加速 |
| 性能调优 | 固定负载对比配置 | 默认配置不保证吞吐最优 |
该提交README明确给出NVIDIA上的确定性配置:batch-invariant路径、关闭MTP_VARLEN、固定seq_seed。它是一组可核对的前提,不是跨硬件、模型与版本无条件相同的承诺。
假设验证一个 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课。
「为什么推理引擎要做 batch invariance?」
「batch组成可改变归约与随机数消费。DeepInfer为支持的NVIDIA路径提供batch-invariant配置,再关闭variable-length verification并固定每请求seq_seed来满足其确定性采样条件。是否值得采用以及性能代价应按用途与负载验证。」
到这儿,整条主线走完了。 你可以回去任选一课重读,或者告诉我:哪一块你想做成能跑的实验?我可以据此加一节实战课(比如在 4070 Ti 上复现投机解码的接受率曲线)。