ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

分体式LLM推理资源分配:基于排队论与SLO约束的动态优化实践

分体式LLM推理资源分配:基于排队论与SLO约束的动态优化实践

1. 分体式推理到底在解决什么问题

第一次看到 SARA 这个工作,是在翻近期系统会议论文列表的时候。标题里的几个词——SLO、排队论、分体式推理、资源分配——单拎出来都不新鲜,但把它们捏在一起,指向的是一个非常具体的工程痛点:当 LLM 推理被拆成多个阶段、跑在多组异构资源上时,怎么按延迟目标动态分配算力,而不是靠拍脑袋或者静态配比。

先说清楚“分体式 LLM 推理”是什么。传统做法是把整个模型塞进一张卡或者一组卡里,请求进来从头跑到尾。但大模型推理其实分两个性质完全不同的阶段:Prefill(预填充)和Decode(解码)。Prefill 要把整段 prompt 一次性过一遍,是计算密集型,GPU 利用率高、吞吐大;Decode 是逐 token 生成,每一步只算一个 token,是访存密集型,算力大量闲置但显存带宽吃紧。把这两个阶段混在同一批资源上跑,结果就是互相拖累——长 prompt 的 Prefill 一上来,正在 Decode 的请求延迟立刻抖动。

分体式(disaggregated)推理的思路很直接:把 Prefill 和 Decode 拆到不同的资源池上,各跑各的,中间通过 KV Cache 传输衔接。这样 Prefill 池可以堆算力、Decode 池可以堆显存带宽,各自按自己的瓶颈去优化。这个思路在近两年的系统工作里已经被反复验证有效,但随之而来的是一个更难的问题:两个池子的资源配比该是多少?

配多了 Prefill,Decode 排队;配多了 Decode,Prefill 堵住。而且线上流量是波动的,白天和深夜的请求长度分布、并发量完全不同。静态配比要么浪费、要么违约。SARA 要解决的就是这件事——用排队论建模,把“配多少资源”变成一个可以实时求解的优化问题,并且把 SLO 作为约束显式写进去。

这篇适合谁看?如果你在做 LLM 推理服务的部署、容量规划、或者调度系统,尤其是已经在用或者打算用分体式架构的团队,这篇的建模思路和参数取舍值得细读。如果你只是调用 API 的应用层开发者,理解这套逻辑也能帮你判断“为什么我的请求在高峰期延迟会抖”。

2. SARA 的整体设计思路拆解

2.1 为什么是排队论而不是简单的阈值控制

很多团队第一版做资源调度,用的是阈值规则:队列长度超过 X 就扩容,低于 Y 就缩容。这套东西在传统 Web 服务里够用,因为请求的处理时间相对稳定。但 LLM 推理不一样,单个请求的处理时间方差极大——一个 50 token 的问答和一个 8000 token 的长文档摘要,Prefill 时间能差两个数量级。阈值控制在这种高方差场景下会疯狂震荡:刚扩容完一波长请求处理完了,队列瞬间清空,又开始缩容,下一波长请求进来又来不及。

排队论的价值在于,它不只看“当前队列有多长”,而是把到达率、服务率、服务时间分布都纳入进来,算出的是稳态下的延迟分布,而不是瞬时快照。SARA 用排队模型去估计“在当前资源配比下,请求的 P99 延迟大概是多少”,然后反推需要多少资源才能把 P99 压到 SLO 以下。这就从“事后补救”变成了“事前预估”。

提示:排队论模型的前提是到达过程和服务过程相对平稳。如果你的流量是突发型的(比如整点秒杀式涌入),纯排队模型会有偏差,需要叠加突发检测或者预留缓冲。这一点在论文的评估部分也有体现。

2.2 分体式架构下资源分配的特殊性

单池架构的资源分配是一维的:加机器就行。分体式是二维甚至多维的:Prefill 池加多少、Decode 池加多少、KV Cache 传输带宽要不要跟着调。而且这三个维度不是独立的——Prefill 池扩容会让更多请求更快进入 Decode 池,反而可能把 Decode 池压垮。

SARA 把这个耦合关系显式建模了。它的核心洞察是:Prefill 和 Decode 两个阶段的 SLO 约束是不同的。Prefill 主要影响的是首 token 延迟(TTFT,Time To First Token),因为用户发出请求后,要等 Prefill 跑完才能看到第一个字。Decode 主要影响的是每 token 输出延迟(TPOT,Time Per Output Token),也就是字与字之间的间隔。这两个指标对应两个不同的 SLO,需要分别约束。

这个拆分非常关键。如果只用一个笼统的“端到端延迟”做约束,模型会分不清是该优化 Prefill 还是 Decode,资源分配就会失焦。SARA 把 TTFT 和 TPOT 分开约束之后,优化目标就清晰了:在满足 TTFT SLO 的前提下,Prefill 池最少需要多少资源;在满足 TPOT SLO 的前提下,Decode 池最少需要多少资源。

2.3 方案选型的取舍逻辑

论文里没有追求“大而全”的调度框架,而是聚焦在资源配比这一个点上。这个取舍我认为是对的。分体式推理的调度问题可以拆成好几层:请求路由、批处理策略、KV Cache 管理、资源扩缩容。如果一篇论文想全解决,大概率每层都浅尝辄止。SARA 只做资源配比,但做得很深——把排队模型、SLO 约束、动态求解都打通了。

另一个取舍是在线求解 vs 离线查表。排队模型的解析解通常很复杂,在线实时求解可能有计算开销。SARA 的做法是把模型简化到可以在毫秒级求解的程度,同时保留关键的非线性特征。具体来说,它对服务时间分布做了近似处理,用更易处理的分布去拟合实际观测到的分布,牺牲一点精度换求解速度。这个权衡在实际系统里是合理的——资源调度本身不需要纳秒级精度,秒级响应就够了。

3. 核心细节解析与实操要点

3.1 排队模型的关键参数怎么定

SARA 的模型里,最核心的几个参数是:到达率 λ、Prefill 服务率 μ_p、Decode 服务率 μ_d、以及服务时间的变异系数。这几个参数不是拍出来的,而是要从实际系统里测出来。

到达率 λ 相对好测,统计单位时间内的请求数就行。但要注意,到达率要按请求类型分开统计。长 prompt 和短 prompt 的到达模式可能完全不同,混在一起算会掩盖掉关键特征。实操中建议至少按 prompt 长度分 2-3 档,分别统计到达率。

Prefill 服务率 μ_p 的定义是单位时间内能处理多少个 Prefill 请求。这个值取决于模型大小、硬件算力、批处理大小。测量方法是:固定一批请求,跑 Prefill 阶段,记录总耗时,除以请求数。但要注意,Prefill 服务率会随批大小变化——批越大,单请求平均耗时越低(因为算力利用率高),但排队等待时间可能更长。SARA 的模型里用的是“有效服务率”,也就是在最优批大小下的服务率。

Decode 服务率 μ_d 更微妙。Decode 是逐 token 的,所以服务率的单位应该是“每秒能生成多少 token”,而不是“每秒处理多少请求”。这个值主要受显存带宽限制,和批大小关系很大——批越大,显存带宽利用率越高,单 token 生成越快。但批太大会导致单请求的 TPOT 变长,因为要等同一批里其他请求的 token 一起算。

注意:测量服务率时一定要在目标硬件上测,不要用理论峰值。实际服务率受显存带宽、KV Cache 命中率、批处理策略影响,和理论值可能差 30% 以上。我见过有团队直接用 GPU 的 TFLOPS 去估算,结果上线后延迟超标一倍。

3.2 SLO 约束怎么转化成资源需求

SLO 通常是这样的形式:“TTFT 的 P99 小于 500ms,TPOT 的 P99 小于 50ms”。SARA 要做的是把这个延迟约束翻译成资源数量约束。

对于 Prefill 池,用排队模型可以算出:在到达率 λ 和服务率 μ_p 下,如果配 N_p 个 Prefill worker,TTFT 的 P99 是多少。然后反解:要让 P99 小于 500ms,N_p 至少是多少。这个反解过程可能没有解析解,需要用数值方法(比如二分搜索)来求。

对于 Decode 池,逻辑类似,但约束是 TPOT 的 P99。这里有个细节:Decode 的延迟不仅取决于 Decode 池的资源,还取决于 KV Cache 从 Prefill 池传过来的速度。如果传输带宽不够,Decode worker 会空等 KV Cache,TPOT 就会抖。SARA 的模型里把传输延迟也作为一个参数纳入,虽然论文里没有展开讲传输层的优化,但建模时考虑到了这个耦合。

实操中,我建议把 SLO 约束做成可配置的。不同业务对 TTFT 和 TPOT 的敏感度不同——对话类应用对 TTFT 更敏感(用户等第一个字不能太久),而长文本生成对 TPOT 更敏感(字与字之间卡顿很难受)。把 SLO 做成配置项,可以让同一套调度逻辑适配不同业务。

3.3 动态调整的触发条件和步长

资源配比不是算一次就完事,流量变了要重新算。SARA 的动态调整逻辑是周期性的:每隔一个时间窗口(比如 30 秒),重新测量到达率和服务率,重新求解资源需求,然后调整。

这里有两个关键决策:窗口大小和调整步长。窗口太小,测量噪声大,会导致频繁调整、系统震荡;窗口太大,响应慢,流量突变时来不及调整。论文里用的窗口是几十秒级别,这个粒度对大多数线上服务是合适的。

调整步长指的是每次调整时,资源变化的最大幅度。如果算出来需要从 10 个 worker 加到 20 个,是一次性加 10 个,还是分几步加?一次性加响应快,但可能过度调整;分步加更平滑,但可能跟不上流量变化。SARA 的做法是设置一个最大变化比例(比如每次最多调整 50%),避免剧烈震荡。

提示:动态调整一定要有“冷却期”。如果刚调整完就立刻重新评估,可能会因为系统还没稳定而做出错误决策。建议在每次调整后等待至少一个窗口时间,再允许下一次调整。

4. 实操过程与核心环节实现

4.1 从零搭建一个分体式推理测试环境

要复现 SARA 的思路,首先得有一个分体式推理的环境。这里我用最常见的开源方案来举例,不涉及任何特定厂商。

第一步是部署 Prefill 和 Decode 两个独立的推理服务。可以用同一套推理框架,但配置不同的参数。Prefill 服务重点优化计算吞吐,批大小可以设大一些;Decode 服务重点优化显存带宽利用率,批大小要适中,避免 TPOT 抖动。

第二步是打通 KV Cache 传输。Prefill 跑完之后,KV Cache 需要传给 Decode 服务。传输方式可以是共享内存(同机)、RDMA(跨机)、或者普通的网络传输。传输延迟直接影响 TTFT 和 TPOT,所以这一步的优化很重要。实操中,如果 Prefill 和 Decode 在同一台机器上,用共享内存最快;跨机的话,RDMA 比普通 TCP 快一个数量级。

第三步是加一个路由层,负责把请求分发到 Prefill 池,以及把 Prefill 的输出和 KV Cache 转发到 Decode 池。路由层还要收集监控数据:到达率、队列长度、TTFT、TPOT 等,这些是排队模型的输入。

# 示例:启动一个简单的 Prefill 服务(伪代码,具体参数按实际框架调整) python -m inference_server \ --mode prefill \ --model /path/to/model \ --max-batch-size 32 \ --port 8001 # 启动 Decode 服务 python -m inference_server \ --mode decode \ --model /path/to/model \ --max-batch-size 16 \ --port 8002

4.2 参数测量与模型拟合的完整流程

环境搭好之后,下一步是测量排队模型需要的参数。我建议按以下流程走:

第一轮:测量服务率。固定批大小,发送一批已知长度的请求,记录 Prefill 和 Decode 的耗时。Prefill 测的是“处理 N 个 token 需要多少时间”,Decode 测的是“生成 N 个 token 需要多少时间”。每个批大小测 3-5 次取平均,减少噪声。

第二轮:测量服务时间分布。服务率是均值,但排队模型还需要方差。方法是记录每个请求的服务时间,算均值和标准差,得到变异系数。如果变异系数接近 1,可以用指数分布近似;如果远小于 1,说明服务时间比较稳定,可以用确定性分布近似。

第三轮:拟合到达过程。统计单位时间内的请求数,看是否符合泊松分布。如果请求到达是独立的、随机的,泊松分布通常是个好近似。但如果有明显的周期性(比如整点高峰),需要把周期因素也建模进去。

第四轮:验证模型。用测出来的参数跑一遍排队模型,预测的 TTFT 和 TPOT 和实际测量值对比。如果偏差在 10% 以内,模型可用;如果偏差大,检查是哪个参数测不准,或者模型假设不成立。

参数测量方法注意事项
到达率 λ统计单位时间请求数按 prompt 长度分档统计
Prefill 服务率 μ_p固定批大小测耗时在目标硬件上测,不用理论值
Decode 服务率 μ_d测每秒生成 token 数注意批大小对服务率的影响
变异系数服务时间标准差/均值决定用哪种分布近似
传输延迟测 KV Cache 传输耗时跨机场景必须测

4.3 资源求解器的实现要点

有了模型参数,接下来要实现资源求解器。输入是当前到达率、SLO 约束,输出是 Prefill 和 Decode 各自需要的 worker 数量。

求解器的核心是一个数值搜索过程。以 Prefill 为例:给定到达率 λ 和 SLO(TTFT P99 < 500ms),要找最小的 N_p,使得排队模型算出的 TTFT P99 小于 500ms。因为 TTFT 随 N_p 增加而单调下降,所以可以用二分搜索。

def find_min_prefill_workers(arrival_rate, slo_ttft, mu_p, cv): """ 二分搜索最小的 Prefill worker 数量 arrival_rate: 到达率 lambda slo_ttft: TTFT 的 SLO 上限(秒) mu_p: 单个 worker 的服务率 cv: 服务时间变异系数 """ lo, hi = 1, 1000 # 搜索范围 while lo < hi: mid = (lo + hi) // 2 predicted_ttft = queue_model_ttft(arrival_rate, mid, mu_p, cv) if predicted_ttft <= slo_ttft: hi = mid else: lo = mid + 1 return lo

Decode 池的求解类似,但要注意 Decode 的服务率单位是 token/秒,而 SLO 是每 token 延迟,需要做单位换算。另外,Decode 池的资源需求还和平均输出长度有关——输出越长,Decode 池需要处理的 token 越多,资源需求越大。

注意:求解器算出来的资源数量是理论最小值,实际部署时要留缓冲。建议在理论值基础上加 10%-20% 的余量,应对测量误差和流量波动。我见过有团队直接按理论值配,结果流量稍微一抖就违约。

5. 常见问题与排查技巧实录

5.1 模型预测和实际延迟对不上怎么办

这是最常见的问题。模型说 TTFT P99 应该是 400ms,实际测出来 700ms。排查思路按以下顺序走:

先查参数测量是否准确。最常见的原因是服务率测高了。比如测 Prefill 服务率时用的是短 prompt,但线上是长 prompt,实际服务率低很多。解决办法是按 prompt 长度分档测量,用加权平均。

再查模型假设是否成立。排队模型通常假设到达过程是泊松的、服务时间是指数分布的。如果实际流量是突发的,或者服务时间分布和假设差很远,预测就会偏。解决办法是换更合适的分布,或者叠加突发检测。

最后查系统是否有额外开销。模型可能没考虑到 KV Cache 传输延迟、批处理调度开销、网络抖动等。这些开销在低负载时不明显,高负载时会放大。解决办法是在模型里加一个经验性的开销项,或者直接在实际系统里测端到端延迟来校准。

5.2 动态调整导致系统震荡怎么破

震荡的表现是:资源数量反复上下跳动,延迟也跟着抖。原因通常是调整太频繁或者步长太大。

第一个手段是加大窗口。如果窗口是 10 秒,改成 30 秒或 60 秒,让测量更稳定。代价是响应变慢,但对大多数业务来说,几十秒的响应延迟是可以接受的。

第二个手段是加冷却期。每次调整后,强制等待至少一个窗口时间再允许下一次调整。这能防止“调整-测量-再调整”的快速循环。

第三个手段是加平滑。不要直接用求解器的输出,而是用指数移动平均(EMA)平滑一下。比如新资源数 = 0.7 * 旧资源数 + 0.3 * 求解器输出。这样即使求解器输出有波动,实际调整也会平滑很多。

第四个手段是设死区。如果求解器算出来的资源需求和当前资源差距小于某个阈值(比如 10%),就不调整。避免为了微小差异频繁调整。

5.3 不同请求长度混在一起怎么处理

线上流量通常是长短混合的。长请求的 Prefill 时间长,会阻塞短请求,导致短请求的 TTFT 超标。SARA 的模型如果只用平均请求长度,会掩盖这个问题。

实操中有两个解法。一是分队列。把长请求和短请求分到不同的 Prefill 队列,各自独立调度。短请求队列优先处理,保证 TTFT;长请求队列可以容忍更长的等待。二是分档建模。在排队模型里按请求长度分 2-3 档,每档单独算资源需求,然后加总。这样更精确,但模型复杂度也更高。

我个人的经验是,如果长短请求比例比较稳定,分档建模就够了;如果比例波动大,分队列更稳。分队列的代价是资源利用率可能略低,因为两个队列的资源不能互相借用。

问题现象可能原因排查手段解决办法
模型预测延迟偏低服务率测高了按长度分档重测用加权平均服务率
模型预测延迟偏低忽略传输开销测端到端延迟模型加开销项
资源频繁震荡窗口太小看调整日志加大窗口+冷却期
资源频繁震荡步长太大看调整幅度加平滑+死区
短请求 TTFT 超标长请求阻塞分长度看延迟分队列或分档建模
Decode TPOT 抖动KV Cache 传输慢测传输延迟优化传输或加缓冲

5.4 几个容易踩的坑

坑一:用理论算力估算服务率。GPU 的 TFLOPS 是峰值算力,实际推理能用到 30%-50% 就不错了。一定要实测。

坑二:忽略批处理的影响。批大小变了,服务率也变。模型里用的服务率要对应实际使用的批大小。

坑三:SLO 设得太紧。如果 SLO 设成 TTFT P99 < 100ms,资源需求会指数级上升。SLO 要根据业务实际需求设,不要盲目追求低延迟。

坑四:不做压测就上线。排队模型是近似,实际系统有各种意外。上线前一定要做压测,验证模型预测和实际表现是否一致。

坑五:忘了监控。动态调整依赖监控数据。如果监控有延迟或者不准,调整就会失准。监控系统本身也要纳入容量规划。

6. 我对这套思路的实践体会

SARA 这套东西最值钱的地方,不是某个具体的排队模型公式,而是把资源分配从“经验驱动”变成了“模型驱动”。以前配资源靠拍脑袋或者简单阈值,现在有了一个可以解释、可以验证、可以迭代的框架。即使你不用它论文里的具体模型,这个思路也值得借鉴。

我在实际项目里试过类似的思路,最大的感受是:参数测量比模型选择更重要。排队模型本身有很多变体,M/M/1、M/M/c、M/G/1 等等,选哪个其实差别没那么大。但如果服务率测不准,再好的模型也白搭。所以如果要在团队里推这套东西,第一步应该是把监控和测量做扎实,而不是急着上模型。

另一个体会是,动态调整的工程实现比算法本身更难。算法上算出一个资源数量很简单,但要在生产系统里安全地扩缩容,涉及容器调度、负载均衡、连接迁移、状态同步等一堆工程问题。这些在论文里通常一笔带过,但实际落地时才是真正花时间的地方。

最后分享一个小技巧:如果你们的流量有明显的日周期规律,可以先用历史数据离线跑一遍模型,算出不同时段的资源需求曲线,作为动态调整的初始值。这样系统启动时就有个合理的起点,不用从零开始摸索。等运行一段时间、积累够数据之后,再逐步切换到完全动态的调整模式。这个过渡策略能显著降低上线初期的风险。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进