☰
投机解码实战:27B大模型本地推理从14提速到159 tok/s
2026/10/13 3:32:20 网站建设 项目流程

把 Qwen3.8-27B 这样的 27B 级别模型拉到本地跑,开局最难受的不是跑不动,而是跑得让人分不清是卡死还是在生成。我在一张 24GB 显存的消费级显卡上第一次跑通时,输出速度只有 14 tok/s,生成一段中等长度的代码要等半分钟起步。这个数字说能用其实也能用,但真到连续对话、翻页看生成结果、反复改代码的场景,等待感非常明显。

那阵子我一直在折腾投机解码(speculative decoding),从最简单的量化开始,一路把量化、草稿模型、批大小、采样器这四步组合拳都打了一遍,最终把同一份 Qwen3.8-27B 在本地推理的速度从 14 tok/s 拉到了 159 tok/s。这篇就把完整过程写出来,包括每一步的实测数据、原理、参数选择,以及那些让速度不升反降的坑。如果你手里也有一张 24GB 左右的显卡,想把这量级的模型跑得流畅一点,这篇可以直接照着抄。

1. 基线 14 tok/s 的真相:27B 模型卡在显存带宽而不是算力

1.1 测试环境与第一版性能

先说清楚测试环境,方便你对照。我这套是 24GB 显存的消费级显卡,配 64GB 内存,系统盘是 NVMe SSD,模型文件放在本地磁盘。操作系统在 Linux 和 Windows 下各跑过一轮,性能差异不大,下面引用的数据默认是 Linux 下的结果。

第一版我其实没做任何优化,直接拿了 FP16 转换的原始权重跑。27B 参数在 FP16 下大约需要 54GB 显存,24GB 根本装不下,只能 GPU 和 CPU 混合跑,结果就是开头说的 14 tok/s。这个数字看起来可怜,但我得说一句公道话:能跑起来已经是胜利,毕竟参数量摆在那里。

随后我从 FP16 切到 GGUF 量化格式,按同样负载重新测了一遍:

配置模型体积显存占用实测速度
FP16 原权重约 54GB24GB 装不下,混合加载14-18 tok/s
Q8_0 量化约 28GB24GB 勉强,仍需部分走内存24-27 tok/s
Q4_K_M 量化约 15GB约 16GB(含部分 KV cache)29-35 tok/s

从这组数据能看清一个趋势:量化级别越激进,速度提升越明显。Q4_K_M 相比 FP16 已经接近翻倍,但 30 多 tok/s 对 27B 模型来说仍然谈不上“流畅”。后面真正拉开差距的,是投机解码。

1.2 为什么 27B 在消费级显卡上这么吃力

在聊投机解码之前,得先搞清楚 27B 模型在本地为什么慢。很多人第一反应是“计算量太大”,其实不全对。

自回归生成是逐 token 输出的,每生成一个 token,都要对整个模型的所有参数做一次完整的前向计算。27B 参数意味着每个 token 都要“过一遍”全部权重,这时候模型消耗的其实不是算力,而是显存带宽——你每出一个 token 就要把所有参数从显存里读一遍,速度上限取决于“能多快把这几百亿参数读完”。

这里有个很关键的估算:一张中高端消费级显卡的显存带宽大致可以理解为每秒几百 GB 到近 1TB 的量级。如果模型压到 15GB 放在显存里,纯粹由带宽决定的生成上限大概就是带宽除以模型体积,约等于每秒 50 到 60 个 token。普通量化最多能把这个理论值吃个七七八八,再想往上走,就必须改变“一个 token 读一遍全部参数”这个前提。

投机解码的巧妙之处就在于,它每次前向计算能同时验证多个候选 token,把“读一遍参数”的成本摊到好几个 token 身上。这相当于不是硬碰硬提升带宽利用率,而是从算法层面减少了前向计算的次数。

2. 投机解码原理:一次批量验证让“从 14 到 159”成为可能

2.1 草稿模型与大模型的分工

投机解码的思路听起来有点“偷懒”,但实际非常优雅。它引入了第二个小模型,通常叫草稿模型(draft model),让这个小模型先快速生成一串候选 token,然后交给大模型一次性验证。

打个比方:大模型是个审批严格的领导,所有文件都要他亲自逐字审核,所以他一天只能处理 100 份文件。投机解码相当于先派一个实习生把文件初稿写好,领导只需要批量扫一眼,确认没问题就全部签收。如果实习生写得足够好,领导一次能签收好几份文件,效率自然上去了。

在 Qwen3.8-27B 的场景里,草稿模型我用的是同系列的 0.6B 和 1.5B 小模型。它们体积小、生成速度快,但知识量和表达质量远不如 27B 主模型。投机解码赌的就是一件事:27B 模型会预测什么,小模型大概率也猜得到,至少在常用词、常见句式、简单逻辑上如此。

2.2 一个公式看懂收益

投机解码的加速效果可以用一个简化的期望收益公式来理解:

每个验证周期产出的 token 数 ≈ r × (d + 1) + (1 - r) × 1

其中 r 是接受率,也就是草稿模型的候选 token 被大模型认可的占比,d 是草稿长度,也就是小模型一次生成的候选 token 数量。

如果草稿模型和大模型完全是“一个模子刻出来的”,r 接近 1,那么一次验证就能产出 d+1 个 token,加速比接近 d+1 倍。如果接受率只有 0.5,草稿长度是 8,那么一次验证大概产出 0.5×9 + 0.5×1 = 5 个 token,仍然比普通解码的 1 个 token 快得多。

这里需要澄清一个常见误解:即使草稿 token 被拒绝,也没有白算。因为大模型在验证的时候本来就要做一个完整前向计算,被拒绝的第一个草稿 token 会被大模型自己生成的 token 替换掉,后面被拒的草稿 token 则直接丢弃。换句话说,最差情况退化回普通逐 token 生成,只多付出了一点草稿模型的计算成本。

2.3 草稿模型怎么选才不白费功夫

草稿模型的选择是整个投机解码里最关键的决策,没有之一。我实测下来,选草稿模型要看三点:

一是 tokenizer 要和主模型一致。这几乎是硬性要求。大模型和小模型如果词表不一样,候选 token 根本对不上,接受率会惨到一个令人崩溃的水平。

二是模型同源。同系列的小模型是在同样的数据分布、同样的训练框架下训练出来的,预测习惯和大模型更接近。我用同系列 0.6B 模型,对话任务初始接受率大约在 0.62-0.73,换成同系列 1.5B 之后,接受率能到 0.83 左右。

三是草稿模型不是越大越好。1.5B 接受率高,但生成草稿本身也要时间。草稿生成太慢,会把验证省下的时间又吃回去。我测试下来,0.6B 在 24GB 显卡上草稿生成几乎无感,1.5B 会稍微拉长周期,但整体收益仍然更高,因为接受率的提升幅度覆盖了草稿成本的增加。

3. 从 14 到 159 的四步调优实操

3.1 第一步:Q4_K_M 量化,先白赚一倍性能

先把最没技术含量的一步做掉:量化。这条建议给所有刚开始碰本地推理的人,不要一上来就追求原始精度,对 27B 这个体量来说,Q4_K_M 是一个效果和速度都比较平衡的选择。

我用的是 llama.cpp 这套生态里常见的命令行工具,不同版本的参数名略有差异,但核心逻辑一致。下面是跑通基线量化版本的参考命令:

llama-cli \ -m ./models/qwen3.8-27b.Q4_K_M.gguf \ -p "写一段冒泡排序的 Python 代码" \ -n 512 \ --temp 0.7 --top-k 40 --top-p 0.9

实测这一档的速度大约在 31 tok/s 上下。相比 FP16 混合加载时的 14 tok/s,量化这一步差不多白赚了一倍多,而且模型体积从 54GB 降到 15GB 左右,终于能完全塞进显存了。这步做完,显存还剩大约 7-8GB 富余,为后面塞草稿模型留下了空间。

3.2 第二步:在 llama.cpp 兼容后端开启投机解码

量化只是热身,真正的大头是投机解码。在 llama.cpp 系命令行工具里,开启方式非常直接,用草稿模型参数指定小模型路径即可:

llama-cli \ -m ./models/qwen3.8-27b.Q4_K_M.gguf \ --model-draft ./models/qwen3.8-0.6b.Q4_K_M.gguf \ --draft-max 8 --draft-min 4 \ -p "写一段冒泡排序的 Python 代码" \ -n 512 \ --temp 0.7 --top-k 40 --top-p 0.9

我第一轮跑出来的结果是 78-92 tok/s。相比量化的 31 tok/s,又是一个接近三倍的跳升。这个结果在当时已经让我觉得“本地 27B 能用了”,但还没到满意的程度。注意这里的关键参数是--draft-max和--draft-min,它们控制草稿长度的动态调整范围。草稿太长,后半段接受率会下降,白白浪费时间;草稿太短,一次验证的收益又不够。系统会在给定的范围内根据最近接受率自动调整。

3.3 第三步:调 batch size、ubatch 与 flash attention

速度到了 80 多,我开始琢磨还能从哪里榨性能。这批调优里收获最大的是批大小相关参数。

投机解码的验证阶段,会把草稿模型产出的 d+1 个 token 组成一个 batch 一次性前向计算。batch 越大,GPU 的并行度越高,单位时间内能验证的 token 越多。默认值通常偏保守,手动调高之后效果明显。我用的参数大致是:

llama-cli \ -m ./models/qwen3.8-27b.Q4_K_M.gguf \ --model-draft ./models/qwen3.8-1.5b.Q4_K_M.gguf \ --draft-max 8 --draft-min 4 \ --batch-size 2048 --ubatch-size 512 \ --flash-attn \ -p "写一段冒泡排序的 Python 代码" \ -n 512 \ --temp 0.7 --top-k 40 --top-p 0.9

这一步后速度从 85 左右提到 109-113 tok/s。其中--flash-attn贡献了大约 7-10 个点,batch 调整贡献了 20 个点上下。

这里有个经验之谈:batch 不是越大越好。我在 2048 的基础上试过 4096,速度几乎没变化,显存占用反而明显上升,在长上下文的场景下还可能挤占 KV cache 的空间。如果你显存只有 16GB 甚至更低,建议 batch 从 1024 开始试。

3.4 第四步:按任务类型压采样器和草稿长度

到这一步,速度稳定在 110 左右,但离 159 还有距离。最后的提升来自对不同任务类型的差异化调参。

先看对话类任务。对话的句子结构相对常规,草稿模型命中率高,草稿长度可以放宽到 8-10。我重新切回 1.5B 草稿模型,上下文长度设置为 4096,温度保持 0.7,对话生成速度实测 141 tok/s。

再看代码类任务。代码的格式、命名、逻辑分支比自然语言更“个性化”,小模型命中率明显下降。我把草稿长度从 8 降到 5,温度从 0.7 压到 0.2,并且关闭了重复惩罚相关选项。这一步对接受率的提升非常明显,最终在代码生成场景下跑出了全流程最高值:159 tok/s。

为什么采样器会影响接受率?投机解码的接受逻辑是看草稿模型采样的 token 是否落在主模型的条件概率分布里。当两个模型用相同的采样器、相同的温度时,它们的分布更接近,接受率自然更高。如果主模型开着重复惩罚,草稿模型没有,两者预测的分布就会产生系统性偏差,接受率会掉得很难看。

四步走完,同一份 Qwen3.8-27B 的性能变化大概是这样:

阶段配置要点实测速度
初始基线FP16 混合加载14 tok/s
第一步Q4_K_M 量化31 tok/s
第二步开启投机解码(0.6B 草稿)78-92 tok/s
第三步batch 2048 + flash attention109-113 tok/s
第四步任务级调采样与草稿长度141-159 tok/s

4. 投机解码翻车实录:这些坑会让速度不升反降

调试到 159 的过程并不顺利,中间踩了不少坑。如果把投机解码理解成“换个模型加个参数就能提速”,那很可能会得到反面结果。下面这几个问题我一个个说。

4.1 草稿模型 tokenizer 不一致,接受率直接崩

我第一次尝试投机解码时,手头恰好有一个其他系列的小模型,想着“反正都是小模型,应该差不多吧”,结果跑出来的速度甚至不如不开投机解码。查看日志发现接受率只有不到 0.2。

问题根源就是 tokenizer 不一致。草稿模型的分词方式和大模型对不上,草稿生成的 token 序列在词表里根本没法和大模型的预测对齐,验证几乎全被驳回。这种情况下投机解码退化成了“草稿模型白算一遍 + 大模型逐 token 生成”,速度自然倒挂。

后来我换回同系列的小模型,接受率立刻回到 0.6 以上。这条经验送给所有想省钱省事的人:投机解码的草稿模型,第一优先级不是“小”,而是“同源”。

4.2 显存不足时,草稿模型成了 CPU 拖油瓶

有一段时间我把上下文长度拉高到 8192,发现速度突然掉回 60 多。排查后发现,主模型加上 KV cache 把显存占得差不多了,1.5B 草稿模型被挤到了 CPU 内存里跑。

草稿模型跑在 CPU 上的速度比 GPU 慢一个数量级,本来它应该是“飞快地产出候选 token”,结果变成了整个流程里最慢的一环。调试时最明显的特征是显存占用接近上限,草稿模型那部分加载速度异常。

解决方案有两个方向:一是把草稿模型换小一号,0.6B 无论如何都能塞进显存;二是降低上下文长度,给草稿模型留出空间。我的标准是主模型和草稿模型都必须在显存里,任何一方被挤到 CPU 都是负优化。

4.3 采样器不统一,主模型和草稿模型各说各话

这个坑比较隐蔽。我之前习惯给主模型开一组比较复杂的采样参数,比如温度 0.8、top-k 50、top-p 0.95、重复惩罚 1.1,草稿模型则保持默认。结果接受率比预期低一大截。

道理前面提过,两个模型用不同的采样方式,预测分布就会系统性偏移。投机解码的验证逻辑本质上是看草稿模型的输出能不能被主模型“认账”,如果主模型的采样习惯和小模型差异太大,哪怕模型本身同源,也会因为参数不一致而互相不认。

我的建议是让两个模型使用相同的基础采样参数。我自己最终用的是 top-k 40 + top-p 0.9,温度按任务区分。代码任务额外关掉重复惩罚,因为这个惩罚项对代码格式的干扰比对自然语言大。

4.4 KV cache 增长后,batch 和显存需要重新平衡

投机解码在短上下文下表现亮眼,但长上下文生成时会出现新的矛盾。上下文越长,KV cache 占的显存越多,留给推理 batch 的空间就越少。batch 被迫降低,验证阶段的并行度下降,速度自然回落。

在 8192 上下文的连续对话测试里,同样是投机解码,开头和结尾的速度能差出 30%。这是 KV cache 增长导致的,不是模型本身变慢了。如果你有长文本处理需求,建议提前给 KV cache 留出余量,或者适当降低 batch-size,用显存换上下文长度。

4.5 不同任务到底差多少

最后放一组不同任务的实测对照,帮你对“投机解码在不同场景下的收益”有个预期:

任务类型草稿长度温度接受率实测速度
对话问答80.70.85141 tok/s
代码生成50.20.68159 tok/s
中英翻译60.50.74128 tok/s
长文本摘要60.40.71122 tok/s

代码生成反而是最高速的,这有点反直觉。原因在于我把温度压到了 0.2,采样集中在最高概率的 token 上,草稿模型和主模型的分布高度一致,接受率虽然不如对话高,但单次验证的“含金量”更大。翻译任务的词汇选择灵活性更高,小模型和大模型经常在措辞上有分歧,所以接受率反而不高。

5. 日常使用配置参考与下一步扩展

5.1 稳定挂机推荐配置

调优完成后,我最常用的稳定配置是这样一组:

llama-cli \ -m ./models/qwen3.8-27b.Q4_K_M.gguf \ --model-draft ./models/qwen3.8-1.5b.Q4_K_M.gguf \ --draft-max 8 --draft-min 4 \ --batch-size 2048 --ubatch-size 512 \ --flash-attn \ --ctx-size 4096 \ --temp 0.5 \ --top-k 40 --top-p 0.9 \ --no-display-prompt

这套配置在混合负载下的平均速度大约在 105-120 tok/s,覆盖对话、代码、文档总结等常见场景。如果你主要跑代码,把--draft-max改成 5,--temp改成 0.2,就能接近文中的峰值表现。

长时间挂着跑还有个容易忽略的问题:GPU 温度。投机解码开启后 GPU 占用率一直很高,显存散热跟不上的话,频率会主动下降,速度能掉 10-15%。跑超过半小时的生成任务,我习惯顺手看一下监控,给机器留好散热空间。

5.2 封装工具的参数对照思路

如果你用的是 Ollama 这类封装好的工具,不需要重新编译命令行程序,但底层原理完全一样。无非是把:

  • 草稿模型路径换成 Ollama 里的模型名或draft_model配置
  • 上下文长度对应到类似num_ctx的参数
  • batch 对应到类似num_batch的参数

强烈建议先看懂底层后端的日志输出,很多封装工具会把草稿接受率打印出来。接受率一眼就能看出配置合不合理,低于 0.5 基本可以断定是选型或采样器出了问题。

5.3 还想更快:后续可以试的方向

159 不是终点,至少还有几个方向可以继续压性能:

一个是更激进的量化组合。主模型用 Q4_K_M,草稿模型可以尝试 Q8 甚至不量化。草稿模型体积小,精度高一点能让它的预测和大模型更接近,代价是草稿速度略降,具体取舍要看实测。

另一个是多级投机解码。用一个超小的模型生成草稿,再用一个中等模型二次修正,最后交给 27B 模型验证。理论上能提高最终接受率,但对显存和内存的压力更大,24GB 显卡跑这套组合会比较吃力。

还有就是把缓存和投机解码结合。对高频重复的开头、常用代码模板做 prompt caching,让草稿模型在长上下文里更快进入状态。这个我在长文档摘要场景里试过,效果不错,能让开头那段慢速期明显缩短。

我自己实际体验下来,投机解码这套组合拳最有价值的地方不是那个 159 的峰值数字,而是让 27B 级别模型在本地跑出了“几乎不需要等”的交互感。最后的建议是:不要执着于把每一个参数都调到极限,先用 Q4 + 同源草稿模型 + 默认 batch 跑通,再按本文的方法逐项加码,你的显卡、系统、任务类型不同,最优参数组合一定和我有差异,照着数据自己调一遍,比抄任何一份现成配置都管用。

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

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

立即咨询