news 2026/10/19 2:56:57

AI模型推理批量执行优化:从动态聚合到连续批处理的实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AI模型推理批量执行优化:从动态聚合到连续批处理的实战指南

做推理服务部署的人,大概率都遇到过同一个让人抓狂的场景:模型权重看着也没问题,代码逻辑也通了,压测一跑,GPU 利用率只有 30% 上下,吞吐死活上不去。换卡太贵,改模型太慢,其实最容易被忽略、又最值得先动的一刀,就是 AI 模型推理的批量执行优化。

批量执行优化说白了就是让模型每次喂进去的数据从 1 条变 N 条,把硬件资源集中起来干一件更“大”的事。听起来简单,但真正落地时会牵扯到请求怎么攒、批次怎么定、变长输入怎么对齐、显存怎么分配、延迟怎么兜底。这一篇我打算把这个方案彻底拆开讲清楚,从原理到实操,从固定批处理到动态聚合再到连续批处理,把我踩过的坑和总结出的参数口诀一并放出来,给正在调推理服务的同学做个参考。

1. 单请求推理的资源账:批量优化为什么见效最快

1.1 一次推理请求的时间和显存去了哪里

不管是图像分类、目标检测还是大模型的文本生成,单条请求送到推理服务后,计算流程大体分三块:权重从显存/内存搬到计算单元、算子逐层执行、中间结果(激活值、KV Cache 等)的读写。很多人在调优时只盯住算子执行那部分,结果发现计算核心压根没吃饱,大多数时间都耗在等待数据搬运和 kernel 启动上。

我举个例子你就明白了。假设一次单请求推理实际需要 10ms,其中真正算矩阵乘法的部分只有 3ms,剩下的时间全花在加载权重、搬运中间张量和启动算子上了。如果一条条请求串行跑,硬件的计算单元相当于每 10ms 才干 3ms 的活,利用率只有三成——这也正好解释了为什么很多模型服务压测时 GPU 利用率特别难看。批量执行优化就是解决这个问题的:把多条请求放在同一个推理步骤里算,权重只加载一次,数据搬运成本被 N 份请求均摊,算力利用率自然就上去了。

从显存角度看也一样。一次推理不仅要存权重,还要给每条请求存中间激活和上下文缓存。请求数多了以后,权重这部分是共享的,但每条请求自己的状态是独占的。如果不做批量执行而是并发开多个进程/多份模型副本,每份副本都要完整加载一份权重,显存会被迅速吃光。批量推理的意义恰恰在于:只保留一份模型权重,让尽量多的请求共享这套权重并行计算。

1.2 算力利用率和带宽利用率:批量优化的双引擎

批量执行优化之所以效果显著,是因为它同时踩中了两个硬件瓶颈:算力利用率和内存带宽利用率。

先说算力。当前主流的 AI 加速硬件在设计上就偏向大规模矩阵运算,比如 GPU 上的张量核心、NPU 上的矩阵单元,它们擅长的是“一块大矩阵一次性算完”,而不是“一小块矩阵反复算很多次”。单条推理请求的小矩阵运算,连硬件的并行能力都喂不饱;但把 N 条请求拼成一个更大的 batch 后,矩阵形状变大,硬件并行度才能真正被发挥出来。从这个角度说,批量执行优化是在“迎合”硬件本身的架构特性。

再说带宽。推理过程中权重的读取量是很大的,尤其在 Transformer 类模型里,每生成一个 token 都要访问全部权重。单条请求时权重只被读一遍、算一小部分;批量推理时,同一份权重从显存读一次,却可以服务于 N 条请求的计算。相当于你花了一份搬运成本,干了 N 份活。带宽利用率提升的逻辑和“高频次小件快递不如打包发整车”是同一个道理。

这也是为什么我强烈建议:推理服务如果还没做批量优化,别急着上量化裁剪或者换硬件,先把 batch 的功课补上。它在不改模型结构、不损失精度的前提下,往往能带来数倍的吞吐提升,属于投入产出比最划算的一档优化。

2. 先确定执行形态:固定批量还是动态聚合

批量执行优化第一步,不是直接改代码把请求 list 攒起来,而是想清楚你的业务场景适合哪种批量形态。我自己把常见的方案分成两类:固定批量和动态聚合。选错了形态,后面所有调参都会很拧巴。

2.1 固定批量:离线任务的确定性打法

固定批量的意思很简单:等请求攒够了一个固定的数量(比如 32 条),再一次性交给模型推理。这个方案非常适合离线批量任务,比如数据分析、批量打分、异步内容生成、评测集推理。这些场景的请求不是用户实时等着的,晚几秒完全无所谓,核心诉求是吞吐越高越好。

落地时固定批量也最省心。只要把请求按顺序收集,凑够 N 条就触发推理,再把 N 个结果按原顺序返回。这里唯一要注意的是请求顺序别乱。用带索引的队列收集,推理时把 batch 传入模型,输出时按索引还原,否则下游拿到结果会对不上号。

不过固定批量搬到在线实时服务上,问题就暴露了。想象一个在线 API,平均每秒来 3 条请求,你设置的触发批量是 32。高峰期还好,请求一多很快能凑满;但低峰期可能一分钟都凑不满 32 条,所有用户都在干等,延迟直接爆掉。所以在线服务更合适的做法是动态聚合。

2.2 动态聚合:在线服务的排队窗口设计

动态聚合的核心思路是:请求进来不是立刻推理,而是先进队列,由调度逻辑每隔一小段时间收集一次,把窗口内到达的请求全部打包。触发条件通常有两个参数:最大批大小 max_batch_size 和最大等待时间 max_batch_delay_ms。只要满足“批内数量达到上限”或“距离首个请求到达超过设定时间”任一条件,就把队列里的请求发出去。

这两个参数实际上是在吞吐和延迟之间画了一条可调曲线。max_batch_size 决定一批最多能处理多少个请求;max_batch_delay_ms 则保证即使在请求稀疏时,也不会让用户无限等下去。我见过不少刚上手的人只设了 max_batch_size 没设 delay 超时,低峰期请求被永久挂起,排查半天才发现是窗口时间忘了配。

具体调参可以按下面这张对照表来理解:

场景max_batch_sizemax_batch_delay_ms效果
离线批量任务32 ~ 128不设或很长优先凑满,吞吐最大化
在线普通API8~1610~50延迟可控,吞吐有提升
强实时交互1~41~10延迟优先,牺牲部分吞吐

我还有个建议:动态聚合时要注意队头阻塞问题。当队列头部请求已经等待接近超时阈值,后面突然涌进来一大批请求,如果非要凑满整批再发,队头那个请求的延迟就会被严重拉长。更好的做法是窗口内尽早发包,宁可批次小一点也不要让最早进来的请求等太久。很多推理框架在实现时已经有这个逻辑,但如果你是自己写的队列调度,这块要特别注意。

2.3 批大小不是玄学:它由延迟预算决定

批大小到底设多少才合适?我有不少初期经验是完全靠拍脑袋:觉得 batch 大吞吐就高,于是直接怼到 128。结果显存吃紧、延迟尾升,线上反馈变差之后再灰溜溜降回来。后来我把延迟拆开算了一笔账,才发现批大小有明确的约束方向。

在线实时推理的总延迟大致等于“排队等待时间 + 推理执行时间 + 网络开销”。批量变大,推理执行时间通常比线性增长要温和(因为有硬件并行),但排队等待时间会随 batch 增大而显著恶化——尤其是当请求到达速率不够快时。你要先问自己:业务容忍的 p99 延迟是多少?如果答案是 200ms,那排队等待最多只能占 50~80ms,剩下的才是推理预算。按这个倒推,batch 一般就能锁在 8~16 附近。

离线场景就没有这个顾虑,p99 是小时级甚至“今晚跑完就行”,那 batch 可以放开到 64、128,甚至继续往上顶,直到显存或者收益递减曲线说停。总之批量执行优化里的批大小不是一个可以独立拍板的数字,它必须和你的服务等级协议(SLA)绑在一起看,先有延迟预算,再有 batch 配置。

3. 变长输入是批量执行的最大隐形杀手:Padding 策略实战

在视觉模型里,图片可以 resize 到统一尺寸,批量执行很省心。但 NLP/大模型推理场景,每一条请求的序列长度都不一样:有的用户问一句 10 个 token,有的丢进 2000 个 token 的长文档。把这些不同长度的输入拼进一个 batch,就必须做 padding——把短的补到和最长的一样长。

3.1 对齐成本的计算账

Padding 造成的浪费比你想象中更隐蔽。假设批内有 16 个请求,平均长度是 512 token,但其中最长的一条是 1024 token。为了把这 16 条拼在同一个 batch 里,系统会把所有短序列都补到 1024,等于算力翻了一倍,但真正有效的数据其实只有平均长度那一部分。

我在实际项目中测过一组数据:当平均请求长度只有最大长度的 60% 时,即使 batch 开得很满,有效算力利用率也只有六成左右。这在生成任务里尤其致命,因为整个生成过程的每一步都要处理完整的序列长度,padding 进去的无效 token 每个 step 都会跟着算一遍。

更麻烦的是 KV Cache 的内存占用。在大模型推理里,每条请求的 KV Cache 是按实际序列长度动态增长的,padding token 虽然不产生有效注意力,但在 prefill 阶段会占显存、占计算。如果 padding 策略不当,本来可以容纳 32 条请求的显存,可能只能容纳 20 条,推理服务的并发能力也会跟着缩水。

3.2 动态分桶和 Dynamic Padding 的落地步骤

解决 padding 浪费的最主流方案是分桶动态 padding,思路和你日常生活中叠衣服分类是一个道理:不把所有衣服统一叠成最大件的形状,而是按大小归到几个桶里,桶内按桶最大长度补。

实操时我会这样做:

  1. 先把请求按序列长度排序,估算长度分布。根据分布把请求分进若干个长度桶,比如 128、256、512、1024 四档。
  2. 每个桶内只按当前桶最大长度做 padding,不同桶之间各自独立成批。
  3. 设置分桶数量时要克制,桶太多会导致每个桶都凑不满 batch,反而降低吞吐。4~8 个桶是大多数场景下的甜点区。
  4. 优先级上可以先处理短桶,因为它们计算量小、能快速释放显存和算力,给后续长请求腾出空间。

如果你用的是成熟推理框架,常见做法是设置允许的最大输入长度和桶数量,让框架自动帮你分组;如果自己实现,核心逻辑大约是一个按长度分类的 dictionary 加上每批的触发条件。注意一点:分桶逻辑不要和动态聚合窗口打架。到达延迟超时的请求应该优先从最短桶里开始发,避免长桶把短请求拖住。

还有一个容易被忽视的细节:不要把 padding 出来的无效 token 也送入注意力掩码。实现时要同步把 attention mask 构建好,短序列在 padding 区域的注意力必须置为无效值。否则你虽然做了批量,模型却把无意义的 padding token 当成真实内容去学,结果输出会莫名其妙地偏。

3.3 页式 KV Cache 管理:批量执行中的显存调度艺术

序列变长带来的另一个连锁反应是 KV Cache 的显存分配。早期很多推理实现是给每条请求预留一整个最大长度对应的显存块,请求短时大量显存被闲置,请求长时可能又刚好超出预留上限导致失败。这本质上和变长请求 padding 是同一个资源错配问题。

现在主流推理服务普遍在借鉴操作系统虚拟内存的思路:把逻辑上连续的 KV Cache 拆分成固定大小的页,按页分配、按需分配。请求需要更多显存时,调度器从空闲页表里再给它挂一页;请求结束时,它占的页全部释放回池子。这样每个请求不再预留一整条连续区域,碎片化大大减少,同一时刻能容纳进 batch 的请求数量显著增加。

页式管理按需分配后,batch 执行优化的空间也被彻底打开了:以前你说“这个 batch 最多只能放 16 条”,是因为显存预留不够;现在约束变成了“这批请求实际需要多少显存”,只要累计不超限,就可以继续往里塞。我自己实测的感受是,上线页式管理之后,显存利用率至少提升 20%~30%,同样的卡能支撑几乎两倍的并发请求量。

如果你用的框架已经实现了这套机制,需要补的功课是对“最大可分配页数”做合理的显存预留;如果自己写推理调度,建议优先做按需分配,别走一次性预留的老路。

4. 连续批处理:把批量执行从“凑满发车”升级为“随到随走”

前面讲的固定批量和动态聚合,本质上都还是“整批进、整批出”的模式。请求攒成一个 batch,模型一口气算完,整批结束,再加载下一批。这个模式在分类模型里问题不大——单次前向时间很短,批量切分带来的间隙很小。但到了自回归生成场景(比如大模型逐 token 生成回复),整批处理的短板会非常明显。

4.1 整批发车模式的尾部等待病

自回归生成有一个特点:同一个 batch 里的请求,计算速度、剩余生成长度差异巨大。有的请求只需生成 20 个 token,很快就结束了;有的请求要生成 500 个 token,还在慢吞吞地一句一句往外吐。如果用整批发车模式,这两个请求被捆在同一批次里,短请求必须等长请求全部生成完才能一起释放。结果就是整批的平均延迟被长请求拖垮,GPU 在同一时刻只能服务一部分请求,其他请求全都在“陪跑”。

这个问题的本质是“批内资源不可折叠”。一个请求结束后,它占的槽位不会立刻让给新请求,必须等整批结束。低峰期请求稀疏时,batch 一直攒不齐,整批模式的等待时间甚至可能超过推理时间本身,在线体验会变得非常糟糕。

4.2 请求级调度:连续批处理打破批的边界

连续批处理(也叫 Continuous Batching / 请求级调度)的思路是彻底改变“批”的定义:模型不再是按整批去等,而是把每个请求作为一个独立计算流,GPU 上同时跑着一组请求。每当有请求完成生成,调度器立刻从等待队列里拉一个新请求进来补位,而不是等整批全部结束。

这有点像餐馆的运营方式。整批模式是一桌菜必须做齐了才上桌,满一桌才接待下一桌;连续批处理是每桌客人吃完就走,空出来的座位马上安排新客人入座。吞吐不会因为某个“长桌客人”赖着不走而清零,短请求也能随时完成随时离开。

在实际推理引擎里,连续批处理的关键在于调度器要同时管理每个请求的生命周期:请求刚进来时处于 prefill 阶段(需要处理完整的输入上下文),生成过程中处于 decode 阶段(每步只生成一个 token)。两种阶段的计算特征差异很大,prefill 阶段计算密集、显存开销大,decode 阶段单步计算轻但耗时更长。调度器要尽量把 prefill 请求安排到 GPU 计算压力较小的窗口执行,避免一次塞太多 prefill 导致 decode 阶段的请求被饿死。

4.3 显存配额与首 token 延迟的两难平衡

连续批处理虽然打开了吞吐上限,但它对显存管理的要求也更精细。每个请求的 KV Cache 是动态增长的,一个长请求可能从 10 页吃到 100 页。如果你让所有请求都无限制增长,显存会瞬间爆掉,整批全部出错。

所以实际部署中至少要设置三件事:单请求最大可分配显存页数、单请求最大生成 token 数(max_tokens)、以及整体显存水位阈值。当显存占用接近阈值时,要主动拒绝新请求或者触发更激进的重算策略,绝不能放任 OOM。这里我再补一句个人经验:很多人都只盯着 batch size 调,但显存配额往往才是真正卡脖子的地方。batch 你设成 32,如果 KV Cache 配额给得太宽,实际跑十几条请求显存就满了,调度器根本不敢往里塞新请求,批量执行的收益自然无从谈起。

首 token 延迟和显存配额也是一对天然矛盾。批内现有请求越多,新请求排队等待进入计算窗口的时间越长,首 token 就越慢。你要在高吞吐和体验之间找平衡,比较实用的做法是给不同来源的请求设不同优先级:交互类请求走小 batch、短队列的快速通道;后台生成类请求走大 batch、长队列的吞吐通道。这样两种需求互不拖累,比统一一个调度策略要灵活得多。

5. 压测指标反向校准:从吞吐、尾延迟到 batch 配置的闭环

批量执行优化做到最后,一定要回到压测数据上来验证,否则很容易陷入“参数调整了,但不知道是否真的变好”的糊涂状态。我见过不少团队把 batch 调到 64,吞吐看起来上去了,结果 p99 延迟完全失控,线上告警一片。批量优化不能只看平均吞吐,该看的指标必须看齐。

5.1 关键指标与采集方式

我会建议至少同时监控这五类指标:

  • 吞吐:每秒处理请求数(req/s)或每秒生成 token 数(token/s)。
  • 算力利用率:GPU 利用率、显存利用率。
  • 平均延迟与 p50:反映整体体验。
  • 尾延迟 p95/p99:反映最差情况,在线服务这个比平均更重要。
  • 排队长度与等待时间:反映动态聚合是否生效。

压测工具也很容易踩坑。用客户端模拟请求时,如果客户端本身是单线程串行发请求,服务端永远攒不齐 batch,压测结果自然很差,这不是服务端的问题,而是压测方式不对。正确的压测应该用多并发客户端同时灌入请求,让服务端有足够多的待处理请求可供聚合。

另外,正式压测前必须先做预热。推理引擎第一次启动时,权重加载、显存分配、内核编译都没完成,跑前几十个请求的速度完全没有参考意义。我通常的做法是先灌一批请求把服务“热”起来,再开始记录指标,否则第一轮压测数据会误导你对 batch 收益的判断。

5.2 三个容易误判的实测场景

下面这三个场景,是我在调批量执行优化时实际踩过、也看别人反复踩的坑。

第一个坑是“把并发数当成 batch size”。压测时客户端开了 32 个并发连接,服务端模型可能实际每次只处理 4 条请求,剩下的全在排队。你的并发数是客户端视角,batch 是服务端视角,两者没有任何换算关系,必须通过监控里的“单次推理实际批大小”来确认,而不是看客户端并发拍脑袋。

第二个坑是“动态窗口开得太小”。我遇到过有人把 max_batch_delay 设成 1ms,结果请求根本来不及攒,实际 batch 永远是 1,批量优化的收益全被参数设计吃掉了。这个参数需要和请求到达速率联动着看,单纯设得小并不能降低延迟,反而会让批处理形同虚设。

第三个坑是“只调 batch 不调 padding”。这种情况在 NLP 场景特别典型:batch 从 8 调到 32,吞吐涨得很欢,但显存涨得更快,因为 padding 比例没优化,大部分显存都在为无效 token 买单。遇到吞吐和显存同时异常增长时,先别急着怀疑 batch,去检查一下长度分布。

调优的正确顺序,我个人认为是这样的:先明确延迟预算,再根据预算定 batch 区间;然后处理变长输入的 padding 问题;接着跑压测看实际批大小和显存水位;最后再决定要不要引入连续批处理和请求级调度。每一步都用数据说话,批量执行优化才不是靠感觉碰运气。

我自己的经验是,批量执行优化没有一劳永逸的参数,唯一不变的是硬件、请求分布和延迟预算这三个变量。每次看到别人问“batch 设多少合适”,我都会反问一句“你的延迟预算和请求长度分布是什么”。把这个问题想清楚,再回去做优化,基本不会跑偏。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 2:56:51

网络安全攻击技术学什么?从入门到进阶书单与学习方法

1. 网络安全攻击技术学习书的正确打开方式这两年网络安全行业热度一直没降,不少朋友想入门“攻击技术”这个方向,但一上来就被一堆英文原版书、晦涩的漏洞分析手册给劝退了。还有人买了好几本所谓的“黑客入门宝典”,翻开一看全是工具命令的堆…

作者头像 李华
网站建设 2026/10/19 2:56:49

MATLAB解析Fluent数据:血流动力学参数批量计算指南

简介:面向生物流体力学与血管模拟研究者的MATLAB计算工具,专门处理Fluent导出的ASCII表面数据,用于提取时间平均壁面剪应力(TAWSS)、振动剪切指数(OSI)、相对停留时间(RRT&#xff0…

作者头像 李华
网站建设 2026/10/19 2:56:31

Xshell/Xftp连接Linux服务器的底层原理与排错指南

1. 为什么新手总在Xshell/Xftp上卡住第一关——不是工具难,是环境认知断层刚接触Linux服务器运维的新人,常会陷入一个奇怪的循环:看教程时觉得“就装个软件点几下”,真动手时却卡在第一步——连不上、输错密码、文件传不进去、界面…

作者头像 李华
网站建设 2026/10/19 2:56:09

FastGPT部署实战:从零搭建私有化知识库问答平台

有段时间没碰FastGPT了,最近又被人问到“想在自己服务器上搭一套知识库问答平台,怎么选型?”我几乎不加犹豫把FastGPT丢过去。它解决的是很常见的问题:公司内部文档散乱、客服回复重复、产品手册没人翻,最要命的是这些…

作者头像 李华
网站建设 2026/10/19 2:56:08

JDBC进阶:从连接池到事务边界,写出生产级数据库代码

JDBC 的入门教程遍地都是,核心套路就那几步:Class.forName加载驱动、DriverManager.getConnection拿连接、Statement执行 SQL、ResultSet遍历结果,最后关掉所有资源。三分钟能跑通,一周后大概率扔进工具类里吃灰。但这个系列既然叫…

作者头像 李华
网站建设 2026/10/19 2:54:54

128×160彩屏LVGL移植全攻略:从ST7735初始化到流畅刷新

简介:一套适用于 STM32 开发者的 LVGL 移植代码工程,目标是把中景园 1.8 寸 128160 彩屏接入 LVGL,解决显示驱动适配、分辨率配置与界面初始化等问题。压缩包共 412 个文件,大小约 3.86MB,以 204 个 C 源文件与 183 个…

作者头像 李华