推测解码(Speculative Decoding)是目前比较热门的一个LLM加速方式,今天这篇文章是FlashSpec 的作者讲解如何构建一个自适应 LLM 推理引擎
推测解码可以在不改变最终输出分布的前提下加快大语言模型推理。较小的草稿模型(draft model)会一次快速提出几个 Token,更大的目标模型(target model)随后用一次前向传播完成验证。通过接受测试的 Token 直接保留;没有通过的 Token,则改用目标模型在对应位置给出的输出。
关键约束没有变化:最终输出分布必须与直接从目标模型采样时完全一致。
FlashSpec 在此基础上增加了两项改动。
1、GPU 原生验证
常见实现会把接受/拒绝决策放到 CPU,每个解码步骤都要在 GPU 和 CPU 之间同步数据,开销也随之出现。
FlashSpec 把完整验证步骤留在 GPU 上,使用自定义 Triton kernel 执行。每个候选 Token 只需读取两个标量值,也就是两项对数概率;即使词表扩大到 32k 或 128k 个 Token,内存使用量仍是常数。
2、在线 Bandit 草稿选择
草稿模型并非选定一个后就始终不变。FlashSpec 把模型选择建模为多臂老虎机(multi-armed bandit)问题,在推理过程中用 UCB1 或 Thompson sampling 动态决定采用哪个草稿模型。
理论结果给出的目标是:在有界遗憾(bounded regret)的条件下,让系统自行找到当前工作负载最合适的草稿模型。
下面是作者构建FlashSpec 时总结的经验
经验 1:提前写规范
草稿 Token 被拒绝时,精确的数学公式是什么?Triton kernel 与纯 PyTorch 实现相比应满足怎样的数值容差?CI 里的分布等价性测试应该使用多大的样本量?这些问题很容易到 bug 出现后再处理。所以把要求预先写进规范,很多偏差就能在更早阶段暴露。
Kolmogorov-Smirnov(KS)检验就是一个直接的例子。它负责验证 FlashSpec 的输出分布是否与目标模型一致,规范要求样本量为 10,000,但最初的代码只跑了 1,000 个样本。
低样本量下测试依然通过,问题并没有显示出来。1,000 个样本的 KS 检验统计功效远低于 10,000 个样本,一些本应捕获的细微分布差异可能直接漏掉。
修复:把样本量提高到 10,000,并把 KS 检验设为 CI 的硬性要求。测试失败,构建就失败。
经验:没有自动检查机制的规范,迟早会在无人察觉时被违反。
经验 2:Temperature bug 在默认设置下完全不会露馅
这是整个项目里最有教育意义的一个 bug。
使用温度缩放(temperature scaling)时,数学规则很明确:原始 logits 必须先除以温度,再应用 log_softmax,两种操作顺序并不等价。
最初的 rejection_sample() 接收 temperature 参数,但输出完全不受它影响。原因在更早的位置:传入的对数概率已经由 score_draft() 算好,而 score_draft() 直接对原始 logits 执行 log_softmax,根本没有温度缩放。参数写进了函数签名和文档,也沿着调用链正常传递,实际计算却没有使用它。
所有测试都采用默认值 temperature = 1.0,所以 bug 一直看不出来。温度为 1.0 时,除以 1.0 和不除没有差别。
修复不能只改一行,架构也要调整:
# 修复前(temperature 没有任何效果)
defscore_draft(self, input_ids, draft_token_ids, gamma):
logits=self._model(...).logits[..., -gamma:, :]
returntorch.log_softmax(logits.float(), dim=-1)
# 修复后(在 log_softmax 之前应用 temperature)
defscore_draft(self, input_ids, draft_token_ids, gamma, temperature=1.0):
logits=self._model(...).logits[..., -gamma:, :]
iftemperature!=1.0:
logits=logits/temperature # ← 在这里应用
returntorch.log_softmax(logits.float(), dim=-1)一共改了三个文件。temperature 也从 rejection_sample() 中移除,因为那里本来就不是它该出现的位置。
经验:机器学习里的错误实现经常能产出“看起来没问题”的结果,默认参数尤其容易掩盖问题。数学不变量的测试必须覆盖非默认值。
经验 3:连续发布三个版本后,包才真正能在 Windows 上安装
0.1.0 发布后,测试者马上遇到:
ERROR: Could not find a version that satisfies the requirement triton>=3.0.0Triton 只提供 Linux 官方 wheel,没有 Windows 或 macOS 官方 wheel。可 pyproject.toml 当时把 triton>=3.0.0 写成必需依赖,于是从第一个版本起,所有非 Linux 系统都无法安装这个包。
0.1.0、0.1.1、0.1.2 在 Windows 和 macOS 上全部不可用,目前三个版本都已从 PyPI 撤下。
完整修复涉及三个位置:
把 Triton 移到可选的
gpuextra,并加上平台标记增加可平稳回退的代码和清晰的错误信息
Triton 不可用时,引导用户改用纯 PyTorch 参考实现
现在,Windows、macOS 和 Linux 都可以正常安装。
经验:第一次公开发布前,应当在干净的 Windows 环境里跑一次 pip install your-package。五分钟足以排掉一整类平台问题。
经验 4:Triton kernel 反而比 PyTorch 慢
这个结果很让人失望,但必须如实写下来。
Tesla T4(Google Colab)上,batch size 为 1 时,自定义 Triton 验证 kernel 明显慢于纯 PyTorch 参考实现,而 batch size 为 1 恰好又是单用户推理最常见的情况。
问题与硬件特性有关。验证 kernel 受内存带宽限制,T4 的带宽相较 H100 这类更新的 GPU 较低;在 T4 上,PyTorch 已高度优化的参考实现仍有很强竞争力。换到内存带宽更高的硬件后,Triton kernel 更小的内存占用应该更容易体现优势。
README 和 JOSS 论文都记录了这组结果,并明确写出测试硬件。FlashSpec 的性能结论以更高端 GPU 为前提,H100 benchmark 仍在进行。
经验:自定义 kernel 不等于自动提速。性能高度依赖硬件,benchmark 应放在目标硬件上跑,测试平台也要写清楚。
经验 5:基于属性的测试不到一分钟就抓到真实 bug
Hypothesis 是一个基于属性的测试(property-based testing)库。把它加入测试套件后,很快就出现了一个此前没有覆盖到的问题。
手写测试一直使用 gamma=4 和 batch_size=2,因为开发时这两组 shape 最顺手。Hypothesis 很早就生成了 gamma=1, batch_size=1,随即触发数组越界错误。这个 shape 完全合法,只是此前从未想到要测。
现在,每次 CI 构建都会运行基于属性的测试,覆盖完整的有效输入范围,而不再局限于人工挑出来的少数 shape。
经验:整数参数只要会影响 shape 或索引,就值得至少加一个基于属性的测试。投入很低,抓到的问题往往是真 bug。
经验 6:自适应算法要遇到真实差异才有价值
bandit 的理论结果不错。受控实验中,UCB1 和 Thompson sampling 都保持在预期的 regret bound 内。
第一次把系统接到真实模型上时,环境却没有给 bandit 足够的选择空间:T4 上运行 TinyLlama,当时只有一个草稿模型可用。算法本身执行正确,却没有任何值得适应的变化。
bandit 的实际价值依赖多个优势不同的草稿模型,例如一个体积小、速度快的 drafter,以及一个更大、更准确的 drafter。环境里没有真实差异时,自适应选择只会增加额外开销。
经验:自适应组件成立的前提,是环境里确实存在值得适应的变化。单独验证算法是必要的,但并不充分。
项目当前状态
目前已经可以工作的部分:
可以通过
pip install flashspec在 Windows、macOS 和 Linux 上正常安装可选的
gpuextra 会在 Linux + CUDA 上添加 Triton kernelsCI 强制执行输出分布保证(10,000 个样本的 KS 检验)
UCB1 和 Thompson sampling 满足各自的理论 regret bound
第一个真实测得的性能:在 T4 上运行 TinyLlama-1.1B(4-bit)达到 44.2 tokens/second
JOSS 论文已经提交
仍在进行的工作:
使用 Llama-3–8B 和 Llama-3–70B 的完整 H100 benchmark(README 里当前的 headline numbers 是设计目标)
为可读性进行少量代码和 lint 修复
核心正确性、打包和分布保证已经稳定。主要剩余工作是到目标硬件上验证性能。
如果重来一次
CI pipeline 应该写在实现之前,而不是与实现并行推进。
CI pipeline 是规范真正落地的执行机制。先写代码、后补测试,中间就会出现一段重要不变量无人验证的时期。这个项目里有几个问题能被发现,靠的是现成规范可以用来对照;如果 CI gate 从第一次 commit 起就运行,它们会更早暴露。
正确顺序是:Specification → CI → Implementation。
如果你从事 LLM 推理系统相关工作,并且对 kernel 设计或 bandit formulation 有想法,我很希望收到反馈。
试用 FlashSpec:
pip install flashspec # Windows、macOS、Linux
pip install flashspec[gpu] # Linux + CUDA(Triton kernels)GitHub: github.com/Mattral/FlashSpec
by Min Htet Myet
本文讨论的是LLM推挤的加速部分 。如果你希望把模型、工具、状态、权限、执行环境和插件生命周期放在同一张架构图里理解,可以继续阅读我的电子书:
《DeepSeek Harness 技术入门与架构原理:从第一个插件到 Agent Runtime》
这是 DeepSeek Harness 开源后首批系统性中文技术电子书之一。全书按照“先运行、再拆解、最后自己设计”的路线,主要包括:
四套 Agent Preset 的能力差异;
Skill、Tool、Hook 与完整 Tool Pipeline;
Guard、Approval、Sandbox 的不同边界;
Cordis 的插件生命周期与依赖管理;
Agent Loop、Session Event、Code Mode 与 Subagent;
如何设计和验证自己的 Harness。
本书适合 AI 应用工程师、Agent 开发者,以及希望从“模型调用”继续深入到 Agent Runtime 的技术读者。