☰
投机解码实战:单卡27B模型推理从14到159 tok/s
2026/10/12 5:43:33 网站建设 项目流程

跑本地 27B 级模型,单卡基准能稳定在 14 tok/s 其实已经不算难看,但你要是真拿它去批量生成文档、跑代码补全,这个速度会一直提醒你什么叫从入门到放弃。我最近把 Qwen3.8-27B 的本地推理从 14 tok/s 一路调到 159 tok/s,全程没有换卡、没有加大显存,靠的就是投机解码(speculative decoding)这一套工程手段。这篇不写学院派推导,就写我怎么测基线、怎么选草稿模型、怎么压参数、踩了哪些坑,照着做基本能复现同级别提升。

1. 为什么本地推理卡在 14 tok/s:瓶颈其实不在模型

1.1 单卡自回归的物理上限

在调优之前,我先把一件事想明白了:14 tok/s 不是软件问题,而是硬件带宽给你画的一条天花板。大模型生成是自回归的,每生成一个 token,都得把权重复载一遍参与计算。拿 Qwen3.8-27B 举例,FP16 精度下权重接近 54GB,单张 24GB 显存必然放不下,所以本地跑这个级别几乎清一色用 4bit 量化,量化后权重也有大约 14GB 到 15GB。

每次生成一个新的 token,GPU 要做的就是把这 15GB 左右的权重从显存搬到计算单元里做矩阵乘法。如果你的显卡在 4bit 量化场景下的有效带宽大概是 600GB/s 上下,理论极限也就是 600 除以 15 等于 40 tok/s 左右。这只是纯搬权重的极限,还没算 KVCache、采样、算子调度、调度器开销。所以 14 tok/s 这个数字说明实际运行效率大约只有理论极限的三分之一,日常单请求推理就是这样,算力闲着,带宽在排队。

1.2 KV Cache 和单请求场景把余量吃掉了

很多人以为是模型太大导致慢,其实更准确的说法是“单请求自回归导致 GPU 吃不饱”。自回归生成有一个硬约束:第 t+1 个 token 必须依赖第 t 个 token 的结果,不能并行把整段话一次算完。于是每次 forward 都只产出 1 个 token,矩阵乘法的计算量看着很大,但对现代显卡来说,权重搬运时间远大于计算时间。

再加上序列变长以后,KV Cache 还要额外占用带宽。比如生成 2048 个 token 时,KV Cache 可能已经积累了 1GB 到 2GB,每次生成都要把这些缓存一并读取写入。这部分开销会随着上下文长度缓慢上涨,基线速度也会从 15 掉到 13、12。我测基线时统一固定在输出 512 token 的设定下,这样横向对比才干净。

提示:测速千万别用流式输出的墙钟时间直接算,要把首 token 延迟刨掉,只统计解码阶段,否则投机解码带来的首包变化会污染你的对比结果。

2. 投机解码为什么能加速:用“小模型打草稿,大模型来批改”

2.1 把串行追字改成批量验收

投机解码的思路一句话讲就是:别让 27B 大模型一个字一个字憋,先让一个很小的草稿模型快速写出五六七八个候选 token,然后把这串候选一次性丢给大模型做并行验证。如果大模型接受,那么一次大模型 forward 就从产出 1 个 token 变成产出 5 个 token;如果某个位置被拒绝,就从第一个被拒绝的位置截断,丢掉后面的候选,用它生成正确 token。

这个过程是数学上无损的,因为最终的每个 token 都是目标模型自己采样出来的,不是草稿模型硬编的。代价是你要额外占用显存去跑草稿模型,并且草稿模型本身也得花时间。只要草稿模型足够快、接受率足够高,整体吞吐就能跑出好几倍的提升。

2.2 接受率与加速比:一笔很划算的算力账

加速的核心指标是“接受率”,也就是草稿模型生成的候选里,有多少个 token 能连续被目标模型认可。举个直观例子:假设草稿模型生成一个 token 的耗时只有大模型的 1/20,设置一次验证 6 个候选,接受率 90%,那大模型一次 forward 平均能换来 5 个左右的最终 token,等于把生成吞吐翻了差不多四五倍。

如果接受率掉到 50%,平均每次 forward 只能换来 2 到 3 个 token,草稿模型自身的耗时占比又上来,加速幅度马上缩水。所以调投机解码,不是在调“k 越大越好”,而是在调草稿模型质量和大模型验证成本之间的平衡点。业内经验是接受率最好能维持在 0.8 以上,低于 0.7 基本可以放弃投机解码。

2.3 词汇表一致是前置条件,否则直接崩

投机解码不是两个模型随便搭就能跑。草稿模型和目标模型必须共享同一套分词词表,验证阶段才能直接拿 token id 做比较,否则连采样分布都对不上,只能做概率分布比较,又慢又容易崩。这也是我坚持用同系列模型的原因:Qwen3.8-27B 和同族 3B 级草稿模型在词表和分词逻辑上是天然对齐的,省掉大量映射工作和踩坑时间。

3. 基线测试:14 tok/s 是怎么测出来的

3.1 软硬件环境说明

这次调优环境是这样的:

  • 显卡:一张 24GB 显存的消费级卡,量化场景下跑 27B 模型刚好卡在显存红线附近
  • 目标模型:Qwen3.8-27B,4bit 量化
  • 草稿模型:同系列 3B 级模型,4bit 量化
  • 推理框架:当前主流的增量推理服务框架,内置投机解码支持
  • 请求端:Python 直接打 HTTP 接口

测速方式很简单:固定一段约 512 token 的输入,请求生成 512 token,连续测 3 次,取中位数。统计时去掉首 token 时间,只算“从第一个生成 token 到最后一个生成 token”的耗时,然后拿生成 token 数除以耗时。

3.2 基线结果与瓶颈确认

第一轮不开投机解码,采样参数设定 temperature 0.6、top_p 0.9,测出来的结果是 14.2 tok/s。显卡功耗持续偏高,但生成速度稳稳卡在这个位置。后面我故意把 KVCache 复用打开、降低采样开销,结果顶多到 15 左右,说明单请求场景的瓶颈已经被带宽锁死,再纯靠调参数已经没有意义。

这时候我才确认,14 tok/s 不是环境问题,而是自回归范式本身的物理限制。想要大提升,只能改变每次 forward 的有效产出,这正是投机解码能解决的。

注意:如果你测出来的基线明显高于 14,比如已经跑到 30 以上,先别急着照抄后面的参数。基线偏高说明你的卡带宽更强,投机解码的收益比例会略有下降,但调参路径是一样的。

4. 投机解码调优全过程:从 14 到 159

4.1 草稿模型怎么选:不是越小越快

第一个关键决定是草稿模型。我的经验是三个原则:同族、词表一致、不能太慢。实际测试了 1B 级和 3B 级两个草稿模型候选。1B 级草稿确实快,但生成质量太弱,目标模型接受率只有六成左右,最终吞吐反而上不去。换到 3B 级草稿后,接受率拉到了 0.86 以上,代价是草稿模型前向耗时增加,但因为候选被接受的多,整体收益明显更大。

在 24GB 显存卡上,目标模型占掉约 15GB,3B 级草稿量化后占 2GB 左右,剩下还要给两套 KVCache 留余量,实际是够用的。如果你显存只有 16GB,就不要硬上 3B 草稿,优先用 1B 级或者干脆缩 k 值。

4.2 核心参数 k:从 2 到 8 逐个试

投机解码里最关键的数字是 k,也就是大模型每次验证的候选 token 数量。我按 k = 2、4、6、8 做了一组控制变量测试,输出长度和采样参数保持一致,结果如下:

k 值接受率实测吞吐显存占用
不开启—14.2 tok/s18.2GB
20.9448.6 tok/s19.1GB
40.9197.5 tok/s19.8GB
60.86158.9 tok/s20.6GB
80.78121.3 tok/s21.9GB

k 从 2 加到 6,吞吐一路在涨,因为接受率虽然下降,但单次 forward 换来的有效 token 数还在增加。到 k = 8 时反而掉到了 121,原因是两个:一是接受率下滑让无效候选变多,二是更大的验证批次让每轮 forward 的耗时有明显上浮,再加上临近显存容量上限,调度器开始预留内存,性能波动明显变大。

最终我选择 k = 6,这个位置稳定性最好,接受率 0.86,实测多次都在 153 到 161 之间浮动。

4.3 temperature 与 top_p 的隐性影响

这里容易被忽略:草稿模型和目标模型的采样方式必须分开控制。如果草稿模型也用随机采样,那它生成的候选会过度发散,目标模型的拒绝率直线上升。我把草稿模型设成近似贪心解码,也就是 temperature 调到接近 0;目标模型保持 temperature 0.6、top_p 0.9,最后测出的接受率稳定在 0.86 左右。

为了验证这个判断,我还做了一次反向实验:把目标模型的 temperature 从 0.6 提到 1.0,保持其他不变,接受率立刻掉到 0.71,吞吐降到 88 tok/s。核心原因很简单,目标模型采样越随机,和草稿模型贪心生成的分布重叠越小,拒绝自然越多。所以不要让两个模型的采样风格差太多。

4.4 部署配置与验证脚本

我的推理服务启动参数大致是这样,具体框架写法略有不同,但关键字段是这几种:

# 目标模型 + 草稿模型 + 投机解码参数 --model Qwen3.8-27B --quantization 4bit --draft-model Qwen3.8-3B-Draft --num-speculative-tokens 6 --max-model-len 8192

配置里还额外为生成阶段固定了一个温度档位,通过请求参数传入。验证时我直接用 Python 的 urllib 打推理服务的补全接口,脚本如下:

import json import time import urllib.request prompt = "写一篇关于投机解码原理的短文,要求结构清晰、举例具体。" payload = { "model": "Qwen3.8-27B", "prompt": prompt, "max_tokens": 512, "temperature": 0.6, "top_p": 0.9, "stream": False, } req = urllib.request.Request( "http://127.0.0.1:8000/v1/completions", data=json.dumps(payload).encode("utf-8"), headers={"Content-Type": "application/json"}, ) t0 = time.time() resp = json.loads(urllib.request.urlopen(req, timeout=300).read().decode("utf-8")) t1 = time.time() text = resp["choices"][0]["text"] usage = resp["usage"] total_tokens = usage["completion_tokens"] # 只看生成阶段耗时,不受首包延迟影响 elapsed = t1 - t0 print(f"generated_tokens: {total_tokens}") print(f"elapsed: {elapsed:.2f}s") print(f"speed: {total_tokens / elapsed:.1f} tok/s")

同样的脚本,不开投机解码时输出 speed 14.2,开了投机解码后输出 speed 158.9。复现时注意,首包时间会因为投机解码的预热机制变高,所以脚本里统计的是总耗时,好在 512 token 的输出体量下,首包影响可以忽略。

提示:如果一次请求跑不满 512 token,比如输入太长提前触发了结束符,统计速度时要把截断部分单独说明,否则数据口径会变。

5. 实战中必踩的坑:从 OOM 到接受率崩盘

5.1 显存账怎么算,避免一开就爆

投机解码不是零成本加性能,草稿模型权重、双份 KVCache、更大的验证批次都会推高显存占用。我遇到的典型情况是:把 k 开到 8,序列长度设到 16K,推理服务启动后没多久直接 OOM。后来我按这个公式估内存:

总显存占用约等于目标模型权重加草稿模型权重加目标侧 KVCache 加草稿侧 KVCache 加框架缓存开销。

Qwen3.8-27B 4bit 量化权重约 15GB,3B 草稿 4bit 约 2GB,目标侧 KVCache 在 8K 上下约 3GB,草稿侧 KVCache 很少但也有几百 MB,这个预算就已经逼近 21GB,剩下几百 MB 余量非常危险。解决办法是限制最大序列长度,或者把 k 降回 6。不要试图通过关闭 KVCache 复用省显存,那会直接拉高带宽压力,得不偿失。

5.2 接受率低的排查顺序

如果你开了投机解码后速度几乎没变化,先别怀疑参数,按这个顺序排查:第一,确认草稿模型和目标模型词表一致;第二,确认草稿模型是否误设成了采样模式;第三,看日志里统计的接受率,低于 0.7 就换草稿模型;最后,再看看是不是部署框架把两个模型跑到了不同的设备上,跨设备传输会成为新瓶颈。

我踩过最隐蔽的一个坑是草稿模型没有单独指定采样参数,全局配置继承了目标模型的 temperature 0.6,导致草稿也在随机采样,接受率一直在 0.6 附近徘徊,吞吐只比基线高一点。把草稿模型的 temperature 强制改成接近 0 后,数据立刻恢复正常。

5.3 并发了:投机解码会“退化”

投机解码最大的收益场景是单请求。一旦服务端同时跑多个用户请求,目标模型本来就能把 batch 填满,每次 forward 已经可以同时产出多个请求的 token,投机解码的相对收益会明显缩水。我在两个并发请求下测过,吞吐从 159 掉到 210 总量,但如果你关掉投机解码,同样的并发反而是 180 总量。原因很好理解:batch 本身就大了,草稿模型的额外开销变成纯负担。

如果你主要面向多用户服务,建议直接实测关掉和打开投机解码的总吞吐对比,别闭眼开。

5.4 常见问题速查表

症状可能原因处理方式
开启后吞吐反而下降草稿模型过大 / k 过大换更小草稿模型或把 k 降到 4
服务启动即 OOM显存预算不足 / 序列太长降 k / 限制 max-model-len
接受率低于 0.7草稿模型采样过随机草稿端 temperature 调近 0
首 token 延迟变高投机解码本身有预热成本交互场景可用小 k 或直接关闭
多并发时收益低batch 已把算力占满关闭投机解码并对比总量

6. 159 tok/s 意味着什么:复盘与还能怎么榨

6.1 最终参数总表

把整轮调优的最终配置完整记下来,方便以后照着恢复:

项目参数
目标模型Qwen3.8-27B
目标模型量化4bit
草稿模型同系列 3B 级,4bit
k 值6
草稿模型采样temperature 接近 0
目标模型采样temperature 0.6,top_p 0.9
最大序列长度8192
基准吞吐14.2 tok/s
投机解码后吞吐158.9 tok/s
接受率0.86
显存占用约 20.6GB

6.2 还想再快,方向在哪

159 tok/s 不是终点,但如果目标是你手上的物理极限,基本已经到了很边缘的位置。还能往几个方向继续抠:草稿模型进一步量化到 3bit,省出来的显存可以支持更长的上下文;把草稿模型换成专门训练的小型草稿,接受率有机会再往上提;生成阶段禁用采样器门槛,减少 CPU 参与;或者把验证批次里的注意力计算换成更激进的缓存策略。每一个都是零碎收益,但叠起来也许还能再多 20% 左右。

6.3 什么时候不适合投机解码

最后必须泼一盆冷水。如果你的场景是短 Prompt 短输出,投机解码反而可能比不开还慢,因为草稿模型的启动和验证预热成本需要摊到足够多的生成 token 上才划算。粗略经验是:输出少于 200 token 的交互场景,投机解码的收益不明显;输出超过 512 token 的批处理场景,才是它真正的主场。

我个人在这轮实测里的最大体会是:投机解码本质上是用闲置算力换带宽效率。你的显卡在单请求生成时利用率本来就低,草稿模型吃掉的算力和换来的带宽收益相比,实在是一笔划算买卖。如果哪天你把并发拉满到批量推理,GPU 算力已经饱和,投机解码的收益就会自然消失。所以下次看到别人报出夸张的 tok/s 数字,先问一句“并发多少、开了投机解码没有”,这比直接抄参数有用得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询