读这篇论文之前,我正好在为一个多模型推理集群头疼:三个LLM服务各自守着独立的GPU池,白天A模型流量爆炸,池子占满,B和C却闲着;到了晚上反过来。说白了就是三份独立的峰值冗余叠在一起,实测GPU利用率连50%都不到。所以当我看到“共享LLM服务的跨模型自动扩缩容”这个题目时,第一反应是终于有人把我在生产环境里最想解决的问题当成正式课题来研究了。这篇论文讨论的不是“把多个模型塞进一张卡”的粗放混部,而是从资源池视角,把扩缩容的粒度从模型级细化到请求级,并引入跨模型的容量迁移机制。无论你在做MLOps、推理平台,还是自己搭LLM服务,这篇论文的思路都值得花半小时看完——它给出了一条在不牺牲P99延迟的前提下,把GPU利用率拉高的可行路径。
1. 先搭个坐标系:共享LLM服务在解决什么问题
1.1 老方案的死穴
过去大多数团队跑LLM推理,都是一个模型独占一个GPU池,池子大小按这个模型的预估峰值流量来定。这种做法最舒服的地方是边界清晰:A模型的流量涨再猛也不会拖垮B模型,排障时责任边界一目了然。可它有一个根本性的浪费——每个池子都要为“自己的峰值”做准备,池子之间却不能互相借力。
我见过一个比较典型的集群:模型A白天高负载、模型B晚上高负载、模型C全天平稳但偶尔突刺。三套池子各自预留了30%-50%的冗余,最终整集群的综合利用率长期在30%到45%之间打转。这里面没有一个模型是有问题的,每个池子的监控曲线都正常,问题出在资源被“切碎”了。单模型自动扩缩容(比如传统的HPA思路)确实能帮每个池子在低峰缩容,但它只能在池子内部折腾,A模型峰值不够用时,哪怕旁边B模型闲得冒烟,也一点忙都帮不上。
这就是论文想打破的第一堵墙:它要的不是单模型各自优化,而是多个模型共享同一个GPU资源池,让闲置的算力在模型之间“流动”起来。
1.2 从“单模型HPA”到“请求级路由”
传统扩缩容的信号是“实例数不够了”,触发动作是“再起一台实例”,但在LLM推理场景里,这个逻辑有严重的滞后问题。LLM请求延迟敏感,P99稍微被拉高,用户体感就会断崖式变差;而新实例的冷启动(加载权重、构建CUDA图、注册到路由)往往要几十秒甚至几分钟。等监控指标告诉你“需要扩容”再动手,早超时了。
论文引入了一个关键转变:把扩缩容的动作从“创建新实例”改为“把请求调度到已有但对当前模型来说冗余的容量上”。说白了,与其等着起一台新卡,不如先把别人闲置的算力借过来用。这个动作的响应时间从分钟级降到了毫秒级,因为不涉及物理资源变动的眼,只是路由规则在变。
从单模型HPA到请求级路由,相当于把问题从“制造更多资源”变成了“更聪明地分配已有资源”。这是论文方案最核心的思维转变,后面所有机制都是围绕它长出来的。
1.3 一个简单的收益账本
假设集群里有4个模型共享同一批GPU,各自峰值流量出现的时间错开。如果每个模型单独准备峰值资源,总资源需求是各峰值之和;如果共享资源池,总资源需求是“所有模型峰值流量重叠最大时间的流量之和”。两个数字之间的差,就是共享池理论上能省下的资源。
我算过一个简化例子:4个模型的峰值需求分别是100、80、60、70单位算力,但它们的峰值错开,任意时刻总需求最大只有180。独立池方案需要310单位算力(各池各自按峰值准备),共享池只需180,省下约42%的硬件成本。这就是论文后面所有复杂机制的驱动力——省的不是零头,是一次采购预算去掉小一半。
2. 论文核心拆解:模型转换器、影子推理与路由策略
2.1 模型转换器:把“显卡份额”当流动性资源
论文里有个角色叫模型转换器(Model Converter),我第一次看到这个名字时觉得不太好懂,后来换个角度就顺了:它其实是一个“份额管理器”,负责给每个模型分配、回收、再平衡GPU算力份额。
传统方式里,模型A占2张卡就是2张卡,多出来的算力谁也不能动。转换器打破了这层绑定,它把GPU算力看作可以按比例切分的流动性资源:模型A当前可以占60%的池子容量,模型B占25%,模型C占15%。当模型A的流量涨起来,转换器会把B和C的份额临时调低,把多出来的容量划给A。
在实现上,这个转换层需要回答两个问题:什么时候转、转多少。“什么时候转”依赖负载预测和实时SLO反馈;“转多少”则是一个优化决策——在保证B、C仍满足各自SLO约束的前提下,尽可能把富余容量释放给A。论文把这个问题建模成带约束的最优化问题,目标通常是“最大化共享池的整体吞吐或GPU利用率”,约束是“每个模型各自的P99或者QPS目标不能被突破”。
用快餐店的例子类比:原来每个窗口固定配两个服务员,现在改成大堂经理(转换器)在窗口间动态调配,哪里排队深就临时调人过去。这套方案比固定窗口更考验管理能力,但收益也直接。
2.2 影子推理:把冷启动变成热切换
模型转换器解决了“算力往哪拨”的问题,但还有一个更现实的问题:即使算力份额拨给了模型A,A的模型权重可能不在这些GPU的显存里。总不能每换一次份额就重新加载几十GB参数。
论文的方案是影子推理(Shadow Inference)。这里说的“影子”,是指在正常服务流量之外,系统提前保持少量的探针性请求在目标GPU上运行,让模型权重、KV Cache、CUDA优化图始终处于活跃状态。一旦转换器决定扩容,这些“影子实例”能被立刻切换为正式服务流量,冷启动被转成了热切换。
你可以把影子推理理解为“常驻热身”。它不承担真实业务压力,业务量极小,但保证了物理侧的模型环境是温的。这与Kubernetes里常见的HPA模型完全不同——HPA是先观察到指标升高,再开始冷启动新实例;影子推理则是先把可能的扩容目标预热,等请求来了直接切。
这里有一个资源开销的取舍问题。影子推理也要消耗GPU算力,论文讨论了影子流量控制在什么比例比较合适,通常带宽占比控制在个位数百分比,比如3%-8%。影子配得太多,省下来的冗余又变成了影子开销;配得太少,高峰到来时热身容量不够,又得退回冷启动。这个比例需要根据你业务的流量波动幅度来调。
2.3 请求级路由:核心调度循环
有了转换器和影子容量,最后需要一个决策者把请求分配到具体资源上,论文的答案是请求级路由(Request-Level Routing)。它和传统负载均衡最大的区别在于:路由决策不仅看目标实例的当前负载,还要同时考虑每个模型的SLO预算和整个池子的资源水位。
我用伪代码把论文里的调度循环归纳一下:
def route_request(req, models_slo, pool_state): candidates = list_available_segments(req.model, pool_state) best_segment = None best_score = -inf for seg in candidates: if not seg.can_serve(req): continue # 调度分数 = 预期完成时间 + SLO 违约风险惩罚 score = estimate_completion_time(seg, req) penalty = slo_violation_risk(seg, req, models_slo) if score + penalty > best_score: best_score = score + penalty best_segment = seg if best_segment is None: return prepare_expansion(req.model) # 触发转换器扩容 return best_segment.assign(req)这个循环每次处理请求都会执行,它没有“等实例数变化后再调度”,而是直接回答“当前这个请求去哪张卡最合适”。这样做的好处是调度粒度极细,负载不均的容忍度极高:即使模型B总体水位偏高,只要某一张卡有富余,请求就能被导过去。
2.4 三个机制怎么咬合
转换器、影子推理、请求级路由,一环扣一环。路由发现“模型A的候选段全满”,触发了转换器;转换器从模型B和C那里回收份额;影子推理保证回收过来的GPU上模型A已经被预热,马上能接流量。
这个链路里最难的是时序配合:路由要在几十毫秒内判断“没有可用容量并触发扩缩容”,转换器要在秒级完成份额调整和流量切换,影子推理要在更早的时刻就把准备工作做完。三层节奏不同步,系统就会出现“路由觉得没容量但实际有容量”的错觉,或者“转换器正在调整但请求已经超时”的窗口。论文的价值在于把这三个时标统一到一个协调框架里,这也是我读的时候觉得工程含量最重的部分。
3. 论文的“得”与“失”
3.1 实打实的收益
论文实验里最打动我的不是宏大的架构叙事,而是几个落地的数字。在多个共享池配置下,GPU利用率能从50%左右拉升到90%以上;同型号模型混合部署时,同等硬件下可支撑的QPS有明显提升;扩缩容响应时间从分钟级缩短到秒级,同时P99没有显著劣化,部分场景甚至因为排队减少反而变好了。
这些数字合理吗?我觉得合理。关键原因是跨模型共享消除了“各自预留峰值冗余”的叠加浪费。利用率提升不是靠把卡塞得更满,而是靠把原本空转的算力拿来用。只要流量错峰确实存在,这个收益就是结构性的。
3.2 代价一:对工作负载形态有隐藏假设
论文方案最舒服的工作负载是“多个模型的峰值时间错开”。但如果你的场景里几个模型同时打满——比如一次大促把所有在线模型都推高到峰值——那么跨模型调配就没有富余容量可以借,此时还不如传统独立池子稳妥。
另外一个隐藏假设是模型的延迟敏感度相近。如果你把“稳定的离线批处理”和“在线聊天”混在一个共享池,在线请求的SLO很容易被离线任务的资源抢占波及。论文讨论的是共享LLM服务,默认模型的水位和优先级差异可控,但生产环境里模型之间优先级往往天差地别。我的建议是:共享池只放延迟敏感度接近的模型,离线任务单独走低优先级队列。
3.3 代价二:复杂度不会消失只会转移
接受这套方案之前,你得有心理准备:原本分散在每个模型侧的容量规划,现在集中到了调度器侧。监控维度从“每个池子的水位”变成“每个模型在共享池中的份额、影子容量比例、路由决策命中率”,排障难度直线上升。
调度器成了新的单点故障源。路由决策一旦出错,影响的不只是一个模型,而是池子里所有模型。论文的方案在理论上很优雅,但工程上要求调度器具备极高的可观测性、可灰度性和快速回滚能力。我没有在生产环境见过一次就上全量共享池的案例,更稳妥的做法是先把两个模型放进一个池子里试运行。
3.4 代价三:路由粒度带来的工程摩擦
请求级路由与传统推理服务的部署模式有天然冲突。很多LLM推理平台是按照“部署单元”来管理资源的,镜像、环境变量、自动扩缩策略都绑定在一个部署上。论文把路由粒度降到请求级,意味着平台层需要支持比部署粒度更细的调度能力,同时还要保证请求能被正确送到加载了对应权重的实例上。
这里有个我在落地时反复遇到的问题:论文假设“任意GPU段都能加载任意模型的权重”,但实际上你把一个70B模型往只有80GB显存的卡上放,物理上就不支持。跨模型调度还得考虑“哪些段放得下哪些模型”的亲和性约束。论文里对这些细节着墨不多,实操中却常常是卡脖子的点。
4. 落地视角:从论文到生产环境的四步走
4.1 SLO驱动的指标改造先行
论文的核心是SLO感知调度,但很多团队目前的扩缩容指标还停留在CPU利用率或者GPU显存利用率上。这两个指标在LLM推理场景都不可靠:GPU算力打满是显存还没满的常见,反之亦然;CPU更是几乎不参与主要计算。真正能反映推理服务质量的是P99尾延迟、请求队列深度、每个请求每Token生成耗时这些和SLO直接挂钩的指标。
我建议的改造顺序是:先给每个模型定义清晰可见的SLO(比如P99首Token延迟<300ms,P99端到端<5s),把这些指标的实时监控跑起来;然后再把指标接入调度决策逻辑。跳过这一步直接上跨模型调度,相当于闭着眼睛在马路上跑,纯碰运气。
4.2 把GPU池做成分层可借出的结构
论文里的“池段”概念落地上很有用。可以把每个模型的容量分成两个部分:基础容量和弹性容量。基础容量不参与跨模型调度,保证模型在绝大模型时间内的稳定表现;弹性容量在平时归模型自己使用,但允许被转换器临时借调给其他模型。
比例怎么定?我的经验是先用四六开起步:60%基础容量,40%弹性容量。跑一段时间观察借用率和SLO违约率。如果弹性容量的借出率长期很低,说明流量错峰不明显,可以调低;如果违约率上升,说明基础容量被切得太薄。
4.3 最小可行的调度循环长什么样
落地时不要一上来就追求论文里的全局最优解,先跑通一个最小可行闭环就行:
- 收集每个池段的实时水位、延迟、队列深度。
- 每5秒评估一次所有模型的SLO达标情况。
- 如果模型A有SLO超限风险,触发转换器检查池内其他模型的弹性容量。
- 找到可以被压缩的模型B,按比例释放其弹性容量。
- 路由层把A的部分流量指向新容量。
- 持续观察A的恢复情况,若恢复则逐步收回释放的容量。
这个闭环看起来简单,但已经足以消灭大部分“模型A缺卡、模型B闲着”的资源错配问题。后续再往里叠加负载预测、影子推理预热,系统会越来越接近论文描述的状态。
4.4 上线路径与可观测性设计
全量切换风险太大,我建议的路径是:先在非核心业务上选两个模型组一个共享池,跑两周,观察SLO稳定性和利用率提升;稳定后再逐步纳入对延迟要求更敏感的核心模型。上线顺序上,让所有利益相关方先看到监控数据,再谈架构改造。
可观测性是这套系统能不能活下来的关键。至少要有三块:池段水位视图(每段当前服务哪些模型、占比多少)、转换器动作日志(每次份额调整的原因、幅度、恢复情况)、路由决策命中率(请求被路由到非本模型主段的比例)。没有这些数据,调度器做得再好,你也不敢修一个调度参数。
5. 实践里最容易翻车的几个点
5.1 影子推理的“影子变真”
影子推理在实现时最怕的事:探针流量被误当成真实业务流量。比如某个请求处理框架的监控组件把影子流量一并统计进去,导致容量预测模型认为系统已经很忙,主动降低了影子带宽占比。结果真实高峰到来时,预热容量不足,整个系统措手不及。
我的对策是在请求头上打一个明确的shadow标签,全链路透传,所有监控和计费系统对这个标签做过滤。这个细节比想象中重要得多,少了它,影子流量会污染你所有的统计数据,调度器决策全部失真。
5.2 多池段副本带来的显存放大
跨模型调度带来的隐性成本是模型副本数变多:模型A的权重必须出现在好几个可供调度的池段,否则无法把请求导向目标位置。对于几B到几十B参数级别的模型,权重本身可能就有几十GB,多副本的显存开销很快吃掉调度收益。
这里需要精细管理“哪些段预加载哪些模型”。我见过一种方案:按模型大小分层,小模型可以在多个段保持活跃副本,大模型只保留在固定段,跨模型调度时优先选用同段的其他小模型弹性容量。论文里对显存物理布局讨论不多,但这恰恰是工程实现时绕不过去的一环。
5.3 会话亲和性:请求级路由的隐形陷阱
请求级路由有个前提:每个请求是独立的。但LLM多轮对话和流式输出场景里,同一个会话的后续请求必须保持亲和性——如果上一轮在这张卡上跑,这一轮换到另一张卡,至少需要重新加载上下文,严重时直接丢状态。
这个问题的标准解是会话级哈希路由:以会话ID做哈希,保证同一会话总是被路由到同一组池段。但这和论文强调的“动态调配”天然有冲突——容量被调走后,会话亲和性怎么维持?我的经验是宁可给每个模型保留一条“主路”:常规流量始终走固定主段,只有主段真正饱和时才启用共享容量,且只为无状态请求开放共享段。牺牲一部分调度自由度,换取会话稳定,这笔账是划算的。
5.4 扩缩容切换时的P99抖动
即使有影子推理,切换时刻也容易出现P99毛刺。原因是切换瞬间新容量的涌入可能导致局部过载,尤其当路由算法在短时间内把大量请求导过来时,被切换段的批处理资源可能瞬间占满。
解决思路是给切换过程加“斜坡”:新容量先接收10%流量,观测延迟正常后升到30%,再逐步到100%。论文中的算法更侧重全局最优,工程上却需要用加斜坡这种“次优”操作来保稳定性。我在实践中吃过亏:第一次全量切换时P99从80ms飙到1.5s,后来把所有扩容动作都改成多步渐进式,毛刺基本消失。
5.5 规避坑位的检查清单
如果你决定尝试这套方案,建议先过一遍自查:模型间峰值是否错开?SLO是否可量化监控?调度器是否具备灰度开关和快速回滚能力?影子流量标签是否隔离干净?会话亲和性策略是否明确?显存预算是否包含多副本开销?切换动作是渐进式还是全量式?这六个问题里任何一个没想清楚,都别急着上全量。
6. 最后说点个人的实践体会
读这篇论文最大的收获不是具体的调度算法,而是“共享”这件事的可行性和边界条件。传统基础设施的思维习惯是把资源预先切好、各用各的,这种模式稳定且好管理,但在LLM推理这种重资源、高峰值、强错峰的负载下,浪费实在太明显。
文中提到的模型转换器、影子推理、请求级路由,单独拿出来每一项都有人在尝试,但把它们放进同一个框架,让它们围绕同一个目标——在SLO约束下最大化共享池利用率——协同运转,这个组合拳我觉得是目前看到的最完整的解。
按我个人经验,最稳妥的切入点是:先只共享两个模型的弹性容量,用清晰指标驱动调度,配好影子流量隔离和会话亲和策略,跑通之后再逐步扩展。这套方案的收益上限很高,但每一步都需要先解决工程侧的可观测性和回滚能力,否则调度器反而不是节省资源的工具,而是制造新故障的源头。如果你正好在搭建或者优化LLM推理平台,我建议花点时间把论文完整读一遍,然后从最小规模的共享池开始试验——用好它是真能省下一笔可观的硬件预算的。