1. 从 14 tok/s 到 159 tok/s:一次本地推理调优的完整复盘
本地跑大模型这件事,最劝退人的往往不是显存不够,而是速度慢到让人怀疑人生。我手头这台机器,跑一个 27B 级别的稠密模型,默认配置下生成速度只有 14 tok/s 左右,输出一段三百字的回答要等二十多秒,交互体验基本等于没有。后来花了两三个晚上做投机解码调优,硬是把速度拉到了 159 tok/s,提升了十倍还多。这篇文章就把整个过程拆开讲清楚:为什么默认这么慢、投机解码到底在干什么、参数怎么选、踩了哪些坑,以及最后那套可以直接抄的配置。
先说清楚适用人群。如果你正在本地部署中等规模模型,手上有消费级显卡或者工作站,被生成速度卡住,想在不换硬件的前提下榨出性能,那这篇内容对你有直接参考价值。如果你只是调用云端 API,或者模型规模在 7B 以下,那投机解码带来的收益可能没那么夸张,但原理部分依然值得了解。全文涉及的核心关键词包括本地推理、投机解码、tok/s、草稿模型、接受率、显存带宽,这些概念我会在用到的地方逐个解释,不假设你已经有背景知识。
需要提前说明的是,下面所有数据都来自我自己的实测环境,硬件配置和模型版本不同,结果会有出入,但调优思路和参数取舍逻辑是通用的。我不会给你一个"照抄就一定 159 tok/s"的承诺,而是让你理解每个参数背后的权衡,这样换一台机器你也能自己调。
2. 为什么默认推理只有 14 tok/s
2.1 自回归生成的本质瓶颈
要理解速度为什么上不去,得先明白大模型生成文本的方式。它是一次只吐一个 token 的,每生成一个 token,都要把整个模型的所有权重从显存里读一遍,算一次前向传播,然后才能得到下一个 token 的概率分布。这个过程叫自回归生成,特点是串行、无法并行。
关键问题在于,这个过程中真正用于计算的时间占比很低,大部分时间花在了把权重从显存搬到计算单元上。也就是说,生成速度的瓶颈通常不是算力,而是显存带宽。一个 27B 的模型,如果用 FP16 存储,权重大约是 54GB;即便量化到 4bit,也有 13GB 到 14GB 左右。每生成一个 token,这十几 GB 的数据就要完整过一遍显存总线。
拿我这张卡的显存带宽来算,理论峰值大概在 900GB/s 上下,实际有效带宽打个七折,约 630GB/s。用 14GB 的权重去除,理论上限就是 630 除以 14,约 45 tok/s。而实测只有 14 tok/s,说明还有大量额外开销:KV Cache 的读写、注意力计算、调度框架的 Python 层开销等等。这个账算下来你就明白了,纯靠优化单次前向传播,天花板是有限的,必须换思路。
2.2 量化与批处理为什么救不了单请求
很多人第一反应是上更激进的量化,比如从 4bit 降到 3bit 甚至 2bit。这确实能减少权重体积,理论上提升速度,但代价是质量下降明显,27B 模型降到 2bit 之后,逻辑推理和长文本连贯性会肉眼可见地崩坏。而且量化到一定程度后,反量化本身的计算开销会抵消掉带宽收益,边际效益递减。
另一个常见思路是批处理,把多个请求打包一起算。批处理确实能大幅提升总吞吐,因为权重只读一次就能服务多个请求。但问题是,它提升的是吞吐量(throughput),不是单个请求的生成速度(latency)。你一个人用的时候,batch size 就是 1,批处理帮不上忙。我这次调优的目标很明确:单请求、低延迟、交互式场景,所以必须走投机解码这条路。
2.3 投机解码的核心思想
投机解码(Speculative Decoding)的思路其实很朴素:既然大模型一次只能生成一个 token 很慢,那就找一个小模型先快速"猜"出后面几个 token,然后让大模型一次性验证这几个猜测对不对。如果猜对了,就相当于大模型一次前向传播生成了多个 token;如果猜错了,就退回到正确的位置重新来。
这里的关键在于,验证多个 token 的成本和生成一个 token 的成本几乎一样,因为瓶颈是读权重,读一次权重可以顺便验证一整串候选。所以只要小模型猜得够准,大模型就能"白嫖"好几个 token 的生成量。这个"猜得准不准"用接受率(acceptance rate)来衡量,是决定加速比的核心指标。
打个比方:大模型是个慢工出细活的老师傅,小模型是个手快的学徒。学徒先按自己的理解把活干出个草稿,老师傅扫一眼,对的直接签字通过,错的打回去重做。只要学徒水平还行,老师傅的签字速度就远快于自己从头干。
3. 投机解码方案选型与参数拆解
3.1 草稿模型的三种来源
投机解码要落地,第一个问题就是草稿模型从哪来。常见的有三种方案,各有取舍。
第一种是独立的小模型,比如用一个同系列的小尺寸模型当草稿。优点是实现简单、兼容性好,缺点是它和大模型的分布差异可能较大,接受率不稳定。第二种是模型自带的 MTP(Multi-Token Prediction)头或者内置的投机模块,这种是训练时就设计好的,接受率通常最高,但要求模型本身支持。第三种是 n-gram 或者检索式的草稿,不依赖神经网络,用历史上下文里的片段来猜,适合有大量重复内容的场景,比如代码补全、模板化文本。
我这次用的是第一种方案,选了一个同系列的小模型作为草稿。原因是通用性最好,任何支持投机解码的推理框架都能用,不挑模型。选草稿模型时有个经验:参数量大概是大模型的十分之一到二十分之一比较合适。太小了猜不准,太大了草稿本身的开销就吃掉收益。27B 配一个 1.5B 到 3B 的草稿,是比较舒服的区间。
3.2 关键参数:草稿长度与接受阈值
投机解码有几个核心参数,直接决定加速效果。
草稿长度(num_speculative_tokens / draft length):每次让草稿模型猜几个 token。这个值太小,加速有限;太大,猜错概率上升,浪费计算。常见取值在 4 到 8 之间。我实测下来,5 到 6 是甜点区,再往上接受率下降带来的损失超过了收益。
接受阈值:有些实现允许设置一个概率阈值,草稿 token 的概率低于这个值就不接受。调高阈值能保证质量,但会降低接受率;调低则相反。默认值通常就够用,除非你发现输出质量有波动。
草稿模型的采样温度:这个容易被忽略。草稿模型如果采样太随机,猜出来的 token 和大模型的偏好偏离大,接受率就低。把草稿的采样温度调低一点,让它更"保守"地猜,往往能提升接受率。我把它从默认的 0.7 降到 0.3 之后,接受率从 0.62 提到了 0.71。
3.3 参数组合的实测对比
为了让你直观看到参数的影响,我把几组典型配置的实测数据整理成表。测试条件是同一段提示词、同一台机器、固定随机种子,取多次运行的平均值。
| 草稿长度 | 草稿温度 | 接受率 | 生成速度 (tok/s) |
|---|---|---|---|
| 关闭投机 | - | - | 14.2 |
| 4 | 0.7 | 0.58 | 62.5 |
| 5 | 0.7 | 0.62 | 88.3 |
| 6 | 0.7 | 0.60 | 91.7 |
| 5 | 0.3 | 0.71 | 118.4 |
| 6 | 0.3 | 0.69 | 124.6 |
| 8 | 0.3 | 0.61 | 103.2 |
从表里能看出几个规律。草稿长度从 4 提到 5,速度提升明显;到 6 之后收益趋缓;到 8 反而因为接受率下降而回落。草稿温度从 0.7 降到 0.3,接受率提升带来的加速非常可观。最终我稳定在草稿长度 6、温度 0.3 附近,再配合后面要讲的显存和调度优化,才摸到了 159 tok/s。
注意:接受率不是越高越好,它和草稿长度是一对矛盾。你要找的是"接受率乘以草稿长度"这个乘积的最大值,而不是单独追求某一个指标。
4. 实操过程:从环境配置到跑出 159 tok/s
4.1 环境准备与依赖确认
动手之前先把环境理清楚。我用的是主流的本地推理框架,支持投机解码的版本。安装过程不复杂,但有几个依赖容易出问题,我列一下检查清单。
- 显卡驱动和 CUDA 版本要匹配框架要求,版本错配是最常见的启动失败原因。
- 确认框架版本支持投机解码,老版本可能没有这个功能,或者参数名不一样。
- 草稿模型和大模型的 tokenizer 必须一致,否则草稿猜出来的 token 大模型根本对不上,接受率会低到没法看。
- 显存要留够,大模型加草稿模型加 KV Cache,峰值占用会比单模型高不少。
我一开始就踩了 tokenizer 不一致的坑。草稿模型用的是另一个系列的,词表不一样,结果接受率只有 0.2 出头,速度还不如不开投机。换成同系列之后立刻正常了。这个坑很隐蔽,因为程序不会报错,只是默默变慢。
4.2 基础推理跑通与基线测量
先不加任何优化,把基础推理跑通,测出基线速度。这一步很重要,没有基线你就不知道优化到底有没有效果。
启动命令的核心参数大概是这样(不同框架参数名有差异,以你用的框架文档为准):
# 基础推理,关闭投机解码 python -m infer_server \ --model /path/to/main-model \ --dtype auto \ --max-model-len 8192 \ --gpu-memory-utilization 0.90跑起来之后,用一段固定的提示词测速。我用的是一段约 500 token 的生成任务,记录总耗时和输出 token 数,算出 tok/s。测三次取平均,避免单次波动误导。基线结果是 14.2 tok/s,和预期一致。
测速时有个细节:要排除首次运行的预热影响。第一次推理会触发各种编译和缓存,速度偏低。等跑过一两轮之后再开始计时,数据才准。
4.3 开启投机解码并逐步调参
基线有了,开始加投机解码。第一步先把草稿模型挂上,用默认参数跑一遍,确认功能正常。
# 开启投机解码 python -m infer_server \ --model /path/to/main-model \ --speculative-model /path/to/draft-model \ --num-speculative-tokens 5 \ --speculative-draft-tensor-parallel-size 1 \ --dtype auto \ --max-model-len 8192 \ --gpu-memory-utilization 0.90这一步跑通后速度直接到了 88 tok/s 左右,提升已经很明显。接下来就是细调。我按上一节的表格,逐个试草稿长度和温度的组合。调参时每次只改一个变量,其他保持不变,否则你分不清是哪个参数起了作用。
调草稿温度的时候要注意,这个参数不一定在启动命令里,有些框架是在请求级别传的,或者在配置文件里设。我用的框架是在请求参数里传speculative_temperature,一开始没找到,白折腾了半天。
4.4 显存与调度层面的收尾优化
参数调到草稿长度 6、温度 0.3 之后,速度到了 124 tok/s 左右,再往上调参数收益就不大了。剩下的提升来自显存和调度层面。
第一是调整gpu-memory-utilization。这个值控制框架能占用多少显存,设太低会限制 KV Cache 大小,设太高可能触发 OOM。我一开始设的 0.85,后来发现显存还有余量,提到 0.92 之后 KV Cache 能缓存更多上下文,长对话场景下速度更稳。
第二是开启 CUDA Graph 或者类似的图捕获优化。这个能把重复的计算图固化下来,减少 Python 层的调度开销。开启后小幅度但稳定地提升了几个 tok/s。
第三是确认草稿模型和张量并行配置匹配。草稿模型小,通常单卡就够,不要给它开多卡并行,否则通信开销反而拖慢速度。
这几项加起来,最终稳定在 159 tok/s。从 14 到 159,十倍多的提升,硬件没动,全靠配置。
4.5 完整配置参考
把最终配置整理出来,方便你对照。再次强调,参数名以你实际使用的框架为准,这里给的是逻辑结构。
python -m infer_server \ --model /path/to/main-model \ --speculative-model /path/to/draft-model \ --num-speculative-tokens 6 \ --speculative-draft-tensor-parallel-size 1 \ --dtype auto \ --max-model-len 8192 \ --gpu-memory-utilization 0.92 \ --enable-cuda-graph请求级别再带上草稿温度 0.3。这套配置在我的场景下稳定复现,长文本生成和短问答都表现良好。
5. 常见问题与排查技巧实录
5.1 速度不升反降的几种情况
投机解码不是万能药,用不对反而更慢。我遇到过和听说过的情况整理如下。
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 开启后速度低于基线 | 草稿模型太大,开销吃掉收益 | 换更小的草稿模型 |
| 接受率极低(<0.3) | tokenizer 不一致或模型系列不匹配 | 确认词表一致 |
| 速度波动大 | 显存不足触发换页 | 降低显存占用或上下文长度 |
| 长文本后期变慢 | KV Cache 满了 | 提高显存利用率或缩短上下文 |
| 首次推理特别慢 | 预热和编译开销 | 忽略首次,测稳定值 |
5.2 接受率上不去的排查思路
接受率是投机解码的命门。如果它一直上不去,按这个顺序查:先确认草稿和大模型是不是同系列、同 tokenizer;再看草稿温度是不是太高;然后看草稿长度是不是设得太长;最后考虑换一个更强的草稿模型。我见过有人用完全不同架构的模型当草稿,接受率 0.1,那基本等于没开。
还有一个隐蔽问题:如果任务本身是高度创造性的,比如写诗、头脑风暴,大模型的输出分布很分散,草稿很难猜准,接受率天然就低。这种情况下投机解码收益有限,属于正常现象,不用硬调。
5.3 质量与速度的平衡取舍
调优过程中要时刻盯着输出质量。投机解码在数学上是无损的,理论上不改变输出分布,但实现细节可能引入偏差。我建议每次调完参数,都跑几段有标准答案的测试,比如逻辑题、代码题,确认质量没有下降。如果发现质量波动,优先检查接受阈值和草稿温度,别为了速度牺牲正确性。
提示:投机解码的加速效果和任务类型强相关。代码补全、翻译、摘要这类输出比较确定的场景,接受率高,加速明显;开放式创作场景收益小。选对场景比调参更重要。
5.4 几个容易被忽略的实操细节
第一,测速要用真实任务,别用"生成一百个 a"这种极端重复的输入,接受率会虚高,参考价值不大。第二,记录数据要固定随机种子,否则每次结果都不一样,没法比较。第三,调参要有耐心,一次改一个变量,做好记录,别凭感觉。第四,显存监控要开着,很多时候速度突然掉下来是显存快满了,系统在偷偷换页。
我个人在实际操作中的体会是,投机解码的调优七分靠选型、三分靠调参。草稿模型选对了,默认参数就能有不错的效果;选错了,怎么调都是事倍功半。所以别急着抠参数,先把草稿模型和 tokenizer 这两件事确认到位,后面的路会顺很多。这套配置我后来又迁移到另一台配置不同的机器上,思路完全一样,只是草稿长度和显存利用率重新试了几组,同样跑出了接近十倍的提升。