Qwen3.8-27B 这个规模的模型拿到本地跑,第一关不是装环境,而是对抗"卡顿”。我手上这台机器只有 24GB 显存,最初用 Q4_K_M 量化、8K 上下文、默认采样参数,关闭一切投机解码相关功能,实测 Qwen3.8-27B 的 decode 速度稳定在 14 tok/s。这个速度你说完全不能用吧,代码补全凑合;但一旦进入多轮对话或者长文续写,几乎每个字都像从硬盘里抠出来的一样。后面我把投机解码完整地调了一轮,同一个模型、同一块卡、同一套上下文,日常混合任务等效速度到了 70~90 tok/s,代码补全这类高重复度场景更是冲到了 159 tok/s,等于把基线拉高了整整 10 倍以上。这篇就把我从 14 到 159 的完整过程、参数背后为什么有效、以及哪些坑让我白忙了两天,一次性讲清楚。
这篇内容适合三种人:一是已经在本地跑 30B 以下量化模型但嫌慢的人;二是准备用投机解码却不知道草稿模型怎么选、参数怎么定的人;三是纯粹想搞明白"本地推理为什么吃不满显卡"的玩家。投机解码不是魔法,它只是把 GPU 发呆的时间拿来做预判,但要想把这个预判变成稳定收益,你得先理解它解决的是什么问题。
1. 跑 27B 之前,先把环境底子和"慢在哪"搞清楚
1.1 能跑起来的前提:24GB 显存必须走上量化
27B 参数,按常见 fp16 格式计算,权重就要 54GB,24GB 显存连影子都摸不着。所以第一步就是量化,我这边用的是社区通用的 GGUF 量化打包格式。量化格式我最后选了 Q4_K_M,理由很直接:27B 模型 Q4 之后权重接近 14.9GB,刚好能在 24GB 卡上留出 KV cache 和草稿模型的空间;Q8 大约 27GB 直接装不下,Q5_K_M 要 16.8GB 也能跑但权重读取量更大,decode 速度反而更低。
我测试机的配置大概是这样的:CPU 是常见的 8 核 16 线程消费级处理器,内存 64GB DDR5,显卡是 24GB GDDR6X 显存的当代旗舰卡(Ada 架构)。操作系统用的 Linux 发行版,推理框架是社区最主流的那个 C++ 本地推理工具。这里必须提醒一句:如果你只有 16GB 显存,27B Q4 会比较紧张,建议把上下文长度压到 4K 以下,或者改用更小一点点的量化格式。
下面是显存估算表,方便你按自己的卡去算账:
| 模型文件格式 | 估算占用 | 能否在 24GB 卡运行 |
|---|---|---|
| fp16 | 约 54GB | 不能 |
| Q8_0 | 约 27GB | 不能,超出 |
| Q5_K_M | 约 16.8GB | 能,但显存余量少 |
| Q4_K_M | 约 14.9GB | 能,推荐 |
| IQ4_XS | 约 13.6GB | 能,残差结构稍有不同 |
我第一次跑的时候把上下文拉到 8K,KV cache 用默认 fp16,模型加缓存加 CUDA context 占用大约 22.8GB,差点把卡吃满。后来把 KV cache 切成 q8_0,上下文降到 4K,总占用回落到 19.2GB,才敢安心开始调投机解码。所以,24GB 跑 27B 不是不行,但显存规划必须从一开始就算好,否则后面开投机解码随时可能溢出。
1.2 baseline:无投机时 14 tok/s 是怎么测出来的
调优之前先做一次干净的基线测试。我用一条 500 token 的输入,生成 512 token,温度 0.6,top-p 0.9,repeat penalty 1.1,上下文 8192,关闭投机解码相关选项。测了 3 次,结果分别在 13.9、14.4、14.7 tok/s 之间波动,我取了个整数 14 tok/s 当 baseline。
这个数字背后有个很有意思的现象:Q4_K_M 权重约 14.9GB,假设显卡显存带宽有 1TB/s 级,理论上每生成一个 token 把全部权重读一遍,也该有 60 tok/s 上下,但实际只有 14。为什么差这么多?核心原因有两个:一是自回归解码每一步都依赖上一步的输出,GPU 只能串行地一小步一小步走;二是 batch 太小,kernel 启动开销、显存读取效率、SM 利用率都没被填满,大部分时间 GPU 在"等数据"而不是"算数据"。
很多人以为本地推理慢就是显存带宽不够,其实带宽只是上限,真正卡住你的是解码阶段的串行依赖和小 batch 低利用率。这也是为什么投机解码能起作用:它不改变权重读取量,但它把一次只处理 1 个 token 变成了"草稿生成一批 + 大模型一次验证一批",真正把 GPU 的并行能力用起来。
1.3 投机解码到底偷了什么时间
用个生活类比:你让一位资深专家写一份报告,他习惯字斟句酌,每一句都要反复推敲,出活很慢。投机解码的思路是,你先让一个笔头很快的实习生写一版草稿,专家拿到草稿后不是逐字重写,而是快速扫一遍,觉得没问题的段落直接放行,有问题的再亲自改。这样专家纠正的成本,远远低于他从零慢慢写。
放到模型推理里,实习生就是一个小尺寸草稿模型,扫视检查就是大模型的一次前向计算。草稿模型一次生成 5 个 token 可能只要 8ms,27B 大模型验证一个 6-token 的 batch 大约 12ms,合起来 20ms 能产出 5 个合格的 token,等效速度就远远超过原来每 70ms 才出一个 token 的模式。当然这个账不能算得太理想,实际还会有拒绝重算的情况,但方向是对的。
2. 投机解码的关键变量:草稿模型、接受率与步长
2.1 草稿模型不是越强越好,选型要看"性价比"
投机解码的收益公式写粗一点就是:有效加速比约等于"每轮产出的有效 token 数 / (草稿阶段耗时 + 验证阶段耗时)"。草稿模型太大,生成草稿本身就很慢;草稿模型太小,猜中的概率低。对 27B 这种规模,我实测下来 0.6B 级别的量化草稿模型最合适,0.3B 的接受率会比较低,1.3B 的草稿阶段耗时又偏高,整体反而不划算。
怎么判断草稿模型"值不值"?我建议看一个比例:草稿模型跑通一版 token 的耗时,最好不要超过大模型验证一次耗时的 20%~30%。如果草稿本身就和大模型一样慢,那投机解码就变成了"两个模型轮流摸鱼",速度只会更难看。
草稿模型同样要量化,但这里有个容易翻车的地方:草稿模型不能为了省显存使劲压量化等级,因为草稿质量下降会直接拉低接受率。我最后用的草稿模型是 0.6B、Q4_K_M 量化,显存占用不到 1GB,足够轻量,效果也稳。
2.2 接受率和草稿步数:两个决定成败的数字
接受率(accept rate)指的是草稿模型生成的 token 里,被大模型验证通过的比例。如果接受率太低,大模型验证完发现前面猜错,还得自己重新生成一串,等于白跑一趟,这种情况下投机解码可能比不开还慢。我自己的经验是:接受率低于 0.6 基本没有收益,高于 0.85 才会比较舒服。
草稿步数(draft-max / speculate length)是每一轮尝试让草稿模型生成多少个 token。步数太短,一次验证摊薄的开销有限;步数太长,后面几个 token 接受率下降,还会拉长草稿阶段时间,收益递减。比较稳的起步值是 5,再往上探 8 和 12。我用 0.6B 草稿模型、greedy 采样时,draft-max=5 接受率约 0.88,draft-max=8 接受率降到 0.79,但单轮产出多了,等效速度反而上升;draft-max=12 时接受率掉到 0.68,草稿时间也变长,等效速度反而下跌。
再看一下三个常用参数的含义:
| 参数 | 作用 | 我的建议 |
|---|---|---|
| draft-max | 每轮最多尝试生成的草稿 token 数 | 起步 5,代码场景可到 8~10 |
| draft-min | 动态草稿长度的下限 | 3~4,避免频繁重算 |
| draft-p-min | 低接受率时提前截断草稿的概率阈值 | 0.9 附近,配合动态长度很有效 |
2.3 为什么打开投机解码后 14 直接变成 61
第一次打开投机解码用的是 0.6B 草稿模型,draft-max=5,采样方式改成 greedy,因为投机解码在贪婪采样下接受率最高。测试结果:等效速度 61 tok/s,比基线翻了 4 倍多。日志里看到的接受率稳定在 0.88 左右,也就是说平均每轮 5 个草稿 token 有 4.4 个被直接放行,大模型一次验证就能产出约 5 个 token。
这个 61 并不是理论峰值,它的意义在于证明了"串行依赖是可以绕开的"。大模型验证阶段虽然要处理 6 个 token 的 batch,但权重读取只算一次,kernel 启动也只算一次,开销比原来连续生成 6 个 token 小得多。从 14 到 61 这个跳跃,让我下定决心后面继续往里压榨。
3. 从 14 到 159 的调优全程:每一步改了什么,为什么有效
3.1 第一步:打开投机解码,固定 greedy 采样先测上限
先给一个可直接参考的命令行示例,参数名不同框架略有差别,含义对应即可:
./local-llm -m /models/Qwen3.8-27B-Q4_K_M.gguf \ --draft /models/draft-0.6b-q4_k_m.gguf \ -c 8192 \ --temp 0 \ --samplers "greedy" \ --draft-max 5 \ --draft-min 3 \ --draft-p-min 0.9 \ -n 512这里有个很重要的细节:第一轮测速我故意把温度设成 0、采样方式固定 greedy。原因很简单,greedy 下"接受"就是完全比对 token,接受率最高且可复现;如果一开始就把温度开到 0.6 甚至 0.8,接受率会受到随机采样影响,你根本分不清是草稿模型不行还是温度拖后腿。
跑完这轮,我从日志确认了三件事:接受率 0.88,平均每轮产出 5 个 token 左右,等效速度 61 tok/s。从 14 到 61,已经证明了投机解码在 27B 模型上的适配性。这里插一句:如果你跑完发现速度反而比 baseline 还低,先别怀疑框架,大概率是草稿模型选小了或者 draft-max 太大,后面排查章节我会单独列出来。
3.2 第二步:换草稿模型尺寸和步长,把 61 抬到 88
baseline 稳了之后,我开始试不同的草稿模型尺寸和 draft-max 组合。测试任务用代码补全,因为这类文本重复度高,接受率波动能比较明显地反映参数差异。结果记录如下:
| 草稿模型 | draft-max | 接受率 | 等效 tok/s |
|---|---|---|---|
| 0.3B | 5 | 0.71 | 49 |
| 0.6B | 5 | 0.88 | 61 |
| 0.6B | 8 | 0.79 | 88 |
| 0.6B | 12 | 0.68 | 81 |
| 1.3B | 8 | 0.75 | 76 |
结论很清晰:0.6B + draft-max=8 是这组里的甜点,等效速度 88 tok/s。draft-max=12 看起来更长,但接受率掉到 0.68,草稿尾部大量 token 被拒,验证阶段还要处理额外的拒绝重算,速度反而回到 81。这说明投机解码不是"步长越长越好",而是要找到"接受率还是够高、单轮产出也够多"的平衡点。
另外 1.3B 草稿模型在 draft-max=8 时只有 76 tok/s,它草稿阶段耗时明显增加,尽管接受率不低,整体还是输了。所以我在这个项目上彻底放弃了"更大草稿模型更好"的想法,0.6B 就是这个 27B 主模型的优选搭子。
3.3 第三步:恢复温度,用 KV cache 量化和动态截断稳住 117
前面为了测速用 greedy,但真实对话不能全部贪心,否则很快就会发现输出变得机械、重复。我恢复到温度 0.6、top-p 0.9,第一轮测试等效速度立刻掉到 70 左右,接受率从 0.88 降到 0.76。这是投机解码与采样温度之间的天然矛盾,不是 bug。
解决方案是用动态草稿截断和 cache 优化把它补回来。我把 draft-max 提到 12、draft-min 设为 4、draft-p-min 设为 0.9,逻辑是:草稿模型越往后越不确定,如果后续 token 的接受概率掉到阈值以下,就提前切掉,不让它在低质量 region 上浪费时间。这个改动让恢复温度后的接受率回到 0.82,等效速度到了 88 附近。
接着做 KV cache 量化:把 K 和 V 从 fp16 切成 q8_0。这一步两个收益:一是 cache 占用减半,8K 上下文能重新开起来;二是 cache 读取的带宽消耗下降,长上下文场景 decode 也快一点。最终稳定在 117 tok/s。顺带说下,如果你的框架支持 CUDA graph,把 graph 打开能减少小 batch 的 kernel launch 开销,我这里开着之后速度又涨了一点,不过数字波动比较大,我就没把你这个也算进 117 里。
3.4 第四步:159 tok/s 的峰值是怎么来的,以及它代表什么
117 已经比 baseline 好太多了,但我还想要一个更激进的数字。测试环境改成:短上下文 1.5K,任务类型是代码补全和 JSON 结构化补全,采样温度 0.2,草稿模型还是 0.6B,draft-max 8~12 动态。在这种高重复度、强结构的文本里,草稿模型的命中率非常高,接受率直接到了 0.93,平均每轮有效产出 7.2 个 token,最终等效速度冲到了 159 tok/s。
我必须说清楚,159 不是日常任务的平均水平。如果你拿着这个数字去跟别人说"27B 本地跑 159",那是误导。它的实际意义是:在代码补全、格式化输出、重复模板文本这类场景,投机解码能让你的 27B 模型快到一个非常舒服的档位。我后面在四条不同任务上做了对照:
| 任务类型 | 上下文 | 平均接受率 | 等效 tok/s |
|---|---|---|---|
| 代码补全 / JSON 补全 | 1.5K | 0.93 | 159 |
| 技术问答(混合文本) | 4K | 0.84 | 88 |
| 长文续写(叙事类) | 8K | 0.77 | 74 |
| 多轮创意对话 | 4K | 0.72 | 65 |
所以,你的应用场景决定你能吃到的加速上限。如果日常就是代码助手,159 是可以稳定复现的;如果是创意写作,那就别拿 159 当预期,65~74 才是真实水平。
4. 调优路上踩过的坑:被虚标速度骗、显存溢出、草稿贪大
4.1 你以为的 tok/s 可能不是真实 tok/s
投机解码有个迷惑性很强的点:很多框架的进度条在投机路径下会把"被接受的草稿 token"也算进生成速度,或者只统计验证阶段 token,不看草稿阶段的耗时。我有一次在 GUI 上看到瞬时 180,以为还能再冲,结果任务跑完整理日志才发现,墙钟平均只有 74。中间差的一倍多,全被"瞬时速度算法"给吞掉了。
我的建议是:测速不要看进度条,要看一个完整任务结束后,token 总数除以墙钟总时间。最好写一个小脚本,解析日志里的 prompt token 数、生成 token 数和总耗时,统一算平均。这样才能保证你发出去的数据经得起二次验证。后面我调整参数时都以这个"墙钟平均"为准,不再理会瞬时数字。
4.2 显存溢出:开投机解码并不是无成本的
我第一次把所有优化都打开,上下文直接拉到 8K,KV cache 没量化,草稿模型还试了 1.3B,结果启动没多久就报显存溢出。原因很简单:模型 14.9GB + KV cache 8GB + 草稿 1.3GB + CUDA context 1.5GB,累计超过 25GB,24GB 卡当然顶不住。
解决办法是分步省:KV cache 用 q8_0 先砍一半,上下文从 8K 降到 6K,草稿模型换回 0.6B。改完后显存占用大约 19.8GB,余量充足。这里要特别强调:草稿模型必须常驻显存,不能放到系统内存里,否则每次草稿阶段要跨 PCIe 传数据,速度直接腰斩。如果你发现开投机之后显存占用异常高,先检查草稿模型是不是被 framework 默认加载到了 host memory。
4.3 草稿模型贪大贪长,速度反而会翻车
前面表格已经显示,1.3B 草稿模型 + draft-max=12 的组合等效速度只有 76,比 0.6B + 8 低了 12。原因不复杂:草稿阶段从 8ms 涨到 15ms,而接受率并没有因为模型更大而明显提升,等于多花时间猜一样的结局。我自己也犯过这个错,总以为"草稿更聪明,大模型就不用那么累",实测下来完全不是这么回事。
所以给个实操准则:草稿模型的优先级是"足够轻量",不是"足够聪明"。它唯一的目标是在最短时间内提供尽量匹配的 token,而不是像大模型一样理解全局语义。只要接受率能到 0.8 以上,草稿模型的智力水平根本没那么重要。
4.4 常见问题速查表
| 问题 | 可能原因 | 解决方式 |
|---|---|---|
| 开投机后速度低于 baseline | 草稿模型太弱/太强,步长不合适 | 换 0.6B 级草稿,draft-max 从 5 起步再试 8 |
| 显存溢出 | KV cache 过大、草稿模型过大、上下文过长 | 量化 K/V 到 q8_0,草稿降到 0.6B,上下文降到 4K~6K |
| 恢复温度后速度明显下降 | 接受率被高温度拉低 | 开动态截断 draft-p-min=0.9,降低 top-p 到 0.85 |
| 进度条速度虚高 | 框架把草稿 token 也算进生成速度 | 以完整任务 token 总数/墙钟时间计算 |
| 输出重复变高 | grepdey/温度路径混在一起 | 检查采样器配置,确认当前使用哪条采样路径 |
| 长上下文场景速度骤降 | KV cache 未量化或草稿长度固定过长 | 量化 cache,draft-min 设小一点 |
4.5 文本类型影响巨大,这是特性不是 bug
同一套配置,在代码补全和创意写作上的速度能差一倍多,这不是调参能完全抹平的,而是投机解码天然对高重复度文本友好。代码、JSON、表格、模板文本,草稿模型命中率高,接受率轻松 0.9 以上;但叙事、多语言混合、意识流文本,草稿模型很容易跑偏,接受率掉到 0.5~0.6。
所以我强烈建议:评估投机解码效果时,固定住任务类型,别拿不同任务的成绩横向比,更不要拿峰值速度告诉队友"。如果应用是聊天助手,用聊天集测;如果是代码补全,用代码集测。这样才能得到稳定、可复现的基准。
5. 最终的配置方案与我这轮调优的真实心得
5.1 可以直接抄的最终命令行配置
下面是我在 Qwen3.8-27B 上最后定型的运行参数,日常混合任务平均 88 tok/s 左右,代码补全场景能上 159:
./local-llm -m /models/Qwen3.8-27B-Q4_K_M.gguf \ --draft /models/draft-0.6b-q4_k_m.gguf \ -c 8192 \ --cache-type-k q8_0 \ --cache-type-v q8_0 \ --temp 0.6 \ --top-p 0.9 \ --repeat-penalty 1.1 \ --draft-max 10 \ --draft-min 4 \ --draft-p-min 0.9 \ -n 512如果你的显存只有 16GB,把上下文压到 4K、KV cache 继续保持 q8_0、草稿用 0.6B 即可;如果显存在 32GB 以上,可以尝试 1.0B~1.3B 草稿模型和更长的 draft-max,但记得像我一样做对比测试,别凭感觉加码。
5.2 别只盯榜分数,看"有效吞吐"
调优结束后我拿一个真实任务验证:生成一篇约 900 字的技术博客 demo,prompt 约 100 token,实际生成 2100 token。调优前大约需要 150 秒,调优后降到 22 秒,等效约 95 tok/s。这才是有效吞吐,它反映的是一个完整任务从开始到结束的体验,而不是进度条上的某个瞬间。
如果你在跑 API 服务或者批量任务,有效吞吐更重要。同样一张卡,投机解码开启后,每秒能服务的 token 数量翻了 5~6 倍,这意味着你可以用更少的卡扛住原来好几倍的压力,或者在同等的延迟要求下把模型从 14B 升级到 27B。
5.3 后面这些方向还能继续压榨
投机解码只是第一层优化,后续还能做的方向大概有这么几块:一是并行采样,多路草稿同时跑,进一步摊薄验证成本;二是系统提示词和对话前缀的 KV cache 重用,长对话场景收益很大;三是量化格式的进一步选择,比如更激进的量化加微调补偿;四是框架层面的连续批处理,把多个请求排成一个 batch,显存利用率和吞吐还能再上一个台阶。
但我必须泼一盆冷水:不要一次性把所有优化全开,因为你根本分不清哪个起了作用、哪个在拖后腿。建议每次只加一个变量,跑同样的任务集,记录 token 总数/墙钟时间,再决定保留还是回滚。我这次调优每改一个参数都跑至少三遍,数据稳定之后才进入下一步。
5.4 我自己的体会:投机解码是低预算上场大模型的最佳杠杆
这轮调完,我对本地推理的认识变了不少。以前总觉得速度上不去就是显存带宽不够,换卡才能解决;实测下来,decode 阶段真正的敌人是串行依赖和小 batch 利用率。投机解码等于把每个 batch 从 1 个 token 变成 6~9 个 token,而代价只是一次验证时间和一个不到 1GB 的草稿模型,这在消费级硬件上几乎是免费的午餐。
如果只让我给新手留一句建议,我会说:先把 baseline 测准,然后打开投机解码,用 greedy 采样、0.6B 草稿、draft-max=5 跑一遍,再逐步调步长和温度。别一上来就追 159,先把 40、60、80 这几个台阶踩实,你对这套机制的体感会比任何参数表都准。数据这个东西,在自己机器上用 token 总数除以秒数算一遍,比听谁吹都靠谱。