有段时间,一个内部工具的回答突然慢了下来。模型没换,提示词没改,机器也没报警,只是原本两秒出结果的问题,开始要等上七八秒。排查一圈才发现,请求量涨了不到一倍,但每台机器上同时排队的任务变多了。单次推理的速度几乎没变,变慢的是等待。
这类问题很常见。人们谈推理优化,第一反应往往是让模型算得更快:换更小的模型、做量化、上更快的硬件。这些当然有用,但落到真实系统里,速度常常不是被某一次计算拖住的,而是被一整条链路的配合拖住的。谁先算、谁后算、显存够不够同时放下几个请求、输出是一个字一个字吐还是一次性给完,都会改变用户感受到的那几秒。
被忽略的那部分时间
理解推理优化,先要承认一个朴素事实:模型生成内容的节奏和人的阅读节奏不一样。模型每输出一个词元,都要重新走一遍计算。如果一次只服务一个人,机器大部分时间在等下一步,利用率并不高。于是工程上会把多个请求拼在一起算,让空闲的算力被填上。这一步能显著提升吞吐,但也带来新的麻烦:一个长请求会让同批次的短请求一起等,首字延迟被拉长。
于是就出现了取舍。想让更多人同时用,往往要接受单个请求稍慢一点;想让每个请求都快,就得留出更多余量,成本随之上升。推理优化在这里更像排班,而不是单纯踩油门。一个系统是否顺畅,取决于它怎么判断哪些请求可以合并、哪些应该插队、什么时候宁可空转也不硬塞。
- 合并请求能提高机器利用率,但要控制批次规模,避免长请求拖累短请求。
- 缓存重复的前缀内容,可以让多轮对话少算一些已经算过的部分,但缓存本身也占显存。
- 把一部分计算提前做完,能减少实时压力,代价是灵活性下降。
速度之外,还有稳定性
另一个容易被低估的点是显存。推理过程中保存的中间状态会随着并发数增长而增长,一旦逼近上限,系统要么拒绝新请求,要么开始频繁换进换出,表现就是时快时慢。很多所谓“优化后反而更卡”的情况,根子不在算法,而在资源调度没有跟上。做推理优化时,如果只盯着单次延迟的数值,很容易忽略系统在高负载下的抖动。
我倾向于把这类工作分成两层看。一层是模型层面的精简,比如减少冗余计算、用更省的方式表达权重;另一层是服务层面的编排,比如请求怎么排队、缓存怎么复用、超时怎么处理。两层都叫推理优化,但解决的问题并不相同。前者让每一步更轻,后者让整体更稳。只做前者,系统在低负载下会很好看,一上量就露出短板;只做后者,天花板又摆在那里。
边界在哪里
也需要说清楚,推理优化不是万能的。它能让同样的硬件服务更多人,能让响应更跟手,但它改变不了模型本身的能力上限,也补不上信息不足带来的错误。有些场景对延迟极其敏感,比如需要即时反馈的交互,这时候优化的重点是缩短首字等待;有些场景更在意结果完整,慢一点反而可以接受。把优化目标定成“越快越好”,很容易做出一个数字好看、体验别扭的系统。
回到开头那个变慢的工具。最后的处理并不复杂:限制了单批次的大小,给短请求留了一条快通道,又把重复的上下文做了缓存。单次推理的耗时几乎没动,但等待时间回到了可接受的范围。这件事让我更愿意把推理优化理解成一种对资源与耐心的安排——不是把某一步压到极致,而是让整条链路在真实负载下保持体面。