☰
27B模型本地推理提速:从14到159 tok/s的投机解码调优指南
2026/10/19 3:22:03 网站建设 项目流程

本地跑 27B 模型,大多数人遇到的第一关不是显存,而是慢。我第一次把 Qwen 系一款 27B 开源对话模型(后面统一叫 Qwen3.8-27B)的量化版部署到一台 24GB 显存的机器上,测速结果只有 14 tok/s。屏幕上的字真是一个一个往外蹦,做一轮稍长的总结任务,光标要停好几秒才动一下。当时我第一反应是:是不是显卡不够大,得上更大显存的卡?后来花了两周时间调参数、换方案、抠实现细节,才明白这个 14 tok/s 里面其实有大量“冤枉慢”,而真正剩下的一部分瓶颈,又被投机解码(speculative decoding)从架构层面绕了过去。最终这台机器的速度稳定在 159 tok/s,前后相差约 11 倍。这篇文章就记录我从 14 到 159 的完整调优路径,适合所有想本地跑 15B 以上模型、又不想为了速度去买新卡的人。

1. 14 tok/s 的真相:27B 模型的瓶颈从来不是算力

1.1 自回归解码为什么这么慢

所谓解码就是模型在已有 token 序列后面预测下一个 token。27B 模型每生成一个新 token,都要把当前输入序列和已经生成的历史 token 一起做一次完整的前向计算:先读一遍全部权重,做 embedding、attention、多层 MLP,最后在词表上算一个概率分布。这里最要命的是“串行”这两个字——下一个 token 的计算依赖前一个 token 的生成结果,不是提前能算好的。

你确实可以并行矩阵内部的计算,但 27B 这个量级的模型,矩阵运算很快,真正拖后腿的是每次都把几百亿参数从显存搬到计算单元。这个搬运成本跟算力没什么关系,它只跟显存/内存带宽有关,业界管这种情况叫 memory-bound(带宽受限)。简单类比:你要计算一个结果,但每次都得先把一整个书架的书搬过来,翻一页,再把书还回去。你翻页的速度很快,搬书的过程才是大头。

1.2 我最初的部署配置与“假慢”

我最初的部署配置非常典型:27B 模型的 int4 GGUF 量化版,权重大约 16GB;一张 24GB 显存的显卡;推理框架用本地最常见的 llama.cpp 系列;上下文长度拉到 32768;GPU 层数没有仔细调,一部分层留在 CPU;进程还开着完整运行日志。结果跑起来就是 14 tok/s。

项目初始配置说明
模型Qwen3.8-27B Q4_K_Mint4 量化,约 16GB 权重
上下文32768KV cache 占显存很大
GPU 层数仅部分层放 GPU其余层 CPU 计算
日志/预加载默认数据读取有抖动
实测速度14 tok/s远低于理论值

为什么会这么慢?核心问题是:只要有一层权重不在显存里,每生成一个 token,CPU 内存里那部分权重就要通过 PCIe 搬到显卡。PCIe 的带宽一般只有 30~60GB/s,而显存带宽普遍在 700GB/s 以上,差一个数量级。所以你的显卡再好,也架不住每个 token 都在走 PCIe。这本质上不是算力不够,而是数据放错了地方。

1.3 带宽受限模型算一下理论上限

先算一个比较乐观的上下文。27B int4 权重约 16GB,24GB 显卡显存带宽按 700GB/s 量级估算,每生成一个 token 至少要读一遍全部权重,理论单 token 耗时大约是 16GB / 700GB/s ≈ 23ms,换算成速度就是 43 tok/s 左右。如果你的显卡带宽在 1TB/s 附近,理论上限大约 60 tok/s。

这段话有两个含义。第一,14 tok/s 远没到硬件理论值,大概率是部署配置把带宽浪费在搬运和等待上;第二,哪怕配置调到完美,不用投机解码时纯自回归上限也就 45~60 tok/s。想在本地把 27B 跑到 150 以上,靠堆算力是堆不出来的,必须从算法层面减少“大模型前向”的次数。后面你会看到,投机解码正是冲着这一点去的。

2. 投机解码的核心思想:用草稿模型给大模型“代笔”,大模型只做审核

2.1 一次验证一串 token 的运行机制

投机解码的思路可以一句话概括:让一个又快又小的大模型“替身”先草拟一串候选 token,然后让真正的大模型一次性验证这一串,而不是一次只验证一个。

具体流程是三步:

  1. 草稿模型(draft model)先用自回归方式生成 k 个候选 token。
  2. 把这些候选 token 拼进原序列,大模型只做一次前向,同时算出这 k 个位置各自的概率分布。
  3. 逐个位置对比:如果草稿模型的 top 候选与大模型算出来的一致,就接受;一旦遇到不一致的位置,就停止接受,并且用大模型在该位置的预测作为真实 token,然后进入下一轮。

这里有个容易忽略的细节:就算草稿序列全错了,大模型至少能产出自己预测的那个 token,所以投机解码最差情况下每一轮也不会一个 token 都没生成,只是多花了草稿的 k 次前向而已。因此它天然有兜底。

为什么这样能快?因为大模型一次前向的耗时,几乎跟验证 1 个 token 还是验证 7 个 token 没区别——反正都要把全部权重读一遍。原本需要做 k 次昂贵前向,现在变成 1 次昂贵前向加 k 次便宜前向。只要小模型的每次前向足够便宜,总账就是赚的。

2.2 接受率:决定加速倍率的第一变量

草稿的 k 个 token 并不全都会被接受,真正被采纳的 token 个数才是收益。我们把每一轮平均采纳的 token 数记作 E[L]。如果 E[L] 是 3,意味着大模型每验证一次,平均能产出 3 个真实 token,相当于原本的 3 次大模型前向被压成了 1 次。

接受率通常受三个因素影响:草稿模型和目标模型的概率分布是否接近;正在生成的文本类型;采样温度。同族的 0.5B 小模型给 27B 大模型草稿,前几个位置接受率常常能到 0.7 以上;但越是后面的位置,分布分歧越明显,接受率迅速下滑。这也是投机解码的加速倍数远到不了 k 的原因。

我刚开始也天真地以为“推测 7 个就能一次生成 7 个”,实测发现平均接受长度能到 4~5 个已经算是非常理想的状态。你真正能赚到的,是把每次大模型前向的产出从 1 个 token 提到 4~5 个 token。

2.3 草稿模型选同族,比选名字好听更重要

草稿模型不是随便拉一个小模型就行。最好的草稿模型是“同族、同词表”的小版本。同一个基座家族里,tokenizer 一致、训练数据相似,小模型对大模型行为的模仿能力最强。

我在调参时对比过同族 1.5B/0.5B 模型和另一个词表差异很大的 1B 模型。那个词表不兼容的 1B 模型,接受率不到 0.3,端到端速度反而不如不开投机。这一点很多人容易忽略——选草稿模型优先看 tokenizer 和训练对齐度,而不是看参数大小。

草稿模型也不是越小越好。太小了概率分布偏差大,接受率低;太大了草稿前向耗时增加,读权重流量变大。所以需要试“0.5B / 1.5B / 2B”几档,结合实测选择。我的最终选择是 0.5B int4 版本,理由会在下一章的实测数据里展开。

3. 从 14 到 159 的调优实测记录

3.1 先清掉配置里的“冤枉慢”:从 14 到 45

我做的第一件事不是立刻上投机解码,而是把这个 14 tok/s 的基线变得正常。主要做了四步:

  • 上下文长度从 32768 砍到 8192。KV cache 的大小和上下文长度近似线性关系,砍到 8K 后显存腾出来不少。
  • GPU 层数调到能放下的最大值,最终把 39 层全部放上 GPU,不再让任何一层跑在 CPU。
  • 开启预加载,并关掉日志输出。日志在长时间会话里会产生大量 I/O,关掉之后明显更顺。
  • CPU 线程数按实际核心调整,别开太少也别开太多,太多会导致线程切换开销过大。

这几步做完,速度直接变成 45 tok/s。一开始我也有点发懵:原来 14 到 45 之间没有增加任何算力,只是让权重待在它该待的地方。但注意,45 已经接近前面算的带宽理论值了,这时再想往上走,必须换个思路。

注意:如果你测出来的速度远低于理论值,别急着怪显卡,先查权重是否全部在显存里。很多时候“慢”不是算力不够,而是数据搬运路径出了大问题。

3.2 投机解码第一版:96 tok/s,离目标还很远

我用的是 llama.cpp 系列框架自带的投机解码支持。草稿模型先选了同族的 1.5B int8 版本,推测 token 数(draft-max)设为 5。启动参数大概是这样的,不同版本参数名略有差异,以你的编译版本 help 为准:

llama-server -m Qwen3.8-27B-Q4_K_M.gguf \ -ngl 39 -c 8192 \ --draft Qwen-1.5B-Instruct-Q8_0.gguf \ --draft-max 5 \ --draft-ngl 39 \ --temp 0.6 --top-p 0.8

实测 96 tok/s,从 45 一下跳到 96,翻了一倍还多。但离 159 还有距离。验算一下:目标模型单 token 大约 22ms,草稿大约 3ms,一轮循环耗时约 22 + 5*3 = 37ms,每轮平均接受 3.6 个 token,产率就是 3.6 / 0.037 ≈ 97 tok/s,跟实测几乎吻合。

3.6/5 = 0.72 的接受率其实不差,问题出在草稿模型太“重”。1.5B int8 权重有 1.5GB,跑 5 步就是 7.5GB 的显存读取流量,在总账里占了不小份额。

3.3 换草稿、调 gamma、压量化,逐步摸到 159

第一项优化,草稿模型从 1.5B 换成 0.5B int8。花在草稿上的时间几乎减半,但接受率也降了,每一轮平均接受 3.2 个 token 左右。算一下总账:0.5B 草稿每步约 1.4ms,一轮耗时 22 + 5*1.4 = 29ms,产率 3.2 / 0.029 ≈ 110 tok/s,实测 114 tok/s。比 96 高,说明草稿越轻,哪怕接受率差一点,仍然划算。

第二项优化,把推测长度从 5 提到 7。gamma 变大后,每轮草稿耗时线性增加,但 E[L] 也在涨,从 3.2 涨到约 4.4。0.5B 草稿每步按 1.2ms 算,一轮耗时 22 + 7*1.2 = 30.4ms,产率 4.4 / 0.0304 ≈ 145 tok/s,实测 139。有效的原因是草稿模型足够小,7 步也只多了 8~9ms;如果是 1.5B 草稿,同样的 gamma 上涨会把草稿耗时拉太高,收益会被吃掉。

第三项优化,把草稿模型从 int8 压到 int4。0.5B 的 int4 权重只有 0.25GB 左右,草稿单步进一步降到 1.1ms 上下;接受率几乎没有明显下降。再把线程调度、预加载、采样参数微调一遍,最终配置下每轮平均接受约 4.8 个 token,循环耗时 22 + 7*1.1 ≈ 29.7ms,产率 4.8 / 0.0297 ≈ 162 tok/s,实测 159。考虑到采样器、日志、线程调度的开销,两个数字已经非常接近。

阶段关键动作平均接受长度 E[L]循环耗时估算实测 tok/s
初始默认部分层 CPU、32K 上下文——14
清理配置全量 GPU、8K 上下文、关日志——45
投机 v1draft=1.5B-int8, gamma=53.6约 37ms96
投机 v2draft=0.5B-int8, gamma=53.2约 29ms114
投机 v3draft=0.5B-int4, gamma=74.4约 30ms139
投机 v4线程/预载/采样微调4.8约 30ms159

3.4 最终配置与复现命令

最终跑得最稳的配置如下。命令仅供参考,不同版本参数名可能不同,建议先跑一次--help确认。

llama-server -m Qwen3.8-27B-Q4_K_M.gguf \ -ngl 39 -c 8192 \ --mlock \ --threads 16 \ --draft Qwen-0.5B-Instruct-Q4_K_M.gguf \ --draft-ngl 39 \ --draft-max 7 \ --draft-min 4 \ --temp 0.6 --top-p 0.8 \ --seed 42

这里有两个参数值得说明。--draft-min 4的意思是,当接受长度偏低时,下一轮自动减少推测 token 数,避免在文本困难地段白跑太多草稿。--mlock把权重锁定在内存里,防止系统把权重换出到磁盘,长时间运行时更稳定。我在实测中发现,这两项对速度稳定性很有帮助,尤其是连续跑长文本时。

4. gamma 取多大?一个公式和一组实测曲线

4.1 简化加速模型:S = E[L] / (1 + k*r)

先给一个简化模型。设目标模型单次解码耗时 T_target,草稿模型单步生成耗时 T_draft,每轮推测 k 个 token。投机解码一轮的总耗时是 T_target + k * T_draft,这一轮能产出 E[L] 个 token。所以投机解码的等效每 token 耗时是 (T_target + k*T_draft) / E[L]。原始每 token 耗时是 T_target。加速比 S 就是:

S = E[L] / (1 + k*r),其中 r = T_draft / T_target。

这个公式第一次看可能有点绕,拿最终配置代入:T_target = 22ms,T_draft = 1.1ms,r ≈ 0.05,k = 7,E[L] = 4.8,S = 4.8 / (1 + 7*0.05) = 4.8 / 1.35 ≈ 3.56,对应 45 * 3.56 ≈ 160 tok/s,和实测 159 几乎一致。

r 是整篇文章最核心的一个比值。它决定了你的草稿模型够不够快,也决定了投机解码的上限。很多人调投机解码只看 gamma,不看 r,这是本末倒置。

4.2 为什么 k 不是越大越好

当 k 增大时,分母 1 + k*r 线性变大,E[L] 也在增大但会趋近于某个上限。以我的配置为例,k=5 时 E[L]≈3.4,k=7 时 E[L]≈4.8,k=9 时 E[L]≈5.3。k 从 5 到 7,产率从 3.4/1.25 = 2.72 提升到 4.8/1.35 = 3.56;k 到 9 时,产率是 5.3/1.45 ≈ 3.66,几乎不涨了。

但如果草稿模型更快,比如 r = 0.02,k=9 时 S = 5.3/1.18 ≈ 4.5,就还有上行空间。所以什么时候该加 k,取决于草稿模型有多快。

实操上我推荐直接跑 5、7、9 三档,盯住 tok/s 选峰值,不要只看接受率。接受率高但草稿慢一样没用,接受率低但草稿飞快也可能划算。

4.3 不同任务下的接受长度波动

我在代码、中文问答、长文档摘要三类任务上分别测过同一个配置。中文问答和摘要类任务接受长度最高,E[L] 能到 5 左右;代码类任务里,函数名、括号嵌套、长 token 的标识符部分是重灾区,E[L] 经常掉到 3 左右。所以如果你拿这个配置来写代码,别指望平均 159,体感可能只有 100 上下;如果你拿它当对话助手、做总结,159 是实打实的。

temperature 也会影响 E[L]。开到 0.9 时,大模型分布变得平滑,草稿模型的自信预测经常被推翻,E[L] 可能降到 3 出头。想又快又保持多样性,我的经验是把温度控制在 0.6~0.7,top_p 别超过 0.9。

提示:投机解码对首 token 没有加速效果,因为第一个 token 之前没有草稿序列。如果你很在意首 token 延迟,这个技术救不了你。

5. 投机解码不是银弹:我在调优中踩过的五个坑

5.1 temperature=0 时一切正常,开到 0.8 就翻车

我最早只在 temperature=0 下测,速度很快;后来把 temperature 调到 0.8,发现同样配置速度掉了一截,而且生成内容跟普通采样对不上。查了源码才明白,有的实现为了省事,只在大模型预测的 top-1 与草稿模型 top-1 完全一致时才接受,这相当于把采样过程偷偷换成了贪心,速度是快了,分布不对。

完整的投机解码应当支持带修正的拒绝采样,需要在草稿阶段保留 logits,再和大模型 logits 做比较。如果你发现开投机解码之后生成多样性明显变差,大概率就是遇到了只做贪心验证的实现。先确认你用的框架支持采样模式的投机解码,再把温度调上去。

5.2 显存利用率超过 90% 反而不如不开

投机解码需要同时放两个模型,主模型和草稿模型。如果主模型加 KV cache 已经吃掉 90% 以上的显存,再放草稿模型会导致频繁的显存缓存重排和页面调度。

我中间做过一次实验:把草稿模型和主模型都塞进 GPU,但 KV cache 只剩很少空间,结果运行时速度掉到 30 tok/s 左右,比不开投机还慢。后来把上下文从 32K 砍到 8K,腾出显存给草稿模型,问题才彻底解决。显卡显存利用率最好不要超过 90%,否则投机解码的收益会被显存管理开销吃掉。

5.3 结构化输出会让投机解码悄悄失效

用 grammar 约束输出 JSON 时,草稿 token 必须也按 grammar 采样,否则草稿大概率被拒绝,甚至触发崩溃。我测试的 llama.cpp 某个版本里,grammar 与投机解码并存时会自动禁用投机逻辑,输出语法还是对的,但速度直接回到非投机档。

我的建议很直接:需要结构化输出时先关投机,或者先确认框架同时支持 grammar 和 speculative decoding 再启用。不要期待一条命令解决所有问题。

5.4 CPU offload 场景下投机解码收益极低

如果你的 GPU 显存不够,主模型有部分层在 CPU,那么每轮验证主模型都要经过 PCIe 多次搬运,本身就慢;草稿模型再快也难补。

我测试过 ngl=24 层 GPU、其余 CPU 的场景,开投机后速度从 8 tok/s 提到 14 tok/s,提升远没有显存充足时那么大。投机解码适合显存基本够用、主模型基本全在 GPU 的情况。显存不够时,优先降量化精度或缩上下文,先把主模型全塞进显存,再考虑投机解码。

5.5 不同推理框架的投机实现差异很大

llama.cpp、vLLM、SGLang 都支持投机解码,但参数语义和性能差异很大。vLLM 的投机解码更偏服务端 batch 场景,单卡单请求反而可能因为调度开销拿不到最佳收益;llama.cpp 的本地单用户体验更直接。

我建议先在一个框架里跑通流程,记录下来 E[L] 和 tok/s,再换另一个框架比较。不要直接照搬别人的参数,因为框架底层对草稿序列的 KV cache 复用、采样管线、并行调度实现都不同,最终参数可能差很多。

6. 质量是否缩水?159 tok/s 的稳定性与适用场景

6.1 无损验证:固定 seed 对比开/关投机

为了确认 159 不是牺牲质量换来的,我做了两次对比。第一次,temperature=0,固定 seed,开投机和不开投机各生成 200 个 token,逐字对比,结果完全一致。第二次,temperature=0.7,固定 seed,对比两条输出的平均 logprob,差异在噪声范围内。

这印证了一个结论:只要框架实现正确,投机解码在贪心模式下是数学无损的;采样模式下如果用了严格的拒绝采样,同样无损。如果你发现开投机之后回答质量下降,优先怀疑采样实现,而不是投机解码本身。

6.2 159 tok/s 是什么体验,适合哪些任务

159 tok/s 换算一下,大约每秒 160 个 token,对话中几乎感觉不到等待。做批量短文本生成、改写润色、摘要,效率提升非常明显。代码补全也能用,但代码局部接受率低,体感会打折扣。长上下文场景中 KV cache 和显存相互挤压,我会选择先关投机换更长上下文,这两者在本地部署中往往是互斥的。

如果你主要是单用户交互式使用,投机解码带来的收益非常直观。如果你要做多用户并发 API,batch 之后投机解码的计算优势会被并行批次稀释,加速比会明显缩水。不同使用场景,策略完全不同。

6.3 还想更快?三个后续方向

一是换更对齐的草稿模型。如果有条件,用大模型中间 checkpoint 蒸馏一个专门的小模型出来当草稿,接受率还能再上一截。二是试 multi-draft 并行预测,多个草稿模型竞争,复杂度和显存开销都高,但上限更高。三是做 kernel 级优化,把草稿模型和大模型放进同一个执行图里,进一步省调度开销。这三条路都超出普通配置调优的范畴,对大多数人说,把前面几步抄一遍,拿到 150+ tok/s 已经非常够用。

最后聊点个人感受。投机解码不是什么玄学,本质就是拿便宜的算力去“预判”贵的算力,再用一次验证把多步合成一步。它之所以在 27B 这种模型上收益巨大,恰恰是因为大模型前向严重带宽受限——验证一串 token 和验证一个 token 的代价几乎一样。如果你也想折腾自己的本地模型,我的建议是先别急着换显卡,先把 14 变成 45,再给一个同族小模型当草稿,可能你手上那台机器的体验就完全不一样了。

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

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

立即咨询