1. 从一次线上事故说起:为什么分体式推理的资源分配这么难
去年下半年,我参与维护的一套大模型推理服务经历了一次挺典型的翻车。白天压测一切正常,P99延迟稳定在2秒出头,结果晚上业务方搞了个活动,请求量翻了不到三倍,整个推理集群直接雪崩——预填充节点(Prefill)的GPU利用率冲到100%,解码节点(Decode)却闲得发慌,队列里的请求越堆越多,超时率从0.5%飙到30%以上。事后复盘,问题根本不在模型本身,也不在硬件不够,而是资源分配策略是静态的:预填充和解码的算力配比写死在配置里,流量结构一变,两边就严重失衡。
这件事让我开始认真研究分体式(Disaggregated)LLM推理架构下的资源调度问题。所谓分体式,就是把传统单体推理里的两个阶段——Prefill(预填充,处理完整输入prompt,计算密集)和Decode(解码,逐token生成,访存密集)——拆到不同的计算资源池上独立部署。这么拆的好处很明显:两个阶段的硬件特性、并行策略、批处理逻辑完全不同,混在一起跑会互相拖累。但拆开之后,新的问题就来了:两边的资源该按什么比例配?流量波动时怎么动态调整?怎么保证端到端的SLO(Service Level Objective,服务等级目标)?
SARA这篇工作(SLO-Aware Resource Allocation)给出的答案是用排队论来建模,把预填充和解码两个阶段分别抽象成排队系统,通过解析模型推导出满足SLO所需的最小资源配置,再结合运行时反馈做动态调整。我读完之后的感受是:这套思路不算特别复杂,但胜在把工程问题数学化了,而且落地路径清晰。下面我就按自己的理解,把这篇论文的核心思路、数学建模过程、实操落地要点,以及我在实际环境中踩过的坑,完整地拆一遍。
2. 分体式推理的架构本质与资源困境
2.1 预填充与解码:两种截然不同的计算模式
要理解SARA为什么这么设计,得先把分体式推理的底层逻辑讲清楚。传统单体推理里,一个请求进来,先做Prefill:把整个输入序列(比如2000个token)一次性送进模型,计算所有位置的注意力,生成KV Cache。这一步是计算密集型的,GPU的算力单元(Tensor Core)基本跑满,显存带宽反而没那么吃紧。然后是Decode:基于KV Cache,每次只生成一个token,循环往复直到遇到终止符。这一步是访存密集型的,每生成一个token都要把整个KV Cache读一遍,算力利用率很低,瓶颈在显存带宽。
把这两个阶段放在同一张卡上跑,会出现什么情况?Prefill的长请求会把Decode的短请求堵在后面,因为Prefill占用算力时间长,Decode虽然单步快但架不住要循环很多次。更麻烦的是,两者的最优批处理策略是冲突的:Prefill适合大batch一次性算完,Decode适合小batch高频迭代。混在一起,调度器左右为难。
分体式架构就是把它们物理隔离:一组GPU专门做Prefill,另一组专门做Decode,中间通过高速网络传输KV Cache。这样一来,每边都可以针对自己的特性做优化——Prefill节点可以用大batch、高并行度;Decode节点可以用连续批处理(Continuous Batching)、PagedAttention等技巧。但代价是:KV Cache的传输成了新的瓶颈,而且两边的资源配比变成了一个需要动态求解的优化问题。
2.2 静态配比的三个致命缺陷
我见过不少团队一开始都是拍脑袋定配比,比如Prefill和Decode按1:2配,或者按1:1配。这种静态策略在流量模式稳定时勉强能用,但一旦遇到以下三种情况就会出问题。
第一种是请求长度分布变化。如果突然来了一批超长prompt的请求(比如RAG场景下检索回来一堆文档),Prefill的计算量会暴增,而Decode的负载不变。这时候Prefill节点成为瓶颈,Decode节点闲置。反过来,如果是一批短prompt但要求生成很长内容的请求(比如写代码、写文章),Decode的负载会远大于Prefill。
第二种是到达率波动。LLM推理服务的请求到达往往不是泊松过程,而是有明显的突发性。活动开始的一瞬间,请求量可能翻好几倍,如果资源不能快速弹性伸缩,队列就会迅速积压。
第三种是SLO的多样性。不同业务对延迟的容忍度不一样:对话类应用可能要求TTFT(Time To First Token)在500ms以内,而离线批量生成任务可能允许几秒的延迟。静态配比无法同时满足多档SLO。
SARA的核心贡献,就是把这三种情况统一到一个排队论框架里,用数学工具求出在给定SLO约束下的最优资源配比。
2.3 排队论为什么适合这个问题
你可能会问:为什么非要用排队论?直接用强化学习或者启发式规则不行吗?我的理解是,排队论的优势在于可解释性和可求解性。强化学习虽然灵活,但训练成本高、收敛慢,而且在生产环境里很难保证稳定性——你不敢让一个RL agent直接控制几千张GPU的分配。启发式规则(比如队列长度超过阈值就扩容)则缺乏理论保证,参数调起来全靠经验。
排队论把系统抽象成“到达过程+服务过程+队列规则”,可以用M/M/c、M/G/1等模型求出平均等待时间、队列长度、超时概率等指标。对于分体式推理,Prefill和Decode可以分别建模成两个串联的排队系统,端到端延迟就是两段延迟之和加上KV Cache传输时间。有了这个解析模型,就可以反过来求:在给定到达率λ和SLO约束下,需要多少台Prefill节点和多少台Decode节点。
当然,实际系统比教科书里的排队模型复杂得多——服务时间不是指数分布,批处理会改变服务率,KV Cache传输有带宽限制。SARA的做法是做合理的近似,然后用运行时数据校准模型参数。这种“解析模型+在线校准”的思路,我觉得是这篇论文最值得借鉴的地方。
3. SARA的核心机制:排队论建模与SLO约束求解
3.1 系统抽象:两级串联排队网络
SARA把整个推理服务抽象成一个两级串联的排队网络。第一级是Prefill队列,请求到达后先在这里排队,等待被分配到某个Prefill实例上执行。执行完成后,请求进入第二级Decode队列,同时KV Cache被传输到Decode节点。Decode阶段会持续多个时间步(每个token一步),直到生成结束。
这里有个关键细节:Prefill的服务时间取决于输入序列长度,Decode的服务时间取决于输出序列长度。两者都是随机变量,不能简单用固定值。SARA假设输入长度和输出长度分别服从某种分布(论文里用的是经验分布),然后通过排队论公式计算各阶段的平均延迟。
端到端延迟可以写成:
E2E_Latency = W_prefill + S_prefill + T_transfer + W_decode + S_decode
其中W是排队等待时间,S是服务时间,T_transfer是KV Cache传输时间。SLO约束通常是对E2E_Latency的某个百分位数(比如P99)提出上限。
3.2 服务时间建模:从token长度到GPU时间
排队论模型要能用,前提是服务时间估计得准。SARA在这块做了比较细的建模。对于Prefill阶段,服务时间大致与输入序列长度的平方成正比(因为注意力机制是O(n²)),但实际中还受batch size、并行策略、GPU型号影响。论文里用了一个线性回归模型来拟合:给定序列长度和batch size,预测Prefill耗时。
Decode阶段的服务时间则与输出序列长度线性相关,但每步的耗时还取决于当前batch里有多少个活跃序列(因为要读所有序列的KV Cache)。SARA用了一个迭代公式来估计Decode的总时间:每生成一个token,更新一次活跃序列集合,累加每步耗时。
我在实际环境里验证过这个思路,发现拟合误差主要来自两个方面:一是GPU的DVFS(动态调频)导致同样计算量耗时波动;二是KV Cache传输的延迟在跨节点时不稳定。SARA的处理方式是用滑动窗口在线更新模型参数,这个后面会细说。
3.3 SLO约束下的资源求解:一个带约束的优化问题
有了延迟模型,资源分配问题就可以形式化为:在满足SLO约束的前提下,最小化总资源成本。设Prefill节点数为N_p,Decode节点数为N_d,总成本C = c_p * N_p + c_d * N_d(c_p和c_d是单位成本)。约束是P99延迟 ≤ SLO_target。
SARA的求解思路是:先固定N_p,求出满足Prefill阶段SLO所需的最小N_p;再固定N_d,求出满足Decode阶段SLO所需的最小N_d;然后通过迭代调整,找到全局最优。由于两个阶段的延迟是耦合的(Prefill的输出速率影响Decode的到达率),所以不能完全解耦,需要用不动点迭代。
论文里给了一个具体的求解算法,大致步骤如下:
- 初始化N_p和N_d为保守估计值。
- 根据当前N_p计算Prefill阶段的输出速率(即Decode的到达率)。
- 根据到达率和SLO约束,计算所需的N_d。
- 根据N_d反推Decode阶段能承受的最大到达率,再调整N_p。
- 重复2-4直到收敛。
这个算法在数学上收敛性有保证(因为是单调映射),实际跑起来通常几轮就收敛了。
3.4 在线校准:让模型跟上真实系统
解析模型再漂亮,如果参数不准也是白搭。SARA设计了一个在线校准模块,每隔一段时间(比如30秒)采集一次运行时指标:实际到达率、实际服务时间、实际队列长度、实际超时率。然后用这些数据去修正模型参数。
具体来说,服务时间模型的系数用递归最小二乘法(RLS)更新,到达率用指数移动平均(EMA)平滑。如果发现模型预测的延迟和实际延迟偏差超过阈值(比如20%),就触发一次重新求解。
我在自己的环境里实现过类似的校准逻辑,有个经验:校准频率不能太高,否则会被噪声带偏;也不能太低,否则跟不上流量变化。30秒是个比较平衡的值,但具体要看你的流量波动周期。如果业务有明显的分钟级突发,可以缩短到10秒;如果是小时级变化,60秒也够用。
4. 实操落地:从论文到生产环境的完整路径
4.1 环境准备与依赖组件
要把SARA的思路落地,你需要一套支持分体式推理的基础设施。我假设你已经有了以下组件:
- 推理引擎:支持Prefill和Decode分离部署的框架,比如vLLM的disaggregated模式,或者TensorRT-LLM的分离式部署。
- KV Cache传输层:通常是RDMA网络或者高速以太网,需要支持零拷贝传输。
- 监控系统:Prometheus + Grafana,采集GPU利用率、队列长度、延迟分位数等指标。
- 调度器:Kubernetes或者自研调度器,支持动态调整Pod数量。
如果这些还没有,建议先把基础的分体式部署跑通,再考虑上SARA的调度逻辑。我见过一些团队一上来就想搞智能调度,结果基础设施不稳定,调度的效果根本测不出来。
4.2 关键参数采集与预处理
SARA模型需要以下几类输入参数:
| 参数类别 | 具体指标 | 采集方式 | 更新频率 |
|---|---|---|---|
| 到达过程 | 请求到达率λ | 入口网关计数 | 每秒 |
| 输入分布 | prompt长度分布 | 请求日志解析 | 每30秒 |
| 输出分布 | 生成长度分布 | 响应日志解析 | 每30秒 |
| Prefill服务时间 | 不同长度/batch下的耗时 | GPU计时器 | 每30秒 |
| Decode服务时间 | 每token耗时 | GPU计时器 | 每30秒 |
| 传输延迟 | KV Cache传输耗时 | 网络监控 | 每10秒 |
| SLO指标 | P99 TTFT、P99 TPOT | 延迟直方图 | 每秒 |
采集的时候要注意:延迟指标要用直方图而不是平均值,因为SLO通常看的是尾部延迟。Prometheus的histogram类型可以满足这个需求。
4.3 求解器的实现与调参
我用Python实现过SARA的求解器,核心逻辑大概200行代码。这里给一个简化版的伪代码:
def solve_resources(lambda_rate, input_dist, output_dist, slo_target): # 初始化 N_p = initial_guess_prefill(lambda_rate) N_d = initial_guess_decode(lambda_rate) for iteration in range(max_iter): # 计算Prefill阶段延迟 prefill_delay = queueing_model_prefill(N_p, lambda_rate, input_dist) # Prefill输出速率即Decode到达率 decode_arrival = lambda_rate # 假设无丢弃 # 计算Decode阶段延迟 decode_delay = queueing_model_decode(N_d, decode_arrival, output_dist) # 端到端延迟 e2e_delay = prefill_delay + transfer_delay + decode_delay # 检查SLO if e2e_delay <= slo_target: # 尝试减少资源 N_p, N_d = try_reduce(N_p, N_d) else: # 增加资源 N_p, N_d = try_increase(N_p, N_d) if converged(N_p, N_d): break return N_p, N_d调参方面,有几个经验值可以参考:迭代次数上限设20次足够,通常5-8次就收敛;资源调整的步长初始可以设大一点(比如10%),接近最优时缩小到2%;如果连续几轮在某个值附近震荡,就取平均值作为最终结果。
4.4 动态调整的安全机制
生产环境里,任何自动调整都需要有安全兜底。我总结了三条必须加的机制:
第一条是调整速率限制。每次调整的资源量不能超过当前总量的20%,否则可能引起震荡。比如当前有100个Prefill节点,一次最多加到120或减到80。
第二条是冷却期。每次调整后,至少等待一个完整的监控周期(比如60秒)才能再次调整。这是为了防止频繁调整导致系统不稳定。
第三条是人工覆盖开关。当系统检测到异常(比如延迟突然飙升但队列不长),自动切换到保守策略,并告警通知人工介入。我遇到过一种情况:模型预测需要扩容,但实际上是因为某个节点挂了导致延迟升高,这时候扩容反而浪费资源。
5. 常见问题与排查技巧实录
5.1 模型预测偏差大怎么办
这是最常见的问题。你按照SARA算出来的资源配置部署下去,发现实际延迟和预测差很远。排查思路如下:
首先检查服务时间模型是否准确。拿一批已知长度的请求,单独测Prefill和Decode的耗时,和模型预测对比。如果偏差超过30%,说明模型需要重新拟合。常见原因是GPU型号变了、batch size策略变了、或者推理引擎版本升级了。
其次检查到达率统计是否有误。有时候网关统计的到达率包含了被限流的请求,实际进入系统的没那么多。或者统计窗口太长,把突发流量平滑掉了。
最后检查KV Cache传输延迟。这个容易被忽略。如果Prefill和Decode跨机房部署,传输延迟可能达到几十毫秒,在端到端延迟里占比很高。SARA的模型里这一项是单独建模的,但如果你的监控没覆盖,就会导致预测偏乐观。
5.2 资源震荡怎么破
震荡的表现是:Prefill节点数一会儿加一会儿减,Decode节点数也跟着反向调整,系统始终稳定不下来。根本原因是两个阶段的调整策略耦合太紧。
我的解法是引入非对称调整:Prefill的调整周期设为Decode的两倍。因为Prefill的服务时间通常更长,对流量变化的响应本来就慢一些。让Decode先调整,等它稳定了再调Prefill,可以打破震荡循环。
另一个技巧是加死区。如果计算出的最优资源和当前资源的差异小于5%,就不调整。这能过滤掉很多微小的波动。
5.3 SLO达标但成本居高不下
有时候SLO是满足了,但资源用量比预期高很多。这时候要检查SLO目标是否设得太紧。比如P99 TTFT要求200ms,但实际业务能容忍500ms,那你就白白多配了一倍资源。建议和业务方对齐SLO,区分“必须满足”和“尽力而为”两档。
还有一个可能是排队模型过于保守。M/M/c模型假设服务时间服从指数分布,但实际的服务时间分布可能更集中(方差更小),这意味着实际排队延迟比模型预测的低。可以改用M/G/c近似或者用实测数据校准。
5.4 突发流量下的快速扩容
SARA的求解器需要几十秒到几分钟才能算出新的资源配置,但突发流量可能几秒内就来了。这时候需要快速响应通道:当队列长度超过阈值时,直接触发预设的扩容策略(比如翻倍),不等求解器。等流量稳定后,再让求解器接管,做精细化调整。
这个快速通道的阈值怎么定?我的经验是:当Prefill队列长度超过当前节点数乘以平均服务时间的2倍时,触发扩容。Decode队列类似,但阈值可以设得更宽松,因为Decode的队列消化速度更快。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 预测延迟远低于实际 | 服务时间模型过时 | 对比实测与预测 | 重新拟合模型 |
| 预测延迟远高于实际 | 到达率统计偏高 | 检查网关限流 | 修正到达率 |
| 资源频繁震荡 | 调整步长过大 | 查看调整日志 | 加死区、降步长 |
| SLO达标但成本高 | SLO过紧或模型保守 | 与业务对齐SLO | 放宽SLO或换模型 |
| 突发时扩容不及时 | 求解器太慢 | 测量求解耗时 | 加快速通道 |
| Decode节点闲置 | Prefill成为瓶颈 | 查看两阶段利用率 | 增加Prefill节点 |
| Prefill节点闲置 | Decode成为瓶颈 | 查看两阶段利用率 | 增加Decode节点 |
6. 我对这套方案的理解与扩展思考
SARA这篇论文最打动我的地方,是它没有追求花哨的算法,而是用经典的排队论解决了一个实际的工程问题。现在很多系统调度的工作动不动就上强化学习、上神经网络,但实际落地时往往因为不稳定、不可解释而被放弃。排队论虽然老,但它的假设清晰、求解可验证、结果可解释,在生产环境里反而更容易被接受。
当然,SARA也不是万能的。它的模型假设了请求之间相互独立,但实际上LLM推理的请求可能有依赖关系(比如多轮对话)。它的服务时间模型是离线拟合的,对于新出现的请求模式可能不准。它的求解器是集中式的,在超大规模集群上可能有性能瓶颈。这些都是可以继续改进的方向。
我在自己的环境里做了一些扩展,比如把SLO约束从单目标改成多目标(同时约束TTFT和TPOT),把资源类型从单一GPU扩展到异构GPU(比如A100和H100混部)。这些扩展的数学推导更复杂,但核心思路还是SARA那一套:建模、求解、校准。
最后分享一个我在实操中总结的小技巧:把SARA的求解结果和实际调整动作都记录下来,定期做回溯分析。你会发现有些调整是有效的,有些是无效的,有些甚至是反效果的。把这些案例整理成规则,可以逐步减少对求解器的依赖,形成一套更稳定的调度策略。毕竟,再好的数学模型,也要靠实际数据来验证和打磨。