☰
Orca迭代级调度:终结静态批处理,LLM推理吞吐提升36.9倍
2026/10/20 21:53:08 网站建设 项目流程

做LLM推理优化的人应该都见过那个经典场景——GPU的利用率统计看起来不错,但实际吞吐就是上不去。我最早做推理服务的时候,用的还是最传统的静态批处理:请求攒够一批,一起进模型,整批跑完再放下一批。这个方案在传统深度学习推理里没什么大毛病,但换到LLM这种自回归生成场景里,问题被无限放大。直到我认真拆解了Orca这篇工作,才彻底搞明白瓶颈出在哪——不是显存不够,不是算力不够,而是调度粒度错了一个量级。

Orca是LLM推理系统演进过程里一篇绕不开的工作。它最核心的贡献是在推理服务里把调度单位从“请求级”降到了“迭代级”,也就是GPU每往前跑一次前向传播,都是新的调度时机。这个视角的转变直接终结了静态批处理这个历史产物,在相同延迟约束下,吞吐能比静态批处理高出36.9倍。这篇内容我会从问题出发,把静态批处理为什么会成为性能天花板、Orca的迭代级调度和选择性批处理到底怎么工作、36.9倍这个数字是怎么出来的,以及在工程落地时会踩到什么坑,一个个讲透。适合正在做推理服务、想优化GPU利用率,或者对LLM推理系统底层机制感兴趣的同学。

1. 静态批处理为什么成了性能天花板

1.1 先理解LLM推理的两阶段模型

要理解Orca的价值,得先把LLM推理的过程拆开看。

LLM生成一个完整的回答,不是一次性吐出来的,而是像人打字一样,一个token一个token地往外蹦。这个过程中,模型实际上经历了两种完全不同性质的计算阶段:

第一个阶段叫PreFill(预填充),模型要把用户输入的整段prompt一次性处理完,生成完整的Key-Value缓存(也就是常说的KV Cache)。这个阶段的特点是计算量巨大、并行度高,是典型的计算密集型(compute-bound)操作。你可以把它想象成老师拿到一篇文章,先通读一遍画出重点,这个过程需要全力以赴,但只需要做一次。

第二个阶段叫Decode(解码),模型逐token生成输出。每生成一个token,都要把之前所有的KV Cache重新读一遍,然后只算当前这一个token的注意力。这个阶段的特点是计算量小、但频繁访问大容量的KV Cache,是典型的访存密集型(memory-bound)操作。想象成老师根据画好的重点,一个字一个字往外写答案,每个字都要回顾一遍之前的内容,但本身的工作量其实很低。

这两个阶段对GPU资源的消耗模式完全不同。PreFill阶段GPU算力跑得满满当当,而Decode阶段大部分时间都耗在搬运数据上,SM(流处理器)经常大部分处于空闲状态。这意味着,如果你希望GPU满载,最好让一批请求停留在Decode阶段,这样在访存间隙还能穿插一些计算任务;同时也要有请求不断进入PreFill阶段,喂给GPU足够的稠密计算。两者需要动态平衡。

1.2 静态批处理的资源浪费

静态批处理是怎么做的?很简单:把请求收集成一个固定大小的batch,整批启动后,直到batch里的所有请求都完成生成,再整批返回结果,然后释放资源,收集下一批请求。

这个方法在传统深度学习推理里是没问题的。比如图像分类,一个batch里有10张图,每张图的前向计算时间几乎相同,大家都在差不多的时间内一起结束,GPU资源不浪费。

但LLM场景完全不一样。LLM请求的输入长度千差万别,输出长度更是完全无法预测——有人问“1+1等于几”,有人要让模型写一篇长文。静态批处理把这一堆长短不一的请求捆在一起,就必然出现三个问题:

第一,短的请求被长的请求拖死。batch里如果有9个短请求和1个长请求,那么9个短请求完成后,只能干等着长请求把剩下的几百个token生成完。这段时间里,GPU的算力被占着,但大量计算单元是空闲的——因为Decode阶段本来就是访存密集,单条长请求根本喂不饱GPU。

第二,空闲资源无法被新请求利用。即使有新的请求在排队,只要当前batch没跑完,新请求就没法插进来。你眼瞅着GPU有富余能力,但它被一个batch“锁定”了,眼睁睁看着大量算力被白白浪费掉。

第三,GPU利用率呈“锯齿状”崩坏。Batch越大,资源被长请求锁住的时间越长,吞吐在batch开始时冲高,然后一路下滑,这显然不是我们想要的。

我当年用静态批处理做在线服务时,最痛苦的一点就是延迟SLA(服务等级协议)和吞吐没法两全。想保吞吐,就得把batch开大,一旦batch开大,整个batch的完成时间就被最长的那个请求决定,P99延迟直接爆炸。最后只能在最大batch size上小心翼翼取一个很小的值,GPU利用率长期只有个位数。这种体验相信很多做过推理服务的人都熟悉。

2. Orca的核心思想:迭代级调度

2.1 从“请求级”到“迭代级”的视角转换

静态批处理的核心病灶,是把“一批请求”当成了不可分割的整体。Orca的突破性在于:它把这个整体打散了,让调度器在每个iteration(即模型的一次前向传播)上都重新做出决策。

什么叫“迭代级调度”?先补一个基础概念:在Transformer模型里,每生成一个token,模型都要执行一次完整的前向传播,这次前向传播就叫做一个iteration。在静态批处理里,调度器只在“请求进来时”和“整批请求跑完时”做决策,中间的所有iterations全部无人管理。Orca把调度的控制粒度改成了单个iteration,相当于把一个大块任务切成了无数个小切片,每个切片都允许调度器重新排兵布阵。

你可以把静态批处理想象成一列火车,整列车厢绑定在一起,要么一起出发,要么一起到站。而迭代级调度就像一个大巴车队,每个乘客(请求)可以随时上车、随时下车,司机在每个路口都重新规划路线,不再是“一列车走到底”。

这个视角转换的意义非常本质:在静态批处理中,请求是绑定在一起的,GPU在这种模式下的行为是被动的——它是被一个固定batch的请求推着走的。而在迭代级调度中,调度器变成了主动的一方,它在每个iteration都审视全局,“这一轮该让谁跑、不让谁跑”,完全由调度器说了算。

2.2 选择性批处理的核心机制

光有迭代级调度的思想还不够,Orca在工程上还需要一套具体的策略来落实“让谁跑”的决策,这就是它提出的选择性批处理。

选择性批处理(Selective Batching)的机制可以拆成三个关键动作:

第一个动作:已完成请求的即时退出。在静态批处理里,一个请求即使在第10个iteration就生成了结束符,它也得等到整个batch跑完才能释放。而在Orca中,某个请求一旦生成完毕,它在下一个iteration直接不再参与,资源和显存立即释放。这意味着长请求不再拖累短请求,短请求也无需等待长请求。

第二个动作:新请求的即时插入。在这个模型里,新请求不需要等“一个批次跑完”,而是在任意一个iteration开始前都可以插入。新请求的PreFill计算,和已有请求的Decode计算,在同一个iteration内完成——一次前向传播里,既处理新请求的prompt计算,也处理旧请求的下一个token生成。这个能力叫“混杂执行”(在论文里叫divergent execution),是Orca的一个关键创新。因为PreFill部分会动用大量闲置算力,而Decode部分的访存瓶颈正好给PreFill留出了计算空间,两者在同一个iteration中反而能互相“充饥”。

第三个动作:持续的满功耗执行。由于请求可以随时进出,理论上GPU只要还有显存空间,调度器就可以保证每个iteration都有活干。这就终结了“batch运行后期,一堆请求已完成,但GPU还空转等最后一个长请求”的尴尬局面。

这三个动作组合起来,本质上是把GPU从一个“按批隔离、各自为战”的模式,变成了一个“持续浇筑、动态共享”的模式。GPU再也不需要在等待中度过,吞吐自然就上去了。

这里有个值得注意的细节:选择性批处理的“选择性”体现在,调度器在每个iteration选择一批请求参与计算,这批请求不是固定不变的,而是动态计算出来的。选谁、不选谁,取决于当前排队情况、各个请求的生成进度、以及显存预算。这正是工程实现中最关键也最复杂的部分。

3. 36.9倍吞吐提升从哪来

3.1 延迟约束下的吞吐对比

36.9倍这个数字听起来夸张,但它的产生逻辑其实非常合理。先看它是怎么测算的。

在推理系统评测中,不能只看裸吞吐(TPS),还必须在满足延迟SLO的前提下比。否则静态批处理把batch开到极限,吞吐也很高,但每个请求延迟几百秒,没人能用。实验设置时,给出一套延迟约束标准(论文里用的是P99尾延迟,比如要求P99延迟低于某个阈值),然后分别测定静态批处理和Orca在满足该延迟前提下的最大吞吐。

在这个设定下,两者差距被急剧放大。为什么?因为静态批处理能跑的最大batch,被延迟约束卡得死死的。

讲个更直白的模型。假设一批请求里有10个请求,平均每个请求要生成50个token,但最长的一个要生成200个token。静态批处理把这个batch作为一个整体,它完成这批请求的总时间等于最长请求的时间,也就是200个token的时间。也就是说,因为有9个平均长度的请求,这10个请求的平均完成时间被拉长了。如果这批请求里有一些短的,比如生成20个token就结束了,那么这9个短请求实际浪费的时间就是(200-50)×9的时间预算。在延迟SLO严格时,你只能选非常小的batch(往往只有1、2个请求),整批算下来吞吐极低。

Orca不同,它让短请求完成后立即退出,长请求继续跑。GPU利用率始终是饱满的。同样满足延迟SLO的前提下,Orca可跑的有效batch size,远大于静态批处理能容忍的batch size。36.9倍恰恰是在这种“延迟约束下的容量差”中算出来的。

3.2 性能提升的数学直觉

36.9倍的提升,背后其实是硬件利用率差距的累积效应。我试着用一个简化模型来说明这个过程。

假设GPU在单位时间里能执行C单位的计算量,请求的平均计算量为A,输出token长度为L。在静态批处理中,batch内请求同步推进,有效计算时间占总占用时间的比例约为(平均完成时间 / 最长完成时间)。如果请求长度分布方差很大,这个比例会非常低。例如:某batch的平均完成时间为50个token,但最长完成时间为200个token,那么GPU有效计算占比最多只有25%,其余时间虽然设备被占用,但实际的“有效产出”很低。

Orca模式下,每个iteration都尽量塞满可运行的请求,GPU的有效计算占比基本维持在接近100%。延迟SLO下的有效吞吐差距,再考虑到显存空间和计算分布,最终的复合效应就产生了10倍量级甚至更大的差距。36.9倍这个数字看起来夸张,但它是“等待浪费+同步浪费+利用率差距”三项叠加的结果,在极端分布下完全合理。

这个数字也给我们一个重要启示:在LLM推理里,吞吞吐吐的瓶颈往往不在单请求的compute能力,而在系统的调度智慧。GPU硬件本身并没有变快,是Orca用算法层面的创新把本来就该被用上的算力全部榨了出来。

4. 工程实现中的关键细节

4.1 调度器的核心功能

迭代级调度在论文里看起来优雅,但工程实现完全是另一回事。调度器在每个iteration要回答三个问题:谁该被添加(Who)、谁该被继续(Whom)、谁该被淘汰(Who out)。这三个决策做不好,性能提升就会被各种副作用吞掉。

先看“谁该被添加”。这一步的核心是:新请求进来后,是先排队等一批再进,还是立即插入下一个iteration?我实际测试的结论是,立即插入在长尾请求多的情况下收益最大。但这里有个限制——插入本身也有代价,它会把一个iteration的执行时间拉长,因为你既要跑PreFill的计算,还要跑already running的Decode。调度器必须估算出新请求带来的额外开销,并将其与当前GPU空闲算力进行匹配。如果估算不准确,GPU可能因为频繁插入被“撑住”,反而增加延迟抖动。

我的做法是给不同长度段的请求设置一个“即时插入触发阈值”——短请求可以随时插入,长请求则要求在解码中途达到一定进度后才能插入,防止一个prefill过重的长请求把当前iteration拖垮。这个策略在很多场景下效果很好,相当于做了个轻量级的准入控制。

再看“谁该被继续”。这里的关键是优先级顺序。Orca默认的做法比较朴素——谁先到就先继续跑,但实际生产环境中,你可以根据下游业务调整优先级,比如VIP用户请求、交互式对话请求优先。不过要小心,如果持续让高优先级的新请求抢占资源,低优先级的普通请求可能会被饿死。

最后是“谁该被淘汰”——这个看似简单,其实是三个决策中最难搞的。它不只是“生成完就退出”这么简单,还要考虑请求使用过的KV Cache的释放时机、显存碎片整理、CPU端队列同步等等。这些操作如果处理得不够快,会在迭代间隙形成新的瓶颈。

4.2 内存管理与KV Cache分配

静态批处理的另一个隐含便利,是每个请求的KV Cache长度基本固定(对齐到最大长度分配),而且整个batch的KV Cache在内存里是连续分布、内存生命周期完全一致。Orca打破了这一切。

请求随心进入/退出之后,KV Cache的内存生命周期变得碎片化:旧请求释放了大量内存,新请求产生的KV Cache需要在内存中找位置存放。LLM的KV Cache是按token不断增长的——每生成一个token,某请求的KV Cache就要在后面追加一部分。如果内存管理不到位,很快就出现外部碎片和内部碎片,显存白白浪费掉。

Orca给出的路线是用分块机制。具体做法是:将显存划分为若干固定大小的块(chunk),每个请求的KV Cache按块分配。请求需要增长时,就再申请一个块;请求结束时,整个块的链表统一释放。这个设计避免了连续内存分配导致的高成本碎片整理。它有点像操作系统的分页存储,把物理内存的不连续性问题藏在了逻辑地址之下。

我落地这个方案时的经验是,块大小设置需要根据实测请求分布去调。块设得太大,内部碎片浪费严重;块设得太小,KV Cache增长时需要频繁申请新块,增加内存管理开销。个人认为,正常情况下把块大小设为一个请求平均KV Cache量的1/4到1/3,是个不错的起点。

4.3 与后续工作的关系

Orca在推理系统演进中处于承上启下的位置。在它之前,静态批处理是绝对主流;在它之后,几乎所有高性能推理框架都沿着迭代级调度的思路往前走。后来的PagedAttention(也是分块逻辑,但是做在注意力计算层面)可以看作是Orca内存管理思想的延续与深化。

从现在的视角回看,Orca最重要的遗产其实不是“36.9倍”这个数字,而是它确立了一套新的调度范式——不再把请求当批次,而是当作活生生的、可以动态进出的个体。后续的很多推理加速手段,比如动态批处理、抢占式调度、KV Cache压缩、prefix caching,全都是建立在这个“请求自由进出”的前提之上。

它也深刻影响了推理硬件利用率的思考方式。以前大家买GPU主要看算力FLOPS,但Orca用实验结果告诉我们:真正决定吞吐的,是调度系统能否将FLOPS转化为有效产出。算法层面的调度优化,可能比多买一张卡带来的收益更大。

5. 实践中的常见问题与避坑指南

5.1 延迟抖动与排队控制

迭代级调度给了系统极大的弹性和吞吐提升,但弹性本身就是双刃剑。最常见的坑是:当系统处于高负载状态时,调度器为了最大化吞吐,可能持续把新请求插入到迭代中,导致真正正在执行Decode的老请求被延迟。这会让延迟产生严重抖动——短请求可能非常快,但长请求可能被排到几百毫秒甚至几秒之后。

我实际排查过几次这类问题。最初以为是自己插入逻辑的bug,后来发现本质上是“新请求准入”和“存量请求完成”两条目标在互相打架。调优的方向很清晰:给新请求的插入设置严格的准许条件,比如只有当队列中已有请求数量低于某个阈值、或者当前iteration的PreFill计算量未超过某个比例时,才允许插入新请求。这相当于给老请求的完成预留了保护带宽。

5.2 公平性保障

Orca的选择性批处理很容易导致不公平:高吞吐的短请求持续插入,低优先级的慢请求就可能长时间得不到调度。比如用户发了一个需要生成1000个token的长文档请求,如果系统一直有短查询进来,长请求可能会被不断“插队”,最终饿死或者超时。

解决这个问题不能只靠调度器的“轮次保证”,还需要在队列层面做文章。我的经验是,把请求按优先级分队列,同时给每个队列设置最低服务比例的预算。比如,低优先级队列至少要在每N个iterations里被调度到1次。这相当于给长任务提供了“最低服务保障”,从机制上避免饥饿现象。

还有一个容易忽略的细节:每次iteration中,不同请求的decode阶段的计算强度其实不同。有些请求刚进入decode,Token Generation的计算还没跑几次,计算量低;有些请求已经跑了几百个iteration,本身的计算量也大。调度时如果只看请求重要性,就要额外关注真正吃时间的请求,而不是机械地按队列取舍。

5.3 调参经验

最后分享一组我在实践中的调参思路,未必适合所有场景,但可以先作为起点:

  • max batch size:建议在不牺牲延迟的前提下尽可能调大,因为迭代级调度本身就是靠“桶越大,动态调度收益越大”;
  • chunk size:根据平均KV Cache量设置,建议先跑一天真实流量,统计请求长度分布后再决定,别拍脑袋;
  • 新请求准入门槛:建议以“当前iteration中PreFill的计算量占比”为指标,超过30%-40%时暂停插入新请求,防止decode被冲垮;
  • 预热策略:系统刚启动时,先以较低的新请求允许率运行几百个iteration,等KV Cache分配器的工作状态稳定后,再逐步放开;
  • 精确测量指标:记录每个iteration内“PreFill计算时间 / Decode计算时间”的比例,比值长期波动大于阈值时候,优先检查准入控制的阈值设置。

如果你准备把这套思路应用到自己的推理服务里,我强烈建议先做一组对比实验:在完全相同的负载和延迟约束下,把静态批处理和基于迭代级调度的方案各跑一段时间,记录P99延迟、吞吐和GPU利用率三项指标。我做了这个实验之后,才真正从数据里看清楚差距来自哪里——不是单项指标的好坏,而是整套系统“利用等待间隙”的能力提升了。

6. 个人落地的一些体会

这套系统真正的难点,不在于理解迭代级调度是怎么回事,而在于把它落成一个稳定、可观测、可排障的工程系统。我在实际接入业务流时,第一版就栽了跟头——只顾着把调度器写好,却忽略了显存分配的不确定性,结果跑了一个小时后,内存碎片导致大量KV Cache分配失败,服务的P99延迟直接冲破天际。

后来我总结出一套组合拳:先把KV Cache分配器的健康度指标接入监控,再在调度器层面对“新请求准入”和“内存分配失败”这两个事件做联动,一旦发现分配失败率升高,立即降低新请求的准入率。这样系统才能用一个我们熟悉的句式描述它的状态:调度器在单位时间内有足够的灵活性,同时又有足够的约束。

对于大多数团队来说,我建议不要从零写一整套Orca式调度器。现在市面上已经有不少开源推理框架实现了迭代级调度的核心思想,你可以直接站在巨人肩膀上改造。重点要做的,是根据自己的业务负载——请求长度分布、速率、SLO要求,把准入控制和内存分块这两个参数调好。这两个参数是灵魂,它们直接决定了你最终能榨出多少性能。

如果你正在被“GPU利用率上不去、延迟和吞吐无法兼顾”这个问题折磨,建议你把静态批处理彻底拿掉,换迭代级调度这套思路。它带来的改变,肉眼可见。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询