ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

投机解码实战:单卡27B模型推理从14到159 tok/s

投机解码实战:单卡27B模型推理从14到159 tok/s 跑本地 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。每次生成一个新的 tokenGPU 要做的就是把这 15GB 左右的权重从显存搬到计算单元里做矩阵乘法。如果你的显卡在 4bit 量化场景下的有效带宽大概是 600GB/s 上下理论极限也就是 600 除以 15 等于 40 tok/s 左右。这只是纯搬权重的极限还没算 KVCache、采样、算子调度、调度器开销。所以 14 tok/s 这个数字说明实际运行效率大约只有理论极限的三分之一日常单请求推理就是这样算力闲着带宽在排队。1.2 KV Cache 和单请求场景把余量吃掉了很多人以为是模型太大导致慢其实更准确的说法是“单请求自回归导致 GPU 吃不饱”。自回归生成有一个硬约束第 t1 个 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-27B4bit 量化草稿模型同系列 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 到 1594.1 草稿模型怎么选不是越小越快第一个关键决定是草稿模型。我的经验是三个原则同族、词表一致、不能太慢。实际测试了 1B 级和 3B 级两个草稿模型候选。1B 级草稿确实快但生成质量太弱目标模型接受率只有六成左右最终吞吐反而上不去。换到 3B 级草稿后接受率拉到了 0.86 以上代价是草稿模型前向耗时增加但因为候选被接受的多整体收益明显更大。在 24GB 显存卡上目标模型占掉约 15GB3B 级草稿量化后占 2GB 左右剩下还要给两套 KVCache 留余量实际是够用的。如果你显存只有 16GB就不要硬上 3B 草稿优先用 1B 级或者干脆缩 k 值。4.2 核心参数 k从 2 到 8 逐个试投机解码里最关键的数字是 k也就是大模型每次验证的候选 token 数量。我按 k 2、4、6、8 做了一组控制变量测试输出长度和采样参数保持一致结果如下k 值接受率实测吞吐显存占用不开启—14.2 tok/s18.2GB20.9448.6 tok/s19.1GB40.9197.5 tok/s19.8GB60.86158.9 tok/s20.6GB80.78121.3 tok/s21.9GBk 从 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, datajson.dumps(payload).encode(utf-8), headers{Content-Type: application/json}, ) t0 time.time() resp json.loads(urllib.request.urlopen(req, timeout300).read().decode(utf-8)) t1 time.time() text resp[choices][0][text] usage resp[usage] total_tokens usage[completion_tokens] # 只看生成阶段耗时不受首包延迟影响 elapsed t1 - t0 print(fgenerated_tokens: {total_tokens}) print(felapsed: {elapsed:.2f}s) print(fspeed: {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 量化权重约 15GB3B 草稿 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 级4bitk 值6草稿模型采样temperature 接近 0目标模型采样temperature 0.6top_p 0.9最大序列长度8192基准吞吐14.2 tok/s投机解码后吞吐158.9 tok/s接受率0.86显存占用约 20.6GB6.2 还想再快方向在哪159 tok/s 不是终点但如果目标是你手上的物理极限基本已经到了很边缘的位置。还能往几个方向继续抠草稿模型进一步量化到 3bit省出来的显存可以支持更长的上下文把草稿模型换成专门训练的小型草稿接受率有机会再往上提生成阶段禁用采样器门槛减少 CPU 参与或者把验证批次里的注意力计算换成更激进的缓存策略。每一个都是零碎收益但叠起来也许还能再多 20% 左右。6.3 什么时候不适合投机解码最后必须泼一盆冷水。如果你的场景是短 Prompt 短输出投机解码反而可能比不开还慢因为草稿模型的启动和验证预热成本需要摊到足够多的生成 token 上才划算。粗略经验是输出少于 200 token 的交互场景投机解码的收益不明显输出超过 512 token 的批处理场景才是它真正的主场。我个人在这轮实测里的最大体会是投机解码本质上是用闲置算力换带宽效率。你的显卡在单请求生成时利用率本来就低草稿模型吃掉的算力和换来的带宽收益相比实在是一笔划算买卖。如果哪天你把并发拉满到批量推理GPU 算力已经饱和投机解码的收益就会自然消失。所以下次看到别人报出夸张的 tok/s 数字先问一句“并发多少、开了投机解码没有”这比直接抄参数有用得多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进