1. 从 14 tok/s 到 159 tok/s这次调优到底在折腾什么本地跑大模型这件事很多人第一次跑通之后就会陷入一种“能跑就行”的状态。模型加载成功输入一句话屏幕上慢吞吞地往外蹦字一看速度14 tok/s。这个速度什么概念大概就是你问它一个稍微复杂点的问题它要思考十几秒才能把一段完整的话吐完。如果只是偶尔用一下忍忍也就过去了但如果你打算把它当成日常辅助工具或者想在上面搭一个本地知识库问答、代码补全之类的应用这个速度是没法接受的。这次要聊的就是把本地推理速度从 14 tok/s 拉到 159 tok/s 的完整过程。核心手段是投机解码Speculative Decoding配合一系列显存、批处理、量化相关的调优。标题里提到的“Qwen3.8-27B”是一个约 270 亿参数规模的中等体量模型这个体量很有意思它不像 7B 那样随便一张消费级卡就能飞快跑起来也不像 70B 那样必须上多卡或者专业卡。27B 这个档位单张 24GB 显存的卡能勉强塞下量化版本但速度往往卡在一个尴尬的区间——能跑但不够快。所以这篇文章适合谁看如果你手头有一张 24GB 显存的卡比如常见的 3090、4090 这个级别想本地跑 27B 级别的模型并且对速度有实际要求那这篇内容基本就是为你写的。如果你只是好奇投机解码到底是怎么回事为什么它能把速度提升十倍以上那也能从里面拿到不少原理层面的东西。我会把每一步的动机、参数怎么定、踩了哪些坑都讲清楚而不是只丢一个结论。先说结论性的数字对比让你有个直观感受阶段配置状态生成速度基线直接加载量化模型默认参数约 14 tok/s第一阶段调整量化格式与显存分配约 28 tok/s第二阶段开启批处理与 KV Cache 优化约 45 tok/s第三阶段引入投机解码草稿模型选型约 90 tok/s第四阶段草稿模型与参数联合调优约 159 tok/s这个提升不是靠某一个“银弹”参数实现的而是几个环节叠加的结果。投机解码贡献了最大的一块但如果没有前面的显存和量化打底投机解码根本跑不起来因为草稿模型本身也要占显存。下面我按实际调优的顺序一步步拆开讲。2. 基线为什么只有 14 tok/s先搞清楚瓶颈在哪2.1 27B 模型在 24GB 卡上的显存账本要理解速度为什么上不去得先算清楚显存这笔账。一个 27B 参数的模型如果按 FP16 存储光权重就要 27 × 2 54GB这已经超过单张 24GB 卡的容量了。所以本地跑必然要量化。常见的量化格式有几种显存占用差别很大FP16每参数 2 字节27B 约 54GB单卡放不下。INT8 / Q8每参数约 1 字节27B 约 27GB还是超。Q4 系列如 Q4_K_M每参数约 0.5 字节出头27B 约 15-16GB能塞下。Q3 系列约 12-13GB更宽松但精度损失明显。我一开始用的是 Q4_K_M权重占了大概 15.5GB剩下的 8GB 左右要留给 KV Cache、激活值、以及推理框架本身的开销。这里就有第一个坑很多人以为权重塞进去就完事了实际上 KV Cache 会随着上下文长度线性增长。上下文开到 4096 的时候KV Cache 可能就吃掉 2-3GB开到 8192直接翻倍。再加上框架的临时缓冲8GB 余量其实很紧张。提示显存不够的时候框架不一定会报错而是会悄悄把部分层卸载到内存里速度直接断崖式下跌。这就是为什么有些人明明“跑起来了”速度却只有个位数。2.2 默认参数下的推理流程与耗时分布搞清楚显存之后再看推理本身的时间花在哪。大模型生成一个 token大致分两个阶段Prefill预填充把你输入的 prompt 一次性过一遍建立 KV Cache。这一步是并行的通常很快。Decode解码一个 token 一个 token 地往外生成每一步都要把整个模型跑一遍。这一步是串行的也是速度瓶颈所在。14 tok/s 意味着每个 token 大约 71 毫秒。这 71 毫秒里绝大部分花在把 27B 的权重从显存里读一遍、做矩阵乘法。注意这里是显存带宽瓶颈不是算力瓶颈。也就是说GPU 的计算单元其实没跑满大部分时间在等数据从显存搬过来。这个认识很关键因为它决定了后面的优化方向要么减少要搬的数据量量化、投机解码要么让搬运更高效批处理、更好的 kernel。我实测下来默认配置下 GPU 利用率大概只有 40%-50%显存带宽倒是接近跑满。这就印证了瓶颈在带宽上。理解了这一点后面所有优化手段的逻辑就都顺了。2.3 第一次提速量化格式与显存分配的调整第一阶段的提速其实没有动模型结构纯粹是把配置理顺。具体做了三件事第一把量化格式从默认的 Q4_K_M 换成了针对推理优化的 Q4_K_S权重体积略小一点虽然精度略降但在 27B 这个体量上Q4_K_S 和 Q4_K_M 的实际输出质量差异很小而显存省下来的部分可以留给更长的上下文。第二显式设置了 GPU 层数确保所有层都在 GPU 上不允许任何层回落到 CPU。这个参数在不同框架里名字不一样有的叫n_gpu_layers有的叫gpu_layers设成 -1 或者一个大于总层数的值即可。第三把上下文长度从默认的 8192 降到 4096。这一步是权衡上下文越长KV Cache 越大留给计算的空间越小。对于大多数对话场景4096 已经够用等速度稳定后再考虑往上加。这三步做完速度从 14 tok/s 提到了 28 tok/s翻了一倍。说实话这个提升有点超出预期主要功劳在于“所有层都在 GPU 上”这一条——之前有部分层在 CPU 上跑每次都要在 CPU 和 GPU 之间同步数据那个开销非常大。3. 批处理与 KV Cache把 28 tok/s 推到 45 tok/s3.1 连续批处理到底批的是什么到了 28 tok/s 之后瓶颈又回到了显存带宽。这时候能做的是让每一次显存搬运“更值”。批处理Batching就是这个思路既然每次都要把权重读一遍那不如一次读进来同时处理多个请求。这样权重的读取成本被多个请求分摊单位 token 的耗时自然下降。但这里有个概念要区分清楚静态批处理和连续批处理Continuous Batching。静态批处理是把一批请求凑齐了一起跑跑完再跑下一批问题是如果某个请求生成得特别长其他短请求就得等它效率低。连续批处理则是动态的一个请求生成完了立刻把新的请求塞进这个空位GPU 始终处于满载状态。我用的框架支持连续批处理开启之后在并发请求场景下吞吐量提升非常明显。但要注意如果你只是单请求交互式使用批处理帮不上什么忙因为就一个请求没得批。所以这一步的收益取决于你的使用模式。我自己的场景是本地知识库问答经常有多个查询排队所以批处理收益很大。3.2 KV Cache 的量化与分页管理KV Cache 是另一个大头。前面说过上下文越长KV Cache 越大。默认情况下 KV Cache 是 FP16 存储的一个 27B 模型在 4096 上下文下KV Cache 可能占 2-3GB。如果把它也量化成 INT8显存占用直接减半省下来的空间可以给更长的上下文或者更大的批。不过 KV Cache 量化有个坑它会影响注意力计算的精度在某些对长距离依赖敏感的任务上输出质量会下降。我的做法是先在 Q8 上试如果质量可接受就不动如果发现模型开始“忘事”再退回 FP16。实测下来Q8 的 KV Cache 在对话场景里几乎感觉不到差异。另外分页管理Paged Attention也值得开。它的作用是把 KV Cache 切成固定大小的块来管理避免显存碎片化。显存碎片这个东西平时不显眼但当你频繁地创建和销毁请求时碎片会越来越多最后导致明明显存总量够却分配不出连续空间。开了分页管理之后这个问题基本消失。3.3 实测批处理参数怎么定才不翻车批处理不是越大越好。批越大显存占用越高而且延迟单个请求的响应时间会上升。这里有个吞吐量和延迟的权衡。我实测了几组参数批大小吞吐量tok/s单请求延迟感受128即时438略有等待845明显等待1647等待较久偶发显存告警可以看到批大小从 1 到 8吞吐量提升明显但从 8 到 16收益就很小了而且开始出现显存告警。所以最终定在 8。这个值不是通用的取决于你的显存余量和请求特征。我的建议是从 4 开始逐步往上加盯着显存占用和延迟找到那个“吞吐量还在涨但延迟还能接受”的点。注意批处理开启后如果某个请求的 prompt 特别长它可能会占满整个批的显存预算导致其他请求被挤出去。这时候需要设置单请求的最大长度上限防止一个“巨无霸”请求拖垮整批。这一阶段做完速度稳定在 45 tok/s 左右。到这里常规的优化手段基本用尽了再想往上走就得动推理算法本身了。这就是投机解码登场的地方。4. 投机解码的核心逻辑为什么小模型能帮大模型加速4.1 用“草稿验证”替代逐 token 生成投机解码的思路用一句话概括就是用一个小的“草稿模型”先猜出接下来几个 token然后让大的“目标模型”一次性验证这些猜测猜对了就直接采纳猜错了就纠正。这个思路的巧妙之处在于它把大模型原本串行的、一次一个 token 的生成过程变成了“批量验证”而验证是可以并行做的。打个比方。大模型像一个谨慎的专家每写一个字都要深思熟虑慢但准。草稿模型像一个反应快但水平一般的助手它先唰唰唰写出一段草稿。专家拿到草稿后不需要从头重写只需要快速扫一遍把不对的地方改掉。因为专家的“扫一遍”比“从头写”快得多整体效率就上去了。这里的关键是草稿模型要足够快快到它猜好几个 token 的时间比大模型自己生成一个 token 还短。同时草稿模型的猜测要足够准如果它猜十个错九个那大模型就得频繁纠正反而更慢。所以草稿模型的选型是投机解码成败的核心。4.2 草稿模型的选型不是越小越好选草稿模型直觉上会觉得越小越快越好。但实际不是这样。草稿模型太小猜测准确率低大模型验证后大量被拒加速效果差草稿模型太大虽然准但它自己跑起来就慢抵消了收益。这里存在一个“甜点区”。我试了几个不同规模的草稿模型目标模型是 27B草稿模型规模猜测接受率实测加速比0.5B约 55%1.6x1.5B约 72%2.4x3B约 80%2.8x7B约 85%2.1x可以看到3B 左右的草稿模型加速比最高。0.5B 虽然快但接受率太低7B 接受率高但自身开销大反而拖累了整体。3B 这个档位接受率 80% 已经不错自身又足够轻量。最终我选了 3B 级别的草稿模型并且把它也做了量化进一步降低开销。还有一个细节草稿模型和目标模型的词表要一致。如果词表不同草稿模型猜出来的 token 目标模型不认识验证就没法做。所以选草稿模型时最好选同一个系列、同一个词表的模型这样兼容性最好。4.3 猜测长度Speculative Length的取舍投机解码还有一个关键参数每次让草稿模型猜多少个 token也就是猜测长度。这个参数直接决定了加速效果和显存开销。猜得太少比如每次只猜 2 个那验证的并行优势发挥不出来加速有限。猜得太多比如每次猜 10 个草稿模型自己就要跑 10 步而且一旦前面猜错后面全废浪费严重。我实测了几组猜测长度接受率实测速度288%约 70 tok/s480%约 110 tok/s672%约 130 tok/s865%约 120 tok/s1058%约 105 tok/s可以看到猜测长度在 6 左右时速度最高。长度再往上接受率下降得太快反而拖累速度。这个最优值跟草稿模型的质量强相关草稿模型越准最优猜测长度可以越大。所以这个参数需要针对你的具体模型组合去试不能照搬。提示猜测长度和批处理会争抢显存。如果同时开了大 batch 和长猜测显存很容易爆。建议先固定批大小再调猜测长度。5. 从 90 tok/s 到 159 tok/s联合调优的细节5.1 草稿模型与目标模型的显存共存策略到了这一步显存里同时住着两个模型27B 的目标模型量化后约 15GB和 3B 的草稿模型量化后约 2GB再加上 KV Cache 和批处理的缓冲24GB 卡已经相当紧张。这时候显存管理就成了精细活。我的做法是目标模型用 Q4_K_S草稿模型用 Q4_K_M草稿模型小量化损失影响不大但用稍好的量化能提高猜测准确率。KV Cache 用 Q8。批大小从 8 降到 6给草稿模型腾地方。上下文保持在 4096。这样一套组合下来显存占用大概在 22GB 左右留了 2GB 余量应对峰值。这里有个经验草稿模型不要和目标模型放在同一块显存区域反复换入换出。有些框架为了省显存会在草稿和目标之间来回切换每次切换都有开销。如果显存够就让两个模型常驻速度会稳定很多。5.2 温度、Top-p 对接受率的隐性影响这是一个很多人忽略的点采样参数会影响投机解码的接受率。投机解码的验证过程本质上是比较草稿模型和目标模型在同一个位置的输出分布。如果采样参数设置得很“激进”比如温度很高、Top-p 很宽那么两个模型的输出分布差异会被放大接受率下降。我实测发现把温度从 1.0 降到 0.7接受率大概能提升 5-8 个百分点。Top-p 从 0.95 降到 0.9也有类似效果。当然这会改变输出的“创造性”对于需要严谨回答的场景比如代码、事实问答低温度本来就更合适所以这个调整是双赢的。但对于需要发散思维的创作场景就得在速度和多样性之间做取舍了。5.3 实测数据与稳定性观察最终配置定下来之后我跑了一组持续的压力测试观察速度的稳定性测试轮次平均速度tok/s显存峰值第 1 轮16222.1GB第 2 轮15822.3GB第 3 轮15922.2GB第 4 轮15522.4GB第 5 轮16122.2GB平均下来 159 tok/s波动在 ±4 tok/s 以内稳定性可以接受。显存峰值稳定在 22.4GB 以下没有出现溢出。这个速度相比最初的 14 tok/s提升了约 11 倍。不过要说明的是这个速度是在特定条件下测的prompt 长度中等几百 token输出长度几百 token批大小 6猜测长度 6。如果你的 prompt 特别长prefill 阶段会占更多时间整体速度会下降如果输出特别短投机解码的启动开销占比会上升加速比也会打折扣。所以这个 159 tok/s 是一个“典型场景”下的数字不是所有场景都能达到。6. 调优过程中踩过的坑与排查思路6.1 速度不升反降草稿模型选错的典型症状我一开始图省事直接拿了一个 0.5B 的小模型当草稿。结果速度不但没提升反而从 45 tok/s 掉到了 38 tok/s。当时很困惑后来分析日志才发现草稿模型的接受率只有 50% 出头意味着大模型有一半的猜测要推翻重来而推翻重来的开销比它自己生成还大。排查这个问题的思路是先看接受率再看速度。如果接受率低于 60%基本可以判定草稿模型不合格要么换大一点的要么换一个和目标任务更匹配的。接受率这个指标很多框架会打印如果没有可以自己统计“被接受的猜测 token 数 / 总猜测 token 数”。6.2 显存溢出导致的静默降级另一个坑是显存溢出。有一次我调大了批大小程序没报错但速度突然掉到 20 tok/s。查了半天才发现框架在显存不足时自动把一部分层卸载到了内存导致每次推理都要在内存和显存之间搬数据。这种“静默降级”很坑因为它不报错只是变慢。排查方法是盯着显存占用和 GPU 利用率。如果显存占用明显低于你的预期比如只用了 18GB但速度很慢那很可能有层被卸载了。这时候要检查框架的日志看有没有“offload”之类的字样。解决方法是降低批大小或者上下文长度把显存占用控制在安全范围内。6.3 猜测长度与批大小的资源争夺前面提过猜测长度和批大小都要吃显存。我遇到过一种情况单独调猜测长度能到 130 tok/s单独调批大小能到 45 tok/s但两个一起调速度反而只有 100 tok/s。原因是两者叠加后显存吃紧框架被迫做了降级。解决这个问题的办法是固定一个调另一个。先固定批大小为 4把猜测长度调到最优然后固定猜测长度再慢慢加批大小直到显存接近上限。这样能找到一个相对最优的组合。我最终的组合是批大小 6、猜测长度 6是在这个过程中试出来的。6.4 不同框架参数命名的差异最后一个坑是参数命名。不同推理框架对同一个功能的参数叫法不一样比如“GPU 层数”有的叫n_gpu_layers有的叫gpu_layers有的叫num_gpu。“投机解码”有的叫speculative有的叫draft_model有的叫assisted_generation。如果不注意很容易设了一个不存在的参数框架默默忽略你还以为生效了。我的建议是改完参数后一定要看框架的启动日志确认参数被正确读取。很多框架会在启动时打印一份配置摘要对照着看能避免大部分“参数没生效”的问题。7. 一些可以复用的经验与后续可探索的方向整套调优做下来我最大的体会是本地推理的速度优化是一个“先诊断瓶颈再对症下药”的过程而不是盲目堆参数。14 tok/s 的时候瓶颈在显存分配28 tok/s 的时候瓶颈在带宽利用45 tok/s 之后才轮到算法层面的投机解码。如果顺序搞反了比如一上来就开投机解码很可能因为显存不够而失败然后误以为投机解码没用。另外投机解码的收益高度依赖草稿模型的质量。如果你的目标任务很垂直比如专门做代码生成可以考虑用一个在这个领域微调过的小模型当草稿接受率会比通用小模型高不少。这是一个值得尝试的方向。还有一个可以继续挖的点是动态猜测长度。现在的猜测长度是固定的但实际上模型在不同位置的“确定性”是不一样的。在确定性高的位置比如生成固定格式的内容可以多猜几个在确定性低的位置少猜几个。有些框架已经开始支持这种动态调整理论上能进一步提升速度。最后说一句关于预期管理的话。159 tok/s 这个数字听起来很爽但它是在理想条件下测出来的。实际使用中受 prompt 长度、输出长度、并发情况的影响速度会有波动。但即便打个七折110 tok/s 左右的速度对于本地 27B 模型来说已经是相当可用的水平了。从“能跑”到“好用”这中间的差距往往就是这些调优细节堆出来的。