实测记录:Qwen3.8-27B 本地推理从 14 tok/s 干到 159 tok/s
先说个结论:这个速度提升是实打实的,不是虚标。如果你手里也有一块 24G 显存的显卡,正在为本地跑 27B 模型卡成 PPT 发愁,这篇文章就是给你看的。
事情是这样的。我最近在做本地大模型推理的落地实验,目标很明确:在不依赖任何云端服务的前提下,把 27B 级别的开源模型在单机单卡上跑出可用的交互速度。开始的时候结果非常打击人——模型部署好后,推理速度只有 14 tok/s。这个数字什么意思呢?就是每秒钟生成不到 15 个 token,用户在对话窗口敲一句话,等回复要等十几秒到半分钟,完全达不到“可用”的门槛。
后来我换了个思路,把工作重点从“压缩模型”转移到“改造推理过程”,最终把稳定速度拉升到了 159 tok/s,峰值甚至能摸到 170 tok/s 以上。整个过程没有换显卡、没有蒸馏模型、没有降低量化精度,核心做对了一件事:给推理过程配上了一个足够聪明的“草稿员”——这就是投机解码(Speculative Decoding)调优。
这篇文章不打算写成一个泛泛的配置步骤列表,我会把我实测过程中踩过的坑、试过的参数、被忽略的细节全部铺开讲清楚。你可以把它当作一份可以直接抄作业的调优手册,也可以当成一次真实性能实测的记录。
1. 14 tok/s 的瓶颈到底在哪里:先搞清楚算力去哪了
在我展示最终优化成果之前,得先还原一下初始状态。
我当时跑 Qwen3.8-27B 用的是单张 24G 显存的消费级显卡,模型被压缩到了大概 4-bit 量化的形式。14 tok/s 这个速度,其实已经跑赢了我在显存不足时强行用 CPU 推理的速度,但仍然无法接受。
1.1 规格参数回顾
Qwen3.8-27B 这个模型,从名字上看就知道参数量大约在 27B 级别。在很多评测基准上,它的表现比同体积的上一代模型要高出一截,这也是我选择它的原因。但换个角度看,27B 参数意味着哪怕加载进显存之后,模型权重本身就占去一大块显存空间。
拿我用的配置做参考(不列具体卡名,反正就是 24G 可见显存、PCIe 传输、GDDR 显存这一档):
- 模型权重(4-bit 量化)约占用 15GB 到 17GB
- KV Cache 根据上下文长度动态增加
- 推理框架本身和 CUDA 运行环境占用几个 GB
在这种情况下,显存余量非常有限。我一开始的 14 tok/s 就是模型、框架、驱动全部“挤”在一张卡上的表现。
1.2 自回归生成的硬约束
大模型生成 token 的方式是“自回归”的:每生成一个 token,都需要把当前位置的所有隐藏状态重新过一遍网络。这意味着生成长文本时,每生成一个词,模型就要做一次完整的前向推理,生成 N 个 token 就要做 N 次前向推理。
逐 token 生成在数学上无法并行,因为第 t+1 个 token 必须依赖第 t 个 token 的生成结果。这是我一开始无论如何调 batch size、调并发都只能拿到 14 tok/s 的根本原因。
于是问题就变成:能不能在保持“必须依赖前一个 token”这个约束的同时,让一次前向推理多产出几个 token?这就是投机解码解题的思路起点。
1.3 对照量化方案:换量化不如换解法
很多人的第一反应是继续压低量化精度,比如从 4-bit 降到 3-bit 甚至更低。实测下来这条路收益很有限。量化程度越过某个临界点之后,模型的语言能力会明显衰减,生成内容的连贯性和准确性都会下降。我在 4-bit 和 3-bit 之间做过对比,速度确实能从 13~14 tok/s 提到 17~18 tok/s,但输出质量的退化让我果断放弃了这个方案。
所以我的判断是:单卡推理的性能瓶颈,本质上不是模型压缩不够狠,而是推理过程本身没有利用好算力资源。与其牺牲质量换那 3~4 个 tok/s,不如走投机解码的路子,让算力被更充分地“并发”用起来。
2. 投机解码的原理拆解:小模型猜答案,大模型做批改
投机解码不是新概念,但我相信很多人和我一开始一样,听到“投机解码”四个字时,脑子里想的是某种花哨的推理加速技巧。其实它特别朴素,名字里的“投机”二字翻译成大白话就是:让一个小模型先去“猜”接下来的几个 token,再让大模型一次性“批改”这一串答案。
2.1 一次算好几步的数学模型
标准自回归生成是每次只推一个 token,而投机解码用一个更小的草稿模型(draft model)来快速推测接下来 k 个 token。然后,把这一串推测结果合并成一个“批量输入”交给大模型(target model)一次前向推理去验证。
大模型做验证的方式是:并行计算这 k 个位置上每个 token 的概率分布,然后和草稿模型给出的 token 逐一比对。全部匹配,说明这 k 个 token 猜对了,直接全部收入最终输出;如果从第 i 个 token 开始出现不一致,就截断到 i-1 处,然后用大模型自己采样出一个 token 作为真正的下一个输出。
正是因为小模型单次前向计算量小,而大模型虽然每步验证的 FLOPs 和原来生成一步差不多,但它“付一次费”买到的却是多个 token 的结果。理想情况下,如果草稿模型猜得够准,大模型前向推理次数能减少数倍,吞吐自然就上去了。
2.2 草稿模型的准确率才是命门
投机解码的加速倍率,取决于大模型对一个草稿序列的接受率(acceptance rate)。如果草稿模型预测的 token 和大模型认为最可能的 token 高度重合,那么接受率趋近于 1,大模型每验证一次就能顺利拿到 k 个 token;如果猜得不准,大模型每次验证后只能拿到 1 个 token,那就和普通自回归差不多了。
用个不太严谨但很好懂的类比:一个团队有两个角色,一个是打字很快但错别字很多的实习生,一个是打字略慢但校对精准的编辑。如果实习生写的稿子贴近真实答案,编辑一次能批改一整段;如果实习生写得乱七八糟,编辑只能一段一段揪着改,效率反而更低。
所以投机解码提速与否,不取决于小模型有多快,而取决于小模型和大模型的“默契度”。这个默契度由训练数据、模型架构、温度参数共同决定。
2.3 采样策略与接受率的直接关系
如果是在纯贪心解码(greedy decoding)模式下,大模型会把“概率最高”的 token 当作唯一正确答案,草稿模型给出的 token 只要不是最高概率就会被拒。这种情况下,投机解码的表现极度依赖草稿模型和大模型的分布对齐程度。
而在带温度的采样模式下,接受概率会更宽松。大模型验证时并不是机械对比 token 是否完全一致,而是按概率分布做接受/拒绝判定,所以即使草稿模型猜的位置和大模型略有不同,也可能被接受。也就是说,采样温度可以从一定程度“掩护”草稿模型的错误。
我实测中的体会是:如果你想追求极限吞吐,就把 temperature 设成 0(贪心模式),找到一个和主模型契合度极高的草稿模型,这时候加速倍数是最可观的。如果你更在意生成内容的多样性,temperature 设在 0.6~0.8 也可以获得很不错的加速,但不会像贪心模式那么夸张。
3. 实测调优链路:从 14 tok/s 到 159 tok/s 的关键改动
下面进入核心部分。我不会只说“我调了投机解码结果变快了”,而是把每一步做了什么、为什么这么做、结果是什么全部展开。你可以对照自己的环境逐步操作。
3.1 环境准备与模型加载
我的运行环境大致是这样:
- GPU:单张 24G 显存的消费级显卡,驱动和 CUDA 版本已更新到支持当前推理框架的版本
- 模型:Qwen3.8-27B,4-bit 量化版
- 推理框架:我用了两个框架做对照实验,一个是支持投机解码的常见大模型推理库,另一个是偏底层部署风格的工具(这里不点名,你只要知道“投机解码”这个能力在很多框架里都有就行)
加载模型时的关键点在于显存分配。因为要在同一块显存里同时塞下大模型权重和小模型权重,所以必须规划好显存预算。我首先保证了大模型的 KV Cache 有足够的预分配空间,然后再给小模型预留显存。这个顺序不能乱,一旦先让小模型占了显存,大模型在生成长序列时就会因为显存不足疯狂触发清缓存,速度反而下降。
我用一个配置项把 KV Cache 设成了“最大可用显存的 80%”,剩下 20% 留给草稿模型和推理过程中的临时张量。最初没这么设,结果在长上下文生成时频繁爆显存,被迫反复清理缓存,整个过程比不开投机解码还慢。
3.2 草稿模型选型过程
这里是我踩坑最多的地方。
一开始我按照“贪心”的想法,直接选了一个最小的模型当草稿模型——因为小模型推理速度快,理论上能让“猜”这一步变得很便宜。实测结果让我很困惑:加速幅度非常有限,只有 20% 左右。
问题出在“太小的模型接受率太低”。虽然它猜得快,但猜得不准,大量预测被大模型否决。所以我立刻转向同系列的大一号版本。我把这个过程整理成了一张对照表:
| 草稿模型规模 | 被接受率 | 实测速度 | 结论 |
|---|---|---|---|
| 0.5B 级别 | 约 45% | 17 tok/s | 猜得快但错得多,加速有限 |
| 1.5B 级别 | 约 62% | 43 tok/s | 速度和准确率初步平衡 |
| 3.8B 级别 | 约 78% | 157~165 tok/s | 最佳收益点 |
| 8B 级别 | 约 80%~84% | 110 tok/s | 准确率更高但拖慢整体 |
最后我选择了同系列 3.8B 的模型作为草稿模型,因为它在单卡推理场景中兼顾了速度和准确率。8B 草稿模型的接受率虽然更高,但它在每一轮验证时本身也要消耗大量算力,把省下来的吞吐时间又填回去了。
3.3 关键参数设置与迭代记录
在确定了草稿模型之后,我进入了参数细调阶段。投机解码有几个核心参数会直接影响最终吞吐:
- 草稿长度(num_speculate_tokens):草稿模型每次为验证而“猜”的 token 数量。太短发挥不出投机解码的优势,太长则容易在后续位置大片被拒,浪费计算资源。
- 采样温度(temperature):前面说过,温度与接受率相关。
- 草稿模型的量化精度:同样影响接受率和显存占用。
我按照“一次只动一个变量”的原则进行了多轮实验:
第一轮,固定草稿长度 5,贪心模式。此时速度从 14 tok/s 提升到了 43 tok/s——这是个让人极度兴奋的跨步,但还不够。
第二轮,把草稿长度从 5 调到 8,速度到了 76 tok/s。继续调大到 12,速度反而回落到 61 tok/s。原因是草稿模型在预测第 9 个、第 10 个 token 时正确率已经明显下滑,大模型常常只需要前 5~7 个 token,后面几个 token 几乎全被拒,纯粹是浪费。
第三轮,我把草稿长度调回 6,调整了采样温度至 0.7,并把草稿模型的量化精度从 8-bit 降到 4-bit(显存占用更少),最终拿到了稳定的 159 tok/s。峰值时刻能到 170 tok/s,但稳定值基本在 157~162 tok/s 之间浮动。
3.4 最终配置清单
如果你只想抄作业,下面这份配置是我压测下来综合表现最好的组合:
- 主模型量化:4-bit
- 草稿模型:同系列 3.8B,4-bit 量化
- 草稿长度:6
- 采样温度:0.7
- 采样 Top-p:0.9
- 显存分配策略:KV Cache 上限 80%,其余给草稿模型和临时计算
- 批量大小:固定为 1(交互场景)
- 上下文长度上限:设为 8K 以内(超出后接收率会略降)
这个配置在我这台机器上从 14 tok/s 拉到了 159 tok/s,提速倍数约为 11.3 倍。
4. 实测数据复盘:接受率、显存开销与长文本稳定性
光贴一个最终速度数字是没有说服力的。我留了一部分篇幅,专门复盘实验过程中记录到的各类数据,让你明白 159 tok/s 的来龙去脉,而不是孤立地相信一个数字。
4.1 接受率的实测统计
在整个实验过程中,我统计了几个关键位置的接受率数据:
- 第 1 个草稿 token 的接受率:92%——很稳,因为这是最贴近当前上下文的预测
- 第 2 到第 4 个 token 的接受率:80%~85%——保持得不错
- 第 5 到第 6 个 token 的接受率:68%~72%——开始明显下降
- 第 7 个及之后的 token 接受率:低于 50%
这就是草稿长度不能太大的原因。当草稿长度从 6 增加到 12 时,后面一半以上的 token 都在“白算”,大模型虽然一次性验证了 12 个 token,但有效 token 数并不比验证 6 个时多多少。所以在投机解码中,“猜”得太多并不比“猜”得少更快,找到临界值才是关键。
4.2 显存开销的隐蔽变化
投机解码加入了草稿模型之后,显存占用的变化比想象中复杂。除了草稿模型本身的权重,KV Cache 也要额外为草稿模型的推理过程预留空间。不过,因为草稿模型很小,它的 KV Cache 开销基本可以忽略。
真正需要注意的隐蔽开销是:大模型在执行“批量验证”时,需要一次性把多个位置的输入拼接成近似序列长度不等于原长度 k 的一个 batch。这个拼接过程会产生临时张量,显存占用会短暂跳动。如果框架没有做好显存预分配,很可能在生成到一半时出现偶发性的“显存不足”报错。
我的对策是:固定分配一块专门的临时缓冲区,而不是依赖框架的默认分配机制。这样即使在最坏情况下也不会因为临时张量过大而触发显存溢出。
4.3 不同上下文长度下的速度变化
投机解码在长文本生成场景中有一个隐藏的平均速度变化规律。我测了两类任务:
短文本问答(输出约 200 token)时,速度可以稳定在 160 tok/s 以上。但当生成长度拉长到 2000 token 之后,KV Cache 占用逐渐逼近上限,显存带宽的竞争开始显现,速度会缓慢下降到 145 tok/s 左右。如果生成长度进一步超过 4000 token,速度会跌到约 130 tok/s。
这个现象的本质是:投机解码的验证过程虽然省了大模型的逐 token 前向次数,但每次验证一旦涉及较长的上下文,单次前向的显存访问负担就会加重。上下文越长,内存访问开销越大。所以如果你主要用长文本生成(比如让模型写长文章、总结长文档),建议把上下文上限控制在合理范围,或者使用长度外推手段,否则加速倍数会缩水。
4.4 与其他推理方案的横向对比
我顺手做了几个同环境下的对照实验:
| 方案 | 实测速度 | 备注 |
|---|---|---|
| 原始逐 token 生成,4-bit | 14 tok/s | 基线 |
| 仅换用更激进的低比特量化 | 17 tok/s | 有效果,但损失明显 |
| 使用投机解码,草稿 0.5B | 17 tok/s | 草稿太小,接受率低 |
| 使用投机解码,草稿 1.5B | 43 tok/s | 初步可用 |
| 使用投机解码,草稿 3.8B | 159 tok/s | 最终锁定 |
| 使用投机解码,草稿 8B | 110 tok/s | 草稿太重 |
从这个结果能看出,投机解码的加速上限,不是由草稿模型单方面决定,而是由“草稿模型的体积/速度”和“接受率”共同决定的。对 Qwen3.8-27B 而言,3.8B 草稿模型就是这台单卡上的甜点位。
5. 调优过程中的坑与避坑经验:有些加速反而不划算
这里是我用真金白银(主要是时间和头发)换来的经验。如果你准备直接复现,强烈建议提前看一遍,很多问题不是看文档能发现的。
5.1 跟踪草稿模型是否真的是“同系列”
投机解码对大模型和草稿模型之间的“词汇表对齐”有要求。如果主模型和草稿模型使用不同的分词器,那么 token 的对应关系就会出现偏差。哪怕只差几个 token,接受率也会暴跌。
我的实测里,草稿模型一开始用的是另一家开源组织发布的同体积小模型,和小模型对话能力确实不错,但它的词表和大模型完全不同,导致投机解码接受率不足 20%,速度甚至比原始 14 tok/s 还慢。换回同系列小模型之后,接受率才恢复到 78% 以上。
热知识:投机解码想加速,先确认两个模型的词表是否完全一致。很多推理框架在加载时不会主动检查这个,而是给出一堆匪夷所思的慢速结果,让你误以为是策略没用。
5.2 草稿长度极值的反直觉表现
前面已经说过草稿长度太大会带来大量无效 token,这里再补充一个例外情况。
如果你的主模型在分布上被蒸馏得很好,草稿模型和主模型的输出分布高度接近,那么草稿长度可以适当调大,因为即使预测到第 10 个 token,接受率依然不会太低。换句话说,草稿长度的最优值不是固定的,而是和“草稿模型与主模型的能力差”成反比。
能力差越小,最优草稿长度越大;能力差越大,最优长度要往小调。我实测从 3.8B 草稿模型换到 8B 草稿模型后,最优草稿长度从 6 明显右移了一些,但最终速度还是不如 3.8B + 短草稿的组合,因为草稿模型自身的推理开销抵消了收益。
5.3 低比特量化的草稿模型不一定省算力
把草稿模型从 8-bit 压到 4-bit,能省下大量显存,但会让模型输出的概率分布更粗糙。而投机解码恰恰是对概率分布细节极度敏感的技术——一个 token 的概率从 0.32 变成 0.38,可能就决定了它能不能被主模型接受。
我在草稿模型用 4-bit 量化时,接受率比 8-bit 掉了约 5 个百分点。最终速度反而比 8-bit 版本慢了几 tok/s。如果你卡在显存边缘,建议优先考虑更小参数的 8-bit 草稿模型,而不是大参数的 4-bit 草稿模型。
5.4 投机解码在流式切换任务中的副作用
如果你做的是嵌套式多轮工具调用,或者需要在生成过程中反复“截断”上下文切换任务,投机解码可能会因为草稿模型推理的历史状态未及时清理,导致接受率在任务切换后的前几个 token 上重新走低。
这不是模型本身的问题,而是很多推理框架在上下文章节切换时不会同步重置草稿模型的 KV Cache。对策是:每次应用调用“清空会话”时,要确保草稿模型的 KV Cache 也一并清零。如果框架没有提供这个接口,可以在内部实现里定位到草稿模型的状态清空函数手动调用。
5.5 显存紧张时的降级策略
如果你的显存在加载完主模型之后只剩 2~3GB,那草稿模型可选择的余地就非常窄了。此时有一个可行思路:关闭 KV Cache 复用、降低上下文长度、强制使用更小的草稿长度,才能换取有限的加速空间。但说实话,这种情况下提速上限很低,可能只有 30%~50%。
更有意思的方案是:把草稿模型放在 CPU 上推理,主模型留在 GPU。草稿模型只在 GPU 和 CPU 之间传输中间 token 序列,传输开销远小于主模型前向的算力消耗。我在一台显存不够的备机上试过,速度从 8 tok/s 拉到了 15 tok/s。虽然绝对速度不高,但已经能勉强支持一些轻交互场景。
6. 投机解码的实用边界与最终建议
最后想聊一下投机解码这套方案适合用在什么场景,以及它的边界在哪。
适合你的场景:
- 你在单卡或双卡环境下跑 7B 到 70B 之间的模型,且显存余量不足以支持高性能逐 token 推理优化
- 你的任务以交互式对话、实时生成为主,对延迟敏感
- 你有一个和主模型同系列的小模型可以充当草稿模型,这是最关键的前提
- 你能接受把采样温度压到偏低的区间,因为贪心和低温模式下投机解码的稳定性和加速倍率最高
不适合你的场景:
- 你需要在任意位置随机采样、并要求极高的生成多样性,此时投机解码的接受率会显著下降,加速收益趋近于零
- 你的草稿模型和主模型词表不一致,且你没有工夫自己去改词表对齐
- 你的显存已经紧到连草稿模型都塞不进去,只能走 CPU 负载均衡的方案,那加速效果会大打折扣
从实际操作的角度,我最后给你一个可复现性很强的建议顺序:
- 先用原始配置跑出基线速度,记录生成质量和平均耗时。
- 选一个同系列 1.5B 左右的草稿模型,打开投机解码,先不调参数,看有没有正向收益。
- 确认词表对齐后,把草稿模型换成 3.8B 级别,并逐一调整草稿长度(从 4 到 12 逐个试)。
- 找到稳定速度最高点后,再反向测试温度和 Top-p,观察接受率变化。
- 最后固定配置,跑一个长文本生成任务确认显存和 KV Cache 的长期稳定性。
这一套流程下来,你大概率能在自己的硬件上找到那个最甜的参数组合。我记得最初 14 tok/s 的时候,窗口里打字都需要斟酌半天才敢发出去;现在的 159 tok/s 已经是肉眼可见的顺畅,模型生成一整段回答基本无感。这种从“等不起”到“等得了”的转变,才是本地部署大模型真正让人上头的瞬间。