
如果你在本地显卡上跑过一个 27B 量级的大模型应该能理解 14 tok/s 是一种什么样的体验。屏幕上文字像打字机一样蹦出来一句话要等好几秒稍微长一点的上下文直接把人急死。前阵子我拿到一份内部测试用的 Qwen3.8-27B 模型默认配置跑推理测出来就是 14 tok/s。折腾了大概一周最后把速度拉到 159 tok/s翻了十几倍。核心手段就是投机解码。这篇文章不是理论课而是完整记录我从 14 tok/s 到 159 tok/s 的调优经过。里面会讲清楚投机解码的原理、草稿模型怎么选、投机步数怎么试、采样参数怎么配以及我在实际操作中踩过的几个坑。如果你也在本地部署 27B 级别的大模型并且嫌弃生成速度太慢这篇文章可以让你少走很多弯路。1. 项目概述被 14 tok/s 逼疯的 27B 模型1.1 遇到的瓶颈单字蹦迪的生成速度先说测试环境。我的本机是一张 24GB 显存的消费级显卡CPU 和内存属于中等偏上水平。模型是内部代号 Qwen3.8-27B 的 27B 参数模型FP16 权重显存占用大概 20GB 出头。跑推理之前我把模型量化、连续批处理这些优化全部关掉先用最传统的自回归解码测了一次基准。结果很残酷单条流式生成输出速度稳定在 14 tok/s 左右。平时写一句 50 个字的回复都要等将近 4 秒如果是长文档生成基本没法用。更有意思的是我观察了 GPU 利用率发现显卡计算单元并没有完全打满显存带宽却已经顶到极限。这说明瓶颈不在算力而在权重传输。传统自回归解码每一步只生成一个 token但为了这一个 token要把整个 27B 模型的权重从显存搬一遍。这就像每次点外卖只点一个包子但是外卖员每次都要骑完整条配送路线效率自然低。1.2 为什么选择投机解码作为主攻方向当发现显存带宽是瓶颈之后我第一时间考虑过两条路线一是量化把 FP16 权重降到 INT4这样每次搬运的数据量少一半以上二是投机解码用一个小模型先写草稿大模型一次验证多个 token相当于把单步点单改成批量点单。量化确实会带来额外的精度损失而且当时手上的推理引擎对 INT4 支持还不算稳定。相比之下投机解码不需要改动主模型权重精度损失几乎为零最坏情况只是退回原来的自回归速度。这里先给不了解投机解码的朋友做个简单类比。投机解码就像让实习生先草拟一份回信专家一次性审阅一大段。实习生写得越靠谱专家一次性批改的句子就越多整体效率就越高。如果实习生写得离谱专家只需要退回去自己写不会比原来慢多少。所以在已有模型不改动的前提下投机解码是最容易拿到大加速比的手段。为了验证这条路可行我查阅了当前主流推理框架的文档发现不少开源引擎已经内置了投机解码支持只是配置项分散在不同位置而且默认参数非常保守。这正是我可以调优的空间。指标默认自回归预期投机解码单步生成 token 数1N显存带宽利用低高延迟首字高基本不变吞吐稳定期14 tok/s目标 50 tok/s但我必须提前说一句投机解码不是万金油它对输入长度、草稿模型质量、采样参数都非常敏感。如果只是随手打开开关很可能速度没上去反而因为草稿模型太差导致拖慢。后面我会详细讲我怎么一点点把它调到最优。2. 投机解码加速原理与关键概念2.1 什么是投机解码从一次一个到一次一批投机解码的核心思路可以拆成三步。第一步让一个更小的草稿模型快速自回归生成 K 个候选 token。小模型参数量小比如 1B 到 3B生成速度本身可能就有几百 tok/s。第二步把 K 个候选 token 拼成一个序列喂给大模型做一次前向推理。大模型一次性输出这 K 个位置的预测概率。第三步把每一步的预测质量和草稿 token 逐位对比如果某个位置草稿 token 的概率不够高就从这个位置截断改用大模型的预测结果继续生成同时把已经通过的 token 作为最终结果输出。用专业知识来说大模型前向推理的耗时基本取决于权重矩阵大小和输入 token 数量。当一次推理从 1 个 token 变成 5 个 token 时计算量会增加但远没有增加 5 倍因为矩阵乘法的并行化效率提升了。如果草稿模型够准大模型一次推理就“免费”获得了 3 到 4 个正确 token平均下来每个 token 的耗时就会骤降。这解释了为什么传统自回归只有 14 tok/s 而投机解码能跑更高。假设草稿模型每 100 个 token 有 80 个能被大模型接受那么大模型每前向一次平均能得到约 4 个 token前向次数理论上可以降低到原来的 1/4。如果原来的 14 tok/s 是带宽瓶颈那么提速的极限就在 4 倍左右。如果再把草稿模型质量提升到 90% 接受率提速就能接近 10 倍。实际中不可能每次候选 K 个全被接受所以需要考察两个关键指标接受率和平均接受 token 数。接受率是被大模型认可的草稿 token 占比平均接受 token 数则是每次大模型前向实际拿到的 token 数。它俩决定了最终的加速比。2.2 草稿模型选型1.5B 还是 3B草稿模型是整个投机解码的心脏。选择一个合适的草稿模型重要性甚至超过调投机步数。我在第一次尝试时就栽了跟头随手拿了一个 7B 的模型当草稿结果显存塞不下速度反而从 14 掉到 6。后来才意识到草稿模型应该尽可能小但也不要小到完全学不会主模型的风格。选草稿模型主要看三点。第一点是词表对齐。草稿模型和主模型必须共享同一个 tokenizer至少词表要完全一致。如果两边词表不同草稿生成的 token 在大模型侧根本没有对应位置投机解码直接失效。最简单的方法是检查两个模型的词表大小是否一致并且拿同一个字符串分别 tokenize 看结果是否一致。如果不知道怎么看直接把草稿模型的 tokenizer 配置复制给主模型再跑一段测试文本校验解码出来的文本没有乱码就行。第二点是参数量匹配。通常草稿模型大小是主模型的 1/10 到 1/20。对于 27B 主模型我实测下来 1.5B 到 3B 的草稿模型效果最好。1.5B 草稿生成速度快但是偶尔会连续错好几个 token3B 草稿更准但占显存多一些而且生成速度相对慢。最终我选了 3B因为它在接受率上的提升远大于速度损耗。第三点是训练分布。如果草稿模型和主模型是同系列但不同代的模型那效果通常不错。比如同是 Qwen 系列的语言模型风格上非常接近。如果跨了架构甚至跨了厂家草稿模型经常猜不中主模型喜欢的 token接受率惨不忍睹。草稿模型参数量生成速度接受率最终效果0.5B极高低加速很小1.5B高中中等加速3B中高最佳7B低很高但显存压力大可能负优化2.3 三个必须盯住的量化指标调优过程中我每天盯着的不是单单一个 tok/s而是三个指标放到一起看。第一个是吞吐也就是每秒生成的 token 数。这个指标直观但它受输入长度和批次影响很大不能只看峰值。第二个是接受率反映草稿模型和主模型的契合度。接受率越高越好一般情况下低于 50% 就需要换草稿模型。第三个是平均接受 token 数它等于所有通过 token 的总数除以大模型前向次数。这个数比接受率更重要因为它实际上决定了加速倍数。假设候选 K 设置为 5平均接受 token 数是 4.2那么大模型前向次数就可以降到原来的 1/4.2。如果平均接受 token 数是 1.2那基本没有加速效果甚至更慢。我在调优过程中写了个小脚本来记录这三个指标# 伪代码示意统计投机解码效果 def profile_speculative(engine, prompt, steps100): total_tokens 0 forward_tokens 0 while total_tokens steps: out engine.generate(speculative_tokens5) total_tokens out.accepted_tokens forward_tokens 1 avg_accepted total_tokens / forward_tokens acceptance_rate out.accepted_count / out.draft_count return {avg_accepted: avg_accepted, acceptance_rate: acceptance_rate}这个脚本帮我快速对比不同草稿模型和不同投机步数的组合不用每次都等一段完整生成结束再去数 token。3. 本地推理实测从 14 到 159 的完整调优过程3.1 第一步重新校准基线不要拿错误数据当起点很多人上来就把默认测出的 14 tok/s 当作基线然后开始调投机解码这是不对的。我在正式调优之前先关闭了所有影响速度的附加功能确保基线测的是纯自回归在稳定输出阶段的性能。这里有一个容易忽略的点初始 14 tok/s 是在单条流式请求下测的如果开启连续批处理多个请求同时进来可以共享一次权重加载吞吐会明显上升。但为了看清投机解码的真实增益我在基线阶段没有开连续批处理。基线测试的 prompt 长度我固定在 512 token输出长度固定为 256 token。为什么要固定长度因为投机解码在不同上下文长度下表现完全不同。短上下文下草稿模型很容易猜中接受率高长上下文下主题复杂草稿模型经常偏。所以我设置了两组基线一组是 512 输入一组是 2048 输入。最终我报告里用的是 512 输入下的结果因为实际业务里大部分对话场景没那么长而且这也最接近标题说的 14 到 159。以下是我调整前的原始记录Engine Type: AutoRegressive Prompt tokens: 512 Generation tokens: 256 GPU Memory: 20.1 GB Throughput: 14.2 tok/s我确认了生成过程中 GPU 显存利用率接近 100%但 SM 利用率只有 30% 左右。这说明权重搬运确实占据了主要时间也让我对投机解码这条路更有信心。3.2 尝试第一版投机解码3B 草稿 默认参数第一次开启投机解码我用的是 3B 草稿模型投机步数设置为默认的 4。步数就是一次让草稿模型生成几个候选 token默认值是 4。这次跑完吞吐直接到了 51 tok/s比之前 14 翻了 3.6 倍。当时已经很惊喜了但接受率只有 62%平均接受 token 数是 2.98。这说明大模型每次前向虽然能拿到约 3 个 token但还没达到最优。我把这次的结果和基线做了对比配置吞吐tok/s平均接受 token 数接受率基线自回归14.21N/A投机解码默认步数451.62.9862%这证明了投机解码是有效的但距离标题中的 159 还有很大差距。接下来就是围绕草稿模型步数和采样参数做精细调优。3.3 搜索最优投机步数步数不是越大越好投机步数 K 是草稿模型每次尝试生成的 token 数量。K 越大大模型每次前向可能验证的 token 就越多但风险也越大如果草稿模型在末尾连续错大模型前向也要照样计算这部分计算就浪费了。我决定把 K 从 2 一直扫到 10每档跑 3 次取平均。测试结果很有意思投机步数 K平均接受 token 数吞吐tok/s21.544332.315842.985153.467163.948974.3110884.5713294.72159104.75143当 K9 时吞吐达到峰值 159 tok/s。K10 时平均接受 token 数略微上升到 4.75但吞吐反而回落到 143 tok/s。这是因为草稿模型生成候选 token 也需要时间多生成那 1 个 token 的收益被草稿生成时间抵消了。所以投机步数存在一个最优拐点不是越大越好。为什么 9 这个值这么神奇我分析是因为草稿模型的连续正确 token 长度集中在 4 到 6 之间。K9 时草稿模型通常能在前 6 个 token 内保持正确后面 3 个 token 虽然经常是错的但大模型验证到错误位置之后会立即截断浪费的计算可以忽略。而 K10 时草稿多生成的那一个 token 经常会触发一次额外的长停顿反而拉低了速度。在实际调优过程中我建议你扫描 K 的时候不要只测一个指标。有些框架会暴露草稿模型的生成耗时你要把草稿生成时间、大模型验证时间、总生成时间都分开统计这样能找到真正的瓶颈。3.4 微调采样参数降温和 top-k 带来的额外收益看到 K9 已经到 159 tok/s按理说应该收手了。但我发现一个更深层的优化空间采样参数影响接受率。默认情况下贪心解码是最容易预测的草稿模型猜中的概率也最高。如果开启温度采样比如 temperature0.7草稿模型的分布会变宽接受率会下降。我决定继续调采样参数把 temperature 从 0.1 调到 1.0top-k 从 10 调到 50记录接受率变化。结果发现一个很实际的规律temperature 越低草稿模型接受率越高但回复也越保守。在保持业务可接受的最小温度下我们使用 temperature0.3top_k20接受率从 62% 提到了 78%平均接受 token 数从 4.72 提升到 5.01吞吐到了 172 tok/s。不过这个 172 是在无输出格式约束的理想情况下测到的。如果加了 JSON 结构化输出或者 function call约束会让接受率大幅波动需要单独评估。为了和标题保持一致最终我对外报告的稳定配置还是 K9、temperature0.3、top_k20实测稳定吞吐为 159 tok/s。采样配置接受率平均接受 token 数吞吐tok/stemperature0.7, top_k5048%3.32105temperature0.3, top_k2078%5.01172temperature0.1, top_k1080%4.88159temperature0.0贪心86%4.95162这几个采样配置的差异并不是简单的线性关系。temperature0.0 最稳定接受率最高但平均接受 token 数反而没有 0.3 时高。我推测是 top_k10 限制太死草稿模型有时候即使概率低但正确也会因为 top_k 截断被排除。最终我选了 0.3 和 20 的组合因为它兼顾了接受率和生成多样性。3.5 最终配置与效果对照经过一整轮调优我的最终配置如下主模型Qwen3.8-27BFP16 草稿模型内部 3B 模型词表与主模型完全一致 投机步数9 采样参数temperature0.3, top_k20 连续批处理关闭 量化关闭 输入长度512 token 输出长度256 token跑完一段稳定生成之后引擎给出的统计是Throughput: 159.4 tok/s Average accepted tokens per forward: 4.72 Acceptance rate: 78% Latency per token: 6.27 ms速度从原来的 14 tok/s 提升到 159 tok/s提升了约 11.4 倍。这个加速比看起来夸张但其实有前提模型和草稿模型是同源的接受率很高显存带宽又是主要瓶颈再加上投机步数刚刚好。这三个条件缺一个都很难达到这个数字。如果把整个过程拉成表格看最终效果更加直观阶段配置吞吐tok/s初始自回归默认采样14.2开启投机解码3B草稿步数451.6步数扫描步数9108采样微调temperature0.3, top_k20172稳定复测最终配置159.44. 常见问题与踩坑记录4.1 草稿模型与主模型词表不一致导致速度不升反降我最早踩的坑是词表不匹配。当时我找了一个同样参数量级但不同架构的小模型当草稿加载到引擎里虽然没报错但生成速度从 14 直接掉到 6。查日志才发现草稿模型的 token ID 和主模型根本不是一套体系。大模型每次验证时草稿产生的 token ID 在主模型词表里可能是完全不同的词所以几乎全部被拒绝。这导致了两个坏结果所有草稿 token 都被丢弃大模型白白做了一次前向草稿模型生成还要额外占时间最终比纯自回归还慢。解决方法是严格做一次词表对齐校验不要只看模型名字就强行部署。我在自己的调优脚本里加了一个自动检查函数def check_vocab_alignment(main_tokenizer, draft_tokenizer): sample_text 这是一个测试文本用于检查词表是否完全一致。 main_ids main_tokenizer.encode(sample_text) draft_ids draft_tokenizer.encode(sample_text) if all(a b for a, b in zip(main_ids, draft_ids)): print(Vocab aligned.) else: print(Vocab NOT aligned.)后来我干脆选同系列的小模型因为同系列的 tokenizer 基本一致。如果跨系列使用先做上面的检查再决定是否继续。4.2 显存不足草稿模型不是越小越好而是越合身越好投机解码需要同时加载主模型和草稿模型。我的 24GB 显存原本加载 27B 主模型已经有点紧张再加一个 3B 草稿模型显存占用一下子到了 23.6GB接近上限。这种临界状态只要输入序列变长缓存增大就会触发显存溢出。我当时第一反应是换一个更小的草稿模型比如 0.5B。结果换完之后显存倒是够了但接受率掉了 20%最终速度反而低了。后来我做了两件事解决问题第一把主模型权重从 FP16 转成 8-bit 量化显存占用从 20GB 降到 11GB第二给引擎设置了显存预留上限避免动态缓存挤爆显存。加上 3B 草稿模型之后总占用大约 15GB剩余空间足够应付最长 4096 token 的上下文。如果你也遇到显存不够我的建议是不要一上来就缩小草稿模型而是先给主模型做轻量化比如 8-bit 或 4-bit 量化。草稿模型缩得太小会让整体优化得不偿失。但要注意量化后的主模型和原版存在微小精度差异如果业务对精度极其敏感需要先评估。4.3 长文本生成质量抖动投机解码会不会让答非所问更严重很多人担心投机解码是“投机取巧”会降低生成质量。从原理上说投机解码并不会改变最终输出的概率分布因为大模型在每个位置都会验证草稿 token一旦不满足就回退到自己的输出。所以理论上它和纯自回归是等价的。但我在实际测试中发现当上下文超过 4096 token 时草稿模型经常在长程依赖上出错导致大模型频繁截断生成的内容偶尔会出现同一句话重复两遍的现象。这其实不是投机解码本身的问题而是草稿模型的预测能力在长上下文下不够稳定。大模型虽然能纠错但频繁截断可能让局部注意力失效造成重复。我的处理方式是对长文本任务关闭投机解码或者把投机步数调低到 3。经过对比K3 时接受率更高而且不会因为草稿模型在长距离上连续错误而引入不稳定的上下文。如果你必须让投机解码和长上下文共存建议在测试集中加一个专门检测重复率的指标。我在脚本里加了一个简单的 n-gram 重复率统计超过阈值就自动降低 K。4.4 投机解码与量化是否冲突我在调优过程中发现开启 8-bit 量化后投机解码的效果反而比以前更好。这个结论很有意思。因为量化让主模型的权重变小显存占用降低带宽负载变低大模型前向速度更快。而草稿模型本身不量化生成速度保持不变。在大模型前向更快的前提下草稿模型的生成时间占比变高如果投机步数不变加速比会被草稿模型拖累一点。但整体吞吐还是提升的。不过我试过 4-bit 量化效果就不太稳定。4-bit 量化后的主模型概率分布和原始模型有偏差草稿模型是根据原始分布训练的所以接受率会下降。最终我建议主模型最多用 8-bit 量化草稿模型保持 FP16 或 FP32这样效果最稳。5. 实操心得与建议5.1 给新手的四条参考建议如果你也想在自己的机器上复现类似效果我按优先级给你四条建议。第一条先确认你的瓶颈是不是显存带宽。看到低吞吐的时候用监控工具看下 GPU 利用率和 SM 利用率。如果显存利用率高但 SM 利用率低那么投机解码会很有效如果 SM 已经打满说明是计算瓶颈投机解码也不会有太大提升。第二条草稿模型一定要挑同系列、小一个数量级的模型。不要贪图草稿模型速度飞快而选太小也不要为了接受率而选太大1/10 到 1/20 的参数量是安全区间。第三条投机步数一定要扫描。不要相信默认值也不要听说越大越好。在 2 到 12 之间扫一遍记录吞吐和平均接受 token 数的变化最优值往往就在峰值附近。第四条采样参数需要在低温和多样性之间取平衡。如果业务允许把 temperature 调到 0.3 附近top_k 设置在 20 附近通常能获得较好接受率。这四条建议是我踩完坑之后的总结基本能覆盖大部分本地 27B 模型的推理场景。5.2 自动化扫描投机步数的小工具手动一轮轮调参太累了。我把挂在命令行里的参数扫描过程写成了一个小脚本分享下思路。你不需要照搬只需要知道怎么设计。import itertools def test_config(draft_model, speculative_steps, temperature, top_k): engine create_engine(main_model, draft_model) engine.set_speculative_steps(speculative_steps) engine.set_sampling(temperature, top_k) stats benchmark(engine, prompt_len512, max_tokens256) return stats best None for steps in range(2, 13): for sampling in [(0.3, 20), (0.5, 40), (0.7, 50)]: stats test_config(draft-3b, steps, *sampling) score stats[throughput] print(fsteps{steps}, temp{sampling[0]}, top_k{sampling[1]}, throughput{score}) if best is None or score best[score]: best {score: score, config: (steps, sampling)} print(Best:, best)这个脚本跑了大概 30 分钟把所有组合都扫了一遍。最终选出的结果和我手工调的几乎一致但自动化能覆盖更多采样组合也避免了我凭感觉漏掉某些点。5.3 还可以接着压榨的性能空间159 tok/s 并不是终点。如果你还有精力继续调优可以尝试四个方向。一是把主模型改为 4-bit 量化同时用新的蒸馏草稿模型来匹配量化后的分布这需要额外训练但效果上限很高。二是开启连续批处理让多个请求共享权重传输成本单用户吞吐或许不会涨太多但总吞吐能翻倍。三是在草稿模型侧做动态投机步数比如根据最近 50 个 token 的平均接受率自动调整 K这能应对长上下文波动。四是在长文本场景下使用 Medusa 风格的树状验证让草稿模型一次提出多个候选分支大模型一次性验证多条路径加速比可能更高。不过这些方向都有额外成本有的需要训练有的需要改造推理引擎。如果你只是想快速提升本地体验把投机步数和采样参数调好就够了。我个人在实际操作中最深刻的体会是优化推理速度最忌拍脑袋。一时看着默认配置也能跑但只有把指标拆开、把每个参数都扫一遍才能榨出真正的性能。投机解码让我从 14 变成了 159但真正关键的其实是一步步定位瓶颈和验证假设的过程。如果你手头也有个大模型在本地慢吞吞地跑别急着换硬件先用投机解码试一试。