最近看到不少关于AI推理服务批处理优化的讨论,我也来聊聊实际落地中的体会。核心突破点其实在于动态批处理(dynamic batching)的调度策略——静态batch size在真实请求波动下反而容易导致资源浪费,而基于时间窗口或请求积压的调度能显著提升吞吐。比如我们测试过,将最大延迟容忍从50ms放宽到100ms,吞吐量能提升约40%,但前提是模型推理的显存占用要提前做好profile。个人经验是,很多人只盯着batch size调优,忽略了padding和序列长度对齐的优化。实际上,对于Transformer类模型,不等长序列的padding浪费可能比想象中严重,用动态padding或分桶策略能减少30%以上的无效计算。我有个问题想和大家探讨:你们在生产环境里是优先保证低延迟还是高吞吐?如何权衡请求排队与batch填充的效率?另外,从行业趋势看,随着Serving框架(如vLLM、TGI)对连续批处理(continuous batching)的支持逐渐成熟,静态批处理可能被逐步取代,但这对显存管理和调度器的复杂性要求更高,小团队是否该直接上这类方案还是先优化基础策略?欢迎有实战经验的同行分享。
批处理推理没那么简单:我踩过的吞吐量优化坑
全部回复
共 23 条说到动态padding这块我深有同感,之前调一个BERT的线上服务,序列长度分布特别散,80%的请求都在100 token以内,但偶尔有几个上千的。一开始没做分桶,直接全局padding到最大长度,显存直接被那几个长序列吃干净,batch size根本提不上去。后来按128、256、512做了三个桶,吞吐直接翻倍,延迟还更稳定了。
另外想请教一下你提到的基于时间窗口的调度,你们是怎么处理请求积压和延迟之间的trade-off的?我们之前试过简单的积压阈值触发,但流量波动大的时候,要么频繁触发导致batch太
小,要么积压太多超时。后来改成自适应窗口,根据最近几秒的平均请求到达率动态调整等待时间,但调参又是个坑,不同模型的推理耗时差异很大,窗口参数的鲁棒性很难保证。
还有个细节想补充,就是显存profile这块,很多人只关注了模型参数和KV cache的静态占用,但实际推理时torch的缓存分配器会有碎片问题,尤其是动态shape场景下,频繁申请释放显存,碎片率能到20%以上。我们后来用了cuda memory pool预分配,配合分桶推理,显存利用率才稳定下来。你们在profile阶段有没有遇到类似的碎片问题?
之前调动态批处理也踩过类似的坑,特别是padding浪费那个点,确实容易被忽略。我刚开始试的时候,直接拿固定最大长度去补,结果显存占用直接炸了,后来才发现分桶+动态padding能好很多。不过想请教一下,你实际落地的时候,时间窗口和请求积压这两种调度策略,在延迟敏感和吞吐优先的场景下具体怎么选?比如我们这边有些业务需要p99小于50ms,但另外一些离线任务可以容忍200ms,感觉同一个模型服务不太好一刀切。
另外,你提到profile显存占用,这个具体是怎么做的?我试过用torch的memory summary,但感觉动态batch的时候,显存峰值和实际可用量之间还有不少buffer,有时候看着显存没满,但一调大batch size就OOM了,是不是跟CUDA的碎片化有关系?还有那个序列长度对齐,除了分桶,有没有试过用某种排序策略,比如把相近长度的请求凑一起再批处理?我听说有些框架支持按请求到达时间+长度做两层调度,但没试过,不知道效果怎么样。
动态padding这块真是说到了痛处,我之前也踩过类似的坑。当时图省事直接上了静态padding,结果跑起来发现显存占用比预期高了一大截,尤其是有几个长序列请求混进来的时候,短序列的padding占比能到60%以上,纯纯的浪费。后来改成按桶分批,比如把序列长度按128、256、512这样分桶,每个桶内再动态padding,吞吐直接涨了30%多,而且对延迟影响很小。
不过想请教一下你们那个基于时间窗口的调度具体是怎么做的?我试过按请求积压量来触发批处理,但遇到流量突发的时候,要么batch size炸了显存OOM,要么积压时间太长导
致超时。后来我改成用滑动窗口+最大batch size上限的双重控制,窗口内积压到一定阈值就触发推理,同时强制限制最大batch数,这样既保证了吞吐又不会爆显存。你们那40%的提升是在什么模型上测的?我这边在LLaMA类模型上测,放宽延迟到100ms确实有收益,但过了某个点之后提升就边际递减了,感觉跟模型的计算特性也有关系。
另外补充一点,除了padding,prefill和decode阶段的显存分配策略其实也值得单独优化,我们之前发现decode阶段的KV cache碎片化也挺严重的,后来做了显存预分配和复用,才把整体吞吐稳住。