loading

Loading

探索与发现

首页 AI 工程化

批内长度分桶:把长请求单独排队之后

分类:AI 工程化
字数: (2496)
阅读: (7)
0
摘要:批处理一直跑得快,但长度差异会让填充和收尾彼此拖。这篇拆解长度分桶的三层做法:Dynamic SplitFuse 的批内切分、MiMo 三级输入端分桶以及按桶路由的实例调度,并核算切点、输出预估与桶间头阻塞这三处代价。

批处理调度里有一个被低估的变量:同一个 batch 内序列长度的离散程度。连续批处理把请求攒到一批一起走 forward,看起来 GPU 一直在忙,但如果一批里混着 128 token 的短问答和 8K token 的长文档摘要,等待的那一方就会被拖住。长度分桶解决的就是这个问题:把长度接近的请求放在同一批,别让长请求和短请求互相拖。

问题在哪儿:批内填充与完成时刻不对齐

传统静态批处理需要把不同长度的序列 pad 到批内最大长度,padding 部分的计算纯属浪费。阿里云开发者社区的推理优化文章明确指出,自适应分组的目标是把相似长度的序列分到同一批次,减少填充带来的额外计算。这是最直观的收益,但线上真正痛的地方不止 padding。

连续批处理下,每个 iteration 都会重新组批,新请求可以随时插入。问题是一个 batch 里如果存在不同 prefill 长度混合的情况,CUDA kernel 的 tile 利用率会随着最长那一条走:短请求本可以在 200 微秒内结束,却要和长请求一起等到更重的计算完成,P99 立刻被拉高。《聊聊大模型推理服务中的优化问题》里给了一个判断:如果 token 序列太长,最好拆到不同的 batch 中。换句话说,一批可以大,但不要同时太长,太长的部分另行处理。

主流做法在做什么

学界和工程界的方向大致分三层。

  • 批内切分:Dynamic SplitFuse 的思路就是限制一个批的最大 token 长度,超出的拆分出去,剩余处理单元长度一致,调度更简单。代价是拆分后的请求要在一个或多个 forward 中分次完成,long prefill 的 TTFT 变差,需要靠批的吞吐把平均延迟拉回来。
  • 请求分桶:小米 MiMo-V2.5 系列推理全链路优化记录里给出了三级长度分桶策略,划分为 0–64K、64K–256K、256K–1M,把负载特征相近的请求聚合到同一桶做计算,线上 prefill 平均吞吐有明显提升。文档同时提到,他们正在探索更细粒度、更灵活的分桶机制来适应负载动态变化,也就是说三级切法已经是取得收益的版本,但粒度还不足。
  • 实例层按桶路由:CN121724158A 这个专利的思路更彻底,请求分桶控制器按输入长度和预测输出长度自适应分桶,每个桶生成独立的推理配置,再由全局调度器的请求路由器按推理组维护的输入长度区间做路由选择,多个候选就估计总完成时间取最优。等于把分桶从批级别上升到部署级别,从工程复杂度来看代价也最高。

分桶之后收益从哪来,代价在哪儿

先算收益。分桶减少 fill 浪费是一个方面,更关键的是让 overlap 成立。MiMo 那篇文档提到,SWA 层只需要 prefetch 极少量 KVCache,layerwise 粒度的调度接近零成本;这意味着一批里请求的结构越接近,Host 到 Device 的拉取越容易和计算重叠,桶切得准,缓存命中长度也更容易复用。KVCache 命中率受益于分桶这一点容易被忽视,但在多轮对话或长 system prompt 共享场景下,同桶意味着同前缀长度分布,命中率稳定比单纯调容量有效。

再说代价,三块。

第一,排队头阻塞从批内转到了批间。按输入长度分桶之后,短请求聚集在短桶,一旦某条长请求独占一个桶、而桶内并发不够,GPU 利用率直接掉下来。MIMO 只用三级分桶,一个桶跨度可能是百K token 级别,命中率是按粗粒度换来的。切得过细,每个桶内请求数不足,batch 又变得很小。分桶是在 batch 大与 batch 均匀之间找一个折点。

第二,输出长度不可控。分桶应当按输入长度做,但输出的长度分布往往呈长尾。输入都以 8K 开头的两条请求,输出可能是 50 token 也可能是 4000 token。网关无法预先准确预估输出长度,分桶只能落得一个期望意义上的接近,别把这件事当成解决了。

第三,离散桶边界会造成迁移问题。当桶边界设在 64K 的整数倍、而线上聊天请求量又大量堆在接近边界的位置时,要么被迫溢出到更高一档,要么吞不到足够请求,调度器还要维护跨桶回填逻辑,实现复杂度上升不亚于重写一次调度器。

落地建议

我们的实践是输入长度先量再定,别照抄别人的阈值。动态分桶门槛应该在该部署下统计出的长度直方图上取谷底值:两个峰值之间的密度低谷,切在哪都不伤主流量。MIMO 采用 0–64K / 64K–256K / 256K–1M 这种粗切口,是这个场景下流量的经验密度产物,搬到别的工作负载多半不合适。

对于输出长度,稳定场景下可以直接做输出预估并发给分桶器,这是 CN121724158A 里按输入长度和估计输出长度共同分桶的部分思路;当预估值不可靠时,只作参考不强行改桶。实测更划算的做法是把分桶和优先级调度结合,短请求放在短桶优先级队列的前端,只在上限上给桶内设定接近的出发期,短边照样会等但短请求的等待被约束住。

上线后要盯紧三个指标,而不是只盯吞吐——单桶内时延 p99、分桶边界两侧请求的溢出率,以及整个 warmup buffer 中桶之间的排队时长差。只要批内方差没降而整体队列长度变长,说明桶设置不是在帮助请求,而是在稀释批次,应该收缩桶的粒度重新标定一次。

一个需要提前说清的边界

Dynamic SplitFuse 起家的位置实际上是 prefill/decode 的混排。decode 阶段 batch 大小和序列长度累加关系密切,而 prefill 阶段更看 GPU 计算边界。同一个桶里如果混了 chat、代码、长文档摘要,其计算密度差异可能远大于长度差异本身。分桶能解决填充与收尾不对齐,却解决不了异构请求之间计算形态的不匹配,别指望它一步到位。

说白了,长度分桶是一笔更接近工程配置优化而不是算法创新的账。它可以省 cap 费、可以压低 p99,但应该被放在连续批处理已经稳定、前缀缓存也未出现明显退化之后。桶不合理时,整个推理还停留在调度器而不是模型容量的层面,往上再做任何优化都会更难看。

转载请注明出处: 皮哈皮阿

本文的链接地址: https://blog.phpat.com/ai-engineering/674.html

手机扫码阅读

微信或手机浏览器扫一扫,随时随地随心阅读与分享

您可能对以下文章感兴趣
1 / 1
预览图