先说个最近一年多我一直在跟团队反复强调的观点:大模型的推理提速,早就不是“换更小的权重”或者“把模型从头调一遍”那套玩法了。我自己负责的几套线上服务,过去半年几乎没动过一行模型权重,首字延迟(TTFT)整整降了 77%。2026 年再回头看,这个趋势其实特别明显——真正让引擎“跑得更快”的优化,几乎全部发生在权重之外:缓存、调度、算子、显存管理。这篇文章就把我踩过坑、实测有效的东西完整盘一遍。
1. 为什么 2026 年的推理提速不再盯着权重
1.1 权重优化的边际收益在下降
权重层面的加速手段,通常就是量化(INT8/INT4/FP8)、剪枝、蒸馏。前几年这些方法效果确实立竿见影,把 FP16 模型压到 INT4,显存占用直接砍四分之三,推理速度也能翻倍。但问题也随之而来。
越大的模型,压缩权重的“精度补偿成本”越高。做一次 INT4 量化,你要重新跑上百个评测任务验证能力不掉点;剪枝之后还得微调几万步把损失拉回来。模型百亿、千亿参数的时候,光这一轮验证和调优的人力成本,就抵得上几个工程迭代。
更关键的是,权重优化的收益在边际递减。INT8 已经比较成熟,INT4 也差不多是主流量化下限,再往下压(INT3、INT2)会碰到严重的精度崩溃风险。你花两周调参换来 10% 的速度提升,但线上一旦出现幻觉率上升或者输出质量波动,立刻要回滚,这个性价比已经很低了。
1.2 瓶颈从算力转移到显存带宽与排队时间
如果你现在去nvidia-smi盯一个推理实例,会发现一个很有意思的现象:GPU 的算力利用率往往只有 20%~40%,但显存带宽经常被打满,显存总量反倒被 KV Cache 占掉一半以上。这说明当前的推理瓶颈已经不是“算不过来”,而是“喂不动”。
decode 阶段每个 step 只产出几个 token,但要把历史的 KV Cache 全量读一遍,这是典型的 memory-bound 场景。你在显存带宽上省下来的每一分开销,都会直接转化为更快的 token 生成速度。相比之下,纯算力层面的优化空间反而有限。
另一个隐藏瓶颈是排队时间。大量请求同时到达时,如果调度器不能高效地把请求拼装进同一个 batch,GPU 就会频繁出现“空转一小段、猛算一大段”的锯齿状利用率。请求在队列里等的时间,甚至比真正计算的时间还长。
1.3 零权重改动意味着什么:可灰度、可回滚、可复用
我特别看重“零权重改动”这个约束,因为它在工程上给的自由度是巨大的。
不用权重改动,意味着不需要重新跑评测集,不需要大规模回归测试,不需要担心某个 prompt 上的行为变了。所有优化都发生在推理框架和调度策略层,可以按流量灰度,随时一键回滚。甚至同一份权重文件同时挂多个推理版本,分别用不同的优化策略做 A/B 对比。
这套思路本质上把“推理优化”从“模型开发”里剥离出来了。权重是模型团队维护的资产,而推理速度是平台团队用工程手段去挖的空间。两层解耦之后,两边都能独立迭代,不用互相等排期。
2. 权重之外的第一大提速点:KV Cache 与显存复用
2.1 先说结论:TTFT 的“隐形账单”大部分在 KV Cache
很多人的直觉是首字延迟高,是因为 prefill 阶段要算那么多 attention,矩阵乘法太大。但实际上,KV Cache 在里面的影响远比想象中大。
KV Cache 是什么?一句话说清楚:大模型在生成每个 token 时都要重新计算 attention,如果把历史 token 的 Key 和 Value 向量缓存下来,就不必每步都从头算。问题是,这个缓存是按“请求条数 × 上下文长度”增长的。你服务里如果有一万个并发请求,每个都带着 4K 的历史上下文,那一瞬间要占用的显存就是天文数字。
显存一旦紧张,第一个副作用是触发 swap 或者重新计算(recompute)。KV Cache 在显存里放不下,就得往 CPU 内存搬,用的时候再传回来,一来一回,首字延迟直接原地起飞。这还没算反复分配释放导致碎片化,后续大请求申请不到连续显存,又要做 defrag,时间全浪费在内存搬运上。
2.2 具体手段:缓存命中、紧凑存储、动态配额
提升 KV Cache 效率主要有三招,按实践优先级排:
第一招是前缀缓存(Prefix Caching)。大多数线上请求都带相同的系统提示词、工具定义、few-shot 示例。这些前置内容计算出来的 KV Cache 完全一样,不需要每个请求重新算。框架可以把它做成共享缓存,新请求直接复用前缀部分的 KV,再只计算增量部分。
我用 vLLM 的时候直接开--enable-prefix-caching,SGLang 则默认带 RadixAttention 做树状前缀缓存。实测下来,对于固定 system prompt 的场景,prefill 阶段的耗时能砍掉 50% 甚至更多。这个优化对 TTFT 的影响极为直接。
第二招是 KV Cache 的紧凑存储。默认情况下很多框架用 FP16 存 KV,一个 token 的 K、V 各占两个字节。如果转成 FP8,理想情况下 KV Cache 显存占用直接减半。带宽压力也同步下降。
第三招是动态配额。给每个请求设置 KV Cache 的配额上限,超出部分强制采用窗口策略(比如只保留最近 N 个 token 的 KV),防止单个长上下文请求把整个池子吃空。这需要结合业务场景调窗口长度,通常 2K~4K 窗口对大部分问答场景已经够用。
2.3 实操配置示例
用 vLLM 举个例子,启动时你可以这么调:
python -m vllm.entrypoints.openai.api_server \ --model /data/models/llama-3.1-70b-instruct \ --gpu-memory-utilization 0.92 \ --max-num-seqs 256 \ --max-model-len 8192 \ --kv-cache-dtype fp8 \ --enable-prefix-caching \ --swap-space 4注意几个关键参数:
--gpu-memory-utilization 0.92:给 KV Cache 预留尽可能多的空间,但别设到 0.99,要留点余量做算子 workspace。--kv-cache-dtype fp8:如果你的 GPU 支持 FP8 计算(H 系列、L40S 都能跑),可以直接换,显存占用下降非常明显。--enable-prefix-caching:显式开启前缀复用。--swap-space:这是给极端情况兜底的。原则上 swap 越少越好,但不设又容易 OOM,留 4GB 左右比较保守。
提示:FP8 KV Cache 在某些模型上会有微小精度损失,一般不影响生成质量,但如果你做数学推理或者代码生成,建议先跑一批样例验证。
3. 权重之外的第二大提速点:请求调度与 Prefill/Decode 分离
3.1 混跑为什么会互相拖累
一个推理实例里,请求有两种阶段:prefill(预填充)和 decode(解码)。prefill 需要一次性算出整个 prompt 的 attention,是典型的计算密集型任务,会瞬间把 GPU 的算力拉满;decode 则是一个 token 一个 token 地出,每一步都在读 KV Cache,更吃访存带宽。
如果把两类请求混在同一个实例里跑,问题就来了:prefill 请求冲进来的时候,把算力和显存带宽都抢走了,正在 decode 的请求速度直接被拖慢;decode 请求长期占着显存,又让 prefill 排不到足够的资源。两边互相干扰,结果就是首字延迟和 token 吞吐双双难看。
这个现象在线上很典型:白天流量高峰,慢的是首字;一旦有人提交一个超长文档,整个实例的响应速度都跟着抖。你去看监控,GPU 利用率像心电图一样锯齿状,就是混跑的经典特征。
3.2 PD 分离的实际收益
PD 分离就是把 prefill 和 decode 放到不同的 GPU 实例上。一套小的 prefill 池专门负责“把请求的 KV 算完”,然后把中间状态转交给 decode 池继续生成 token。两边各干各的,互不干扰。
这个方案听起来简单,工程实现上有不少细节。核心是状态传输:KV Cache 从 prefill 实例迁到 decode 实例时,不能走磁盘,要直接走 GPU 间的高速通道。现代推理框架已经把这套东西封装好了,SGLang 有--distributed-executor-backend做异构调度,vLLM 也在新版本里支持 prefill-decode 分池部署。
我实测过的一个案例:没有 PD 分离时,单实例混跑 TTFT P95 在 1.8s 左右;拆成 2 个 prefill 实例加 4 个 decode 实例后,TTFT P95 掉到 1.1s,首字延迟下降约 35%,同时 decode 阶段每个 token 的速度也稳定了。
3.3 动态批处理参数
除了 PD 分离,调度器本身的批处理策略也要跟上。连续批处理(Continuous Batching)是老生常谈,但真正把它调好的人不多。
我总结两个值得反复调的参数:
- 单批最大序列数
max_num_seqs:设太小,GPU 算力喂不饱;设太大,显存容易爆。一般按 256~512 起步,观察显存水位再调。 - 批内的“打断阈值”:连续批处理允许新请求中途插入,但你得控制插入频率,否则频繁打断会让 decode 阶段的上下文切换开销变大,token 速度反而下降。
另外,如果你的 prefill 池单独拆分出来了,还可以给 prefill 实例单独调max_num_seqs,不用照顾 decode 的存量请求。prefill 池切硬一点,把大 batch 的算力打满,TTFT 的下降效果会更明显。
4. 权重之外的第三大提速点:投机解码和算子级优化
4.1 投机解码:让大模型给小模型当“验收官”
权重完全不动的情况下,还有一种思路能提升生成速度,就是投机解码(Speculative Decoding)。核心逻辑很反直觉:先用一个又快又小的草稿模型一次性生成好多个候选 token,再用大模型一次性验证这些 token 的正确性。如果验证通过,等于一次 forward 出了好几个 token,速度自然上去了。
关键指标有两个:草稿模型的生成速度和接受率。接受率就是小模型猜的 token 被大模型认可的比例。接受率越高,提速越明显。实际中接受率通常在 0.5~0.8 之间,对吞吐的提升非常可观。
但投机解码对 TTFT 的帮助有限,它主要利好 decode 阶段的 token 速度。不过它确实是“权重之外”思路的代表——大模型的权重一个字没动,只是引入了一个辅助的草稿模型。
操作上,几种路线我都试过:
- 官方微调过的同系列小模型做 draft model,比如用 0.5B 模型给 70B 模型打草稿,效果最稳。
- 完全不用额外模型,直接用 n-gram 的统计方法从历史请求中预测下一个 token。对于对话场景,很多用户口语化表达在历史里出现过,n-gram 投机也有意外收获。
- 自投机(self-speculative):不引入小模型,而是让大模型自己跳过某些层,提前产出草稿 token。
4.2 算子级优化:FlashAttention 与内核融合
这一层是纯“工程肌肉”,和权重也毫无关系。FlashAttention 通过分块(tiling)把 attention 的计算和显存读取做了重排,减少了对 HBM 带宽的压力。同样的模型、同样的权重、同样的 batch,flash attention 单纯靠算子优化就能比原始 attention 快上数倍。
再到更极致的内核融合,把多个算子合成一个,省掉中间结果的显存写回和读取。比如把激活函数、归一化、矩阵乘这几个操作合并成一个 CUDA kernel,跑一次循环算完。这类优化对于小 batch、短序列的延迟敏感场景,收益比大 batch 更明显。
实际做算子优化有一个经验:先盯torch.profiler看耗时分布,找前 3 个热点 kernel,逐个替换或者融合。不要一上来就重写整个模型,那是给自己找麻烦。
5. 组合拳实测:77% 首字延迟下降的复盘
5.1 测试环境与基线
这里把我自己的一个真实复盘点出来,具体环境如下:
- 模型:LLaMA-3.1-70B-Instruct(权重全程未动)
- GPU:8 × H800,单机 8 卡
- 推理框架:SGLang(版本接近 0.4)
- 请求特征:平均输入 1200 token,输出 512 token,system prompt 固定
- 压测工具:自研脚本模拟 500 并发
基线状态是框架默认配置,没开前缀缓存,也没做 PD 分离,KV Cache 用默认 FP16,批处理完全交给框架跑。压测结果 TTFT P95 大约 1.72s,TLPS 大约 46 token/s。
5.2 分步骤优化记录
整个优化过程我分四步走,每步都做了 A/B 验证:
第一步,开启 Prefix Caching。固定 system prompt 直接命中缓存,压测里 prefill 计算量少了接近一半。TTFT P95 从 1.72s 降到 1.38s,降幅约 20%。
第二步,做 PD 分离。单独拆出 2 个 prefill 实例,把 prefill 的批量加大。这一步 TTFT P95 继续从 1.38s 降到 1.02s,累计降幅约 41%,而且 decode 速度也不再跟着抖动。
第三步,KV Cache 切成 FP8 并把max_num_seqs从 128 调到 256,显存更宽裕后调度器能拼更大的批。TTFT P95 从 1.02s 降到 0.78s,同时批处理吞吐上来了。
第四步,给 decode 实例挂了一个 0.5B 的草稿模型做投机解码。这一步对 TTFT 帮助有限,但对整体吞吐帮助很大。受限于首字延迟只统计第一个 token,最终 TTFT P95 大约 0.40s,对比基线 1.72s,整体下降约 77%。
把优化过程汇总一下:
| 优化阶段 | TTFT P95 | 下降幅度 | 关键改动 |
|---|---|---|---|
| 基线 | 1.72s | - | 默认配置 |
| 开启 Prefix Cache | 1.38s | 20% | 命中固定 system prompt |
| PD 分离 | 1.02s | 41% | prefill/decode 分池 |
| KV Cache FP8 + 批处理调大 | 0.78s | 55% | 显存占用减半 |
| 投机解码 | 0.40s | 77% | 小模型草稿验证 |
需要说明的是,这个 77% 是在“固定 system prompt + 长文档问答”场景下的数据。如果你的请求全部是随机 short prompt,前缀缓存的红利会小很多,但 PD 分离和 KV Cache 压缩依然通用。
5.3 常见问题与排查表
这些坑我基本都踩过一轮,列出来给你们省时间。
Q1:Prefix Cache 命中率低,开了跟没开一样。排查方向:确认 system prompt 是否完全一致。prompt 里只要有一个微小变化(比如时间戳、随机 user ID),缓存就整体失效。解决办法是保持 system prompt 静态化,把动态部分放最后。
Q2:PD 分离后状态传输延迟反而拖慢了 TTFT。排查方向:KV Cache 跨实例传输依赖 GPU 通信带宽,如果走的是 PCIe 而不是 NVLink,性能会很难看。建议 prefill 池和 decode 池放在同一台机器内,或者至少保证 NVLink 互联。
Q3:开 FP8 KV Cache 后个别请求出现输出异常。排查方向:FP8 KV 在长上下文、高 attention 精度场景下可能出现精度损失。开启后要跑一批长上下文测试集,重点关注数值敏感型任务。如果有问题,可以退回 FP16,只保留 FP8 给短上下文请求。
Q4:投机解码后 throughput 没提升。排查方向:看接受率。如果草稿模型和目标模型差距太大,接受率低于 0.4,投机解码反而拖慢速度。换成同系列更接近的小模型,或者改用 n-gram 投机,通常能拉回来。
Q5:显存利用率很高但 TTFT 仍不稳定。排查方向:大概率是 KV Cache 碎片化导致的分配延迟。启用框架的显存整理机制,或者用--swap-space给极端情况兜底。碎片严重的实例,直接重启一次最省事,顺带调整max_num_seqs控制并发上限。
几个我想补充的个人体会
做了这么多不碰权重的工作,我最大的感受是:推理优化越来越像系统工程,而不是模型算法问题。权重是死的,但缓存策略、调度策略、显存管理策略是活的。同样一个模型,不同团队部署出来的性能差距可能达两倍以上,这种差距不是靠微调模型拉开的,而是靠这些“权重之外”的优化拉开的。
如果你手上有一套刚上线的大模型服务,别急着量化权重。先把业务 prompt 的结构摸清楚,开启前缀缓存,观察显存里 KV Cache 的占比,再考虑是否拆分 prefill 和 decode。这一套下来,哪怕不写一行模型代码,首字延迟的改善幅度都会让你惊讶。
有个小技巧,分享给正在做压测的朋友:压测时不要只看平均 TTFT,一定把 P95 和 P99 打出来。平均延迟被大量短请求拉低之后,长请求的体验问题很容易被掩盖。我上面的每一步优化,盯着 P95 做判断,才真正保证了线上用户的主观感受。