ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

权重之外:一次让LLM首字延迟下降77%的系统优化复盘

权重之外:一次让LLM首字延迟下降77%的系统优化复盘

最近复盘线上LLM推理服务的提速记录,我发现自己做了一件很有意思的事:模型权重文件从头到尾没有动过一行,但服务的首字延迟下降了77%。身边很多人一听到推理提速,第一反应就是改权重——上LoRA、做剪枝、搞蒸馏,似乎不动权重就没有优化空间。可在2026年这个节点上,被反复验证的高收益优化,几乎全发生在权重之外。

权重决定模型的上限,但决定用户多快看到第一个字的是另一条链路:请求排队、显存分配、KV Cache调度、预填充阶段的计算效率、算子执行速度。这些环节里的每一次优化,都不需要触碰权重本身。这篇文章算是我对这次优化的一次完整复盘,既包括思路层面的底层逻辑,也包括可以直接抄走的配置清单,还有一些从开源权重下载那一刻起就能用上的提速套路。

1. 为什么提速的火力都集中在权重之外

1.1 改权重是慢变量,系统优化才是快变量

我见过太多团队拿到延迟指标后,第一件事就是召集算法同学开会讨论要不要做一个更小的模型,或者拿LoRA微调一下。不是说这个方向没有用,而是它的成本周期完全不适合做在线服务的性能优化。

动权重的路径无非几条:结构化剪枝、蒸馏、低秩微调,还有最极端的权重矩阵分解。它们能带来模型体积和浮点运算量的下降,理论上推理延迟也会跟着低。但这里面每一步都需要训练数据、GPU训练卡、效果回归和多轮评测。剪枝剪掉一个层,可能让某个任务的能力悄悄滑坡;微调想把能力拉回来,通常半个月就打底。对线上业务来说,这个时间窗口太长了。

系统侧优化完全不同。改一个调度参数、开启一段前缀缓存,代码变更量可能只有几行,甚至只是推理引擎的启动参数,几十分钟就能验证效果,风险上限也低。我常打一个比方:改权重是给引擎换凸轮轴,系统优化是把油箱加满、把路况规划好、把红灯变绿灯。前者技术含量高,但慢;后者见效快,而且立竿见影。当前的推理优化趋势之所以越来越倾向于先动后者,就是因为它的投入产出比太诱人了。

1.2 首字延迟到底由什么组成

要理解为什么权重之外能挤出77%,得先把首字延迟这个大指标拆开。首字延迟(TTFT)是从用户发出请求到模型吐出第一个token之间的总时间,它不是单一计算环节,而是整条服务链路的叠加。

在我的实测环境里,一次请求的时间大致分成五段:排队等待时间、请求解析与tokenize、KV Cache的显存分配、预填充(prefill)阶段计算,以及生成第一个token的采样和传输开销。其中排队时间和显存分配属于纯系统行为,和权重数值无关;prefill阶段是真正调用模型算力的部分,但它消耗的时间取决于prompt token的长度、注意力实现方式和算子效率,权重参数本身只决定了每一轮矩阵乘法的浮点规模,而不是这个阶段的全部。

权重会通过参数量影响矩阵乘法的总运算量,但即使把模型从7B换成13B,如果prefill的算子写得烂、显存反复分配、请求在前台排队,延迟照样惨不忍睹。反过来也是一样:用一个高效的调度器和缓存策略,大模型的端到端体验往往比裸奔的小模型还要好。所以,当我们讨论"推理提速为什么都发生在权重之外",本质是承认系统侧还有大量低垂的果子没人摘。

这里还要提醒一句:衡量首字延迟一定要看分位数,别只盯平均值。平均值容易被少数长尾巴请求拉平,真正影响用户感受的是P95甚至是P99的体验。后续所有优化,我都会用P50和P95两个口径来记录。

2. 四大权重外引擎,把首字延迟按在地上摩擦

2.1 前缀缓存:最直接的那一刀

这轮77%的降幅里,前缀缓存贡献最大。原理很简单:真实业务里的prompt并不是纯随机拼接,而是高度结构化——前面经常是一段固定的系统提示词、几轮固定的few-shot示例,后面才接着用户输入。这段固定前缀每次都要重新走一遍prefill,计算完全重复。

前缀缓存做的事情,就是把已经计算过的前缀KV Cache放进显存池,下一次遇到相同前缀的请求时直接复用,跳过重复的prefill计算。我这边的实现用的是推理框架自带的能力(vLLM对应enable_prefix_caching,SGLang则用RadixAttention做前缀树索引),它对使用者是透明的:不需要改动任何权重文件,也不要求改模型结构,唯一要做的就是开启开关和保证prompt前缀的一致性。

它的加速上限取决于两个数字:公共前缀占整个prompt的比例,以及前缀缓存池的容量。我的业务场景里,固定前缀占prompt token数的六成到七成五左右,命中后prefill耗时直接砍掉一大截。可以算一笔账:假设prefill占了首字延迟的大头,命中后这部分计算变成一次显存读取,耗时从240毫秒左右降到60毫秒,首字延迟自然断崖式下跌。

需要注意,前缀缓存不是完全免费。被缓存的KV会长期占据显存,如果你把缓存池开得过大,挤压了正常计算所需的显存,服务吞吐反而会下降。我建议结合请求的真实前缀分布先把缓存上限划出来,再通过命中率指标持续观察。

提示:前缀缓存池不是越大越好。建议先用线上请求回放估算出最大KV需求,再通过命中率指标反向调整上限。

2.2 预填充阶段加速:把算力花在刀刃上

哪怕前缀缓存命中了,剩下的那部分prefill依然要扎扎实实地跑。因为prefill是并行计算,所有输入token一次过完,它的优化空间和decoder阶段的串行特性完全不同,所以要单独说。

这一层我主要靠三件事。第一是FlashAttention这类高效的注意力实现,它把注意力矩阵计算从显存里反复读写改成算子内部流式处理,对长prompt 的prefill提升非常明显。第二是算子和图的优化,比如CUDA Graph捕获并预热模型,避免每次请求都重新创建kernel;把多个小算子融合成一个kernel,减少GPU发射指令的开销。第三是KV Cache用量管理。KV Cache是prefill的核心产物,如果做成固定连续分配,长请求可能产生严重显存碎片;换成页式分配之后,显存利用率高得多,同时还能把缓存池做得更大。

另外一个值得单独说的东西是分块预填充,也叫Chunked Prefill。不切分时,一个超长prompt会独占GPU很长时间,其他排队请求全部干等。按块切分后,prefill可以拆开穿插到别的请求的decode间隙执行,排队时间大幅缩短。它是牺牲单请求一点首字延迟,换全局整体低延迟的典型取舍,也是权重之外系统优化最体现工程水平的地方。

我实测下来的感受是,prefill优化很难有单一开关能一步到位,它更像组合拳。FlashAttention换掉、显存页式化、分块预填充打开,三者叠加后prefill耗时能降到原来的六成到七成,再配合前缀缓存,效果翻倍。

2.3 投机解码与并行解码:从串行里挤时间

prefill快起来之后,下一个瓶颈是自回归解码的串行特性。大模型生成是一个token一个token往外蹦的,每一步都依赖上一步的输出,这个串行特性让计算单元大量空转。

投机解码的思路是用一个便宜的小模型先解码出几个候选token,再用大模型并行验证。验证正确就一次向前跳好几步,总生成时间能减少三到五成。它对于首字延迟本身的直接贡献有限——因为第一个token往往还是要小模型先跑一步——但当整个服务的吞吐提升后,请求排队时间下降,用户感知的响应速度同样会变好。如果你把TTFT理解为从发出请求到看到完整回答的第一屏内容,投机解码带来的吞吐提升同样会直接改善这个感知。

如果你想要完全零权重的投机解码,还有两条路可以走。一条是用n-gram草稿模型,完全不需要额外训练,直接从历史文本构造候选;另一条是让模型自己扮演草稿模型,用浅层解码的结果当草稿。两者都省去了训练小模型的成本,属于真正意义上"一行权重都不改"的提速手段。

我建议在prefill和缓存优化做完之后再看投机解码。它的收益对不同的任务分布很敏感。代码生成、结构化输出这类可预测性强的场景收益很大,自由闲聊场景收益就没那么稳定。

2.4 连续批处理与调度:别让请求在路上堵车

在真实在线服务里,首字延迟里最容易被人忽略的一块是排队时间。传统的静态批处理会把请求攒成固定批次再一起处理,一个长请求没结束,后面的请求就一直卡着,GPU利用率波动极大。

连续批处理把批处理粒度从"请求级"细化到"token级":只要一个请求的某个token生成完了,就把这轮空闲的计算位让给新来的请求。新请求几乎可以立刻插入当前批次,等待时间从"等整个批次跑完"变成"等几个token结束"。这直接砍掉了排队时间的大头。

调度策略也有讲究。默认的先来先服务很公平,但一个长响应的请求会堵住后面的一堆短请求。我上线时给调度器加入了优先级队列,让短请求优先插队,系统的P95首字延迟肉眼可见地在下降。做这一步就要想清楚业务性质的取舍,不是所有场景都适合短请求优先,但绝大多数聊天和问答类场景值得试。

在vLLM这类框架里,连续批处理基本是默认开启的,需要去调的是max_num_seqs和max_num_batched_tokens,用来控制每一轮GPU调度承受的batch大小。调得太大,显存会撑爆;调得太小,GPU又喂不满。我通常会把max_num_batched_tokens压到与模型单次attention吞吐匹配的水平,再通过GPU利用率来上下修正。

3. 0行权重改动复现77%下降的实操记录

3.1 先给首字延迟建立一条可复测的基准线

在动任何配置之前,先把基线打扎实。这一步别偷懒,否则后面所有优化数据都没有说服力。我用的是一套固定负载脚本:模拟客户端并发请求,保持prompt长度分布和业务线上一致,压测持续十分钟,分别记录P50与P95的首字延迟、prefill耗时、排队时间。

压测时最容易被忽视的是缓存状态。如果你先发了一轮请求预热了前缀缓存,再测命中后的延迟,数据自然会好看很多,但这就掩盖了冷启动的问题。我的做法是把优化前和优化后的两次测试都在冷缓存状态下启动,再分别跑第二轮热缓存,两套数据都记录。

工具方面,我推荐简单直接的指标采集:客户端记录完整耗时,服务端用vLLM或SGLang暴露的metrics拉取prefill耗时和调度队列长度。两边对照才能定位延迟到底花在排队、计算还是网络传输上。

注意:压测时请记录两套数据——冷缓存和热缓存。只报热缓存结果会让优化数字虚高。

3.2 关键配置改动清单:一行权重都不碰

下面这张表是我这次优化过程中真正动过的地方,全部属于推理引擎和系统层面的配置,模型权重文件从头到尾没有变化。

配置项我的设置作用
前缀缓存enable_prefix_caching / RadixAttention 开启复用相同前缀的KV Cache,砍掉重复prefill
分块预填充chunked_prefill 开启避免长prompt霸占GPU,缩短排队
调度批次max_num_batched_tokens 调低提高GPU调度频率,降低首token等待
优先级调度开启短请求优先压低P95首字延迟
KV Cache精度kv_cache_dtype=fp8减半KV显存占用,扩大缓存池
显存利用率gpu_memory_utilization=0.85给KV缓存留足空间
上下文长度max_model_len 收紧减少显存过度预留和碎片

这些配置项看似琐碎,但每一个都对应首字延迟的某个组成环节。前缀缓存打的是prefill计算,分块预填充和调度策略打的是排队时间,KV Cache精度打的是显存容量约束,上下文长度打的是分配浪费。它们全部和权重数值无关,因此也就不需要做任何模型参数层面的回归测试。

有一点要特别注意:配置之间是有耦合的。比如KV Cache量化成FP8后,单个请求占用的显存变小,你能开更大的缓存池,前缀缓存命中率也会跟着上升。这些改进是乘法效应而非加法效应,这也是为什么组合起来能拿到77%这种看似夸张的降幅。

3.3 实测对比:从数据看时间到底去了哪里

我把优化前后的关键指标整理在一张表里,所有数据来自同一套压测负载和同一台GPU,唯一区别就是上面那批配置项。

指标优化前优化后变化
TTFT P50850 ms195 ms下降77%
TTFT P952100 ms480 ms下降77%
KV Cache命中率0%82%--
prefill计算耗时P50240 ms58 ms下降76%
排队等待时间P50480 ms60 ms下降87%
其他开销P50130 ms77 ms下降41%

三块组成相加正好是850毫秒到195毫秒,时间去向非常清楚:排队从480压到60,prefill从240压到58,剩下的解析和分配开销也从130降到77。正是因为每一块都被对症下药,最终端到端才有77%的整体降幅。

看到这个数据,我对"推理提速发生在权重之外"有了更直白的理解:如果当时一门心思干模型瘦身,大概率折腾一个月也未必能拿到这个降幅。可把同样精力放在前缀缓存和调度上,效率完全不同。

4. 权重之外,还有哪些隐藏入口

4.1 权重衰减不等于推理提速

很多朋友一谈到权重就想到训练,尤其会问"权重衰减对推理提速有没有帮助"。这里先澄清一下:权重衰减是训练阶段优化器里的L2正则化项,它作用于损失函数,让权重的数值在收敛时不至于过大。它不改变推理阶段的任何计算路径,也绝不会让模型跑得更快。

但它和权重之外的提速有一点间接关系。一个正常训练并配有合理权重衰减的模型,权重分布通常更平滑、极端离群值更少,这会直接影响到后训练压缩和量化的难度。我在做FP8 KV Cache这类压缩方案时发现,离群值越少的模型,压缩后的精度损失越小,推理框架也越敢于激进地降低显存占用。

换句话说,权重对提速的真正贡献在于"被压缩时的稳定性",而不是"被改动时的加速"。这句话建议所有做推理优化的朋友记下来,它能帮你分清哪些工作属于模型团队,哪些属于推理工程。

4.2 开源权重下载之后:DINOv3、SAM3、DEIM的适配套路

标题涉及的推理优化不只是大语言模型,视觉模型同样走"权重之外"的路。像DINOv3、SAM3这类视觉基础模型,以及DEIM这类检测模型在COCO上的预训练权重,拿到手往往是一个PyTorch的checkpoint。它能不能跑得快,取决于你接下来在权重之外做了多少事情。

常规套路是把checkpoint从训练框架里剥出来,转成ONNX或TensorRT的推理中间表示,再套上推理引擎跑。在这一步里,权重数值完全没有变化,但推理延迟能差出几倍,原因就是训练框架里那些动态shape、autograd开销和重复分配在ONNX或TensorRT里被静态化处理了。DINOv3的ViT结构对这种优化很友好,DETR一派的检测模型则还需要处理后处理里的解码头和NMS,这些也都是权重之外的工程活。

这类视觉权重还有个共同点:预处理和后处理往往比模型本身更吃CPU和内存带宽。我在部署SAM3时,把图像缩放、padding和归一化的流程全部向量化,并和GPU推理流水重叠,端到端吞吐反而比优化模型算子提升得更多。这些经验再次印证,权重只是一个起点,速度从来都不是权重自带的属性。

4.3 压榨算子和通信的最后一公里

如果前几层优化都做完了,还想继续深挖,那就得往更细的"权重运行环境"里钻。首先是算子级替换,比如把FlashAttention的版本升级到更新一代,或换用针对特定GPU架构手工调优的kernel,部分矩阵乘算子能提升10%到20%。

其次是多卡部署时的通信优化。用张量并行或流水线并行时,GPU之间要做allreduce通信,通信和计算能不能重叠对延迟影响很大。为什么很多线上服务把TP规模控制在2卡或4卡,而不是8卡?不是算力不够,而是通信开销会把收益吃掉。这个判断需要在权重之外的调度层面做出取舍。

最后是运行时层面的琐碎优化:显存池复用、锁竞争消除、Python解释器开销、请求解析的字符串处理。这些听着不像新技术,但在一轮高并发下,每个环节省下的几毫秒都会积少成多。我是把它们当作"权重之外的最后5%"来看待的。

5. 常见问题与排查实录

5.1 为什么我的缓存命中率就是上不去

开启前缀缓存后,第一件事就是盯命中率。常见原因是prompt前缀不够稳定。很多人会在系统提示词里拼上当前时间戳、随机用户ID甚至动态生成的工具描述,每次请求前缀都不同,缓存自然永远打不中。

排查方式很简单:把线上真实请求的prompt前缀打出来看一遍,找出哪些字段是动态拼接的,挪到指令内联的地方去,让固定前缀尽可能长。还有一个容易被忽略的原因是缓存池太小,KV被频繁驱逐。这时就要加大gpu_memory_utilization,或者降低并发请求带来的KV压力。

5.2 改完配置之后首字延迟反而变慢,怎么排查

有一回我把max_num_batched_tokens调得很小,想着让GPU调度更频繁,结果每个batch太小,GPU利用率崩了,吞吐下降,排队反而更长。这个教训说明配置不是越低越好,它跟负载模式强相关。

排查建议是二分法:先关闭所有新增配置,回到基线,确认数据恢复正常;然后一个一个开,每开一个就跑一次压测,找出那个拖后腿的参数。同时看服务端日志里的调度器和显存占用,绝大多数"变慢"其实是资源被某种配置挤爆后触发的重试和等待。

5.3 KV Cache量化之后精度掉了,权重之外怎么补救

KV Cache从BF16压成FP8或INT8,显存占用缩小一半,但长上下文场景下精度损失会被放大。补救手段有几种:一个是给KV Cache加per-channel或per-token的scale,让离群值被单独照顾;一个是开启异常值补偿,把个别数值较大的通道保留在高精度格式里。这些都不需要动权重,只影响推理层的存储与计算表示。

我的建议是先用FP8过渡,验证线上核心业务指标不掉再考虑INT8。量化收益很大,但要在精度和速度之间拿到自己的平衡点,而不是照着别人的配置照抄。

这次做下来,我个人最大的体会是:推理提速的优先级排序里,系统侧的权重之外优化,应该排在模型改动之前。权重是模型能力的根,动它是大事;权重之外的问题是工程问题,每天都能上手,每天都有反馈。如果你正卡在首字延迟下不来的困境里,先别急着做模型瘦身,从缓存、调度和显存布局开始排查,这个顺序我测了很多次,极少让人失望。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进