1. 共享推理服务里,扩缩容到底难在哪
做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零。一开始大家各占一块卡,井水不犯河水,结果 GPU 利用率长期趴在 30% 以下,老板看着账单直皱眉。于是有人提出:干脆把模型都塞到一个共享服务里,谁有请求谁用卡,按需扩缩容。听起来很美,真做起来问题一大堆。
这篇要聊的,就是论文里那个很具体的问题——共享 LLM 服务下的跨模型自动扩缩容(Cross-Model Autoscaling)。关键词里出现的 LLM、自动扩缩容、Kubernetes、Shared LLM Serving,基本勾勒出了它的全貌:在一个 Kubernetes 集群里,多个不同的大模型共享同一批 GPU 资源,系统要根据实时请求负载,动态决定"给哪个模型加副本、从哪个模型减副本"。它解决的不是单个模型的扩容问题,而是多个模型抢同一池子资源时,怎么分配才不浪费、又不掉服务质量。
为什么这件事比传统微服务扩缩容难?因为传统 Web 服务的副本是无状态的、等价的,加一个副本就是线性加一份算力。但 LLM 推理不一样:不同模型的单副本吞吐差异巨大,一个 7B 模型单卡能扛几百 QPS,一个 70B 模型可能只有几十;而且请求的 token 长度分布极不均匀,长 prompt 和短 prompt 对显存和计算时间的占用能差一个数量级。更麻烦的是,模型加载本身要花几十秒甚至几分钟,扩出来的副本不能立刻干活,缩容又怕把正在处理的请求掐断。
所以这篇文章适合三类人看:一是正在做 LLM 推理平台、被 GPU 成本压得喘不过气的工程同学;二是用 Kubernetes 部署过模型服务、想进一步做精细化调度的运维同学;三是对自动扩缩容机制本身感兴趣、想理解"跨模型"这个新维度到底新在哪的技术爱好者。我会把论文的核心思路拆开,补上论文里没细说但实操中一定会遇到的坑,尽量让你读完能对着自己的集群动手试。
2. 从单模型 HPA 到跨模型调度:问题定义的变化
2.1 传统扩缩容为什么直接搬过来不好使
Kubernetes 原生的 HPA(Horizontal Pod Autoscaler)逻辑很朴素:看某个 Deployment 的平均 CPU 或自定义指标,超过阈值就加副本,低于阈值就减副本。这套东西用在无状态 Web 服务上很稳,但搬到 LLM 推理上就露怯了。
第一个问题是指标选得不对。LLM 推理的瓶颈往往不在 CPU,而在 GPU 显存和计算单元。你拿 CPU 利用率去触发扩容,可能 GPU 早就打满了,CPU 还在那悠哉游哉。业界的常见做法是换成 GPU 利用率、显存占用、或者更贴近业务的指标——比如请求排队时长(queue time)、首 token 延迟(TTFT)、每秒生成 token 数(TPOT)。论文里通常会用排队延迟作为核心信号,因为它直接反映用户体验,而且对负载变化敏感。
第二个问题是扩容的滞后性。LLM 副本从"决定扩容"到"真正能接请求",中间要经历调度、拉镜像、加载权重、预热这几个阶段。一个 70B 模型冷启动动辄一两分钟,等你扩出来,流量高峰可能都过去了。这就引出了"预测式扩容"和"预留缓冲副本"的思路,后面会细讲。
第三个问题,也是最关键的:多模型共享时,扩容决策不再是独立的。单模型场景下,你给模型 A 加副本,不影响模型 B。但在共享池子里,GPU 总数是固定的,给 A 加一张卡,就意味着 B 少一张卡。这时候"给谁加"变成了一个资源分配的博弈问题,而不是简单的阈值触发。
2.2 跨模型扩缩容的形式化描述
把问题说清楚一点。假设集群里有 N 个模型,共享 M 张 GPU。每个模型 i 当前有 r_i 个副本,每个副本占 g_i 张卡(大模型可能一张卡放不下,要张量并行)。系统在每个决策周期观测到每个模型的请求到达率、排队长度、平均处理时间,然后决定下一周期每个模型的副本数 r_i'。
目标通常有两个,而且是矛盾的:一是服务质量约束,比如每个模型的 P99 排队延迟不能超过某个阈值;二是资源效率,比如整体 GPU 利用率要尽量高,或者说在满足服务质量的前提下用的卡越少越好。
这里有个容易被忽略的细节:副本数不是连续可调的整数那么简单。因为 g_i 可能大于 1,而且不同模型的 g_i 不同,所以资源分配是个带约束的整数规划问题。比如总共 8 张卡,模型 A 每副本 2 卡,模型 B 每副本 1 卡,那可能的组合是有限的几种。论文里一般会把这个约束显式写出来,实操中你也要在调度器里体现,否则扩出来的副本可能因为凑不齐卡而一直 Pending。
2.3 "跨模型"三个字的价值到底在哪
如果只是把多个模型各自跑一套 HPA,那叫"多模型扩缩容",不叫"跨模型"。跨模型的核心在于共享与腾挪:当模型 A 的流量低谷时,它占着的卡可以被回收,临时借给正在高峰的模型 B;等 A 的流量回来,再从 B 那里要回来。这种动态腾挪,才是把 GPU 利用率从 30% 拉到 70% 的关键。
但腾挪是有代价的。模型 B 的副本被缩掉,正在处理的请求要么等它处理完(drain),要么被迁移到其他副本重试。前者会让缩容变慢,后者会浪费已经算了一半的计算。所以论文里通常会设计一套"优雅缩容"机制:先停止给某个副本派新请求,等它把在途请求处理完,再真正释放资源。这个 drain 的超时时间设多少,是个需要根据业务调的参数,设短了会掐断请求,设长了资源回收不及时。
3. 论文方案拆解:控制器、预测与调度三层结构
3.1 整体架构:谁在观测,谁在决策,谁在执行
论文这类系统一般会拆成三层,我按自己的理解重新组织一下,方便你对照自己的集群。
最上面是指标采集层。它从每个模型副本那里收集请求队列长度、处理延迟、GPU 利用率等信号,聚合成每个模型的全局状态。这一层的关键是采样频率和聚合窗口。采太快,指标抖动大,扩缩容会来回震荡;采太慢,反应迟钝,高峰来了还没察觉。实践中常用 15 到 30 秒的窗口做平滑,再配合一个短周期的快速响应通道处理突发流量。
中间是决策层,也就是论文的核心。它拿到所有模型的状态后,跑一个分配算法,算出每个模型下一周期应该有多少副本。这个算法要同时考虑服务质量约束和资源效率,还要处理"副本数必须是整数""每副本占卡数固定"这些约束。论文里常见的做法是把它建模成一个优化问题,用启发式或近似算法求解,因为精确求解在模型数量多的时候太慢。
最下面是执行层,对接 Kubernetes。决策层给出目标副本数,执行层通过修改 Deployment 或自定义 CRD 的 replicas 字段来落地。这里要注意,Kubernetes 的调度和实际 Pod 就绪之间有延迟,所以执行层最好能感知 Pod 的真实状态,而不是发完指令就当完成了。
3.2 决策算法:怎么在约束下做分配
决策算法是论文最值得细看的部分。我把它拆成几个关键设计点。
第一,服务质量怎么量化。常见做法是用排队论近似。把每个模型副本看成一个服务台,请求到达服从泊松分布,服务时间服从某个分布,然后用 M/M/c 或者更复杂的模型估算排队延迟。但 LLM 推理的服务时间分布很怪,长尾特别严重,简单排队论会低估延迟。所以论文里往往会用实测数据拟合,或者干脆用一个轻量的延迟预测模型。
第二,资源怎么在模型间分配。一个直观的思路是"按需分配 + 保底"。先给每个模型分配一个最小副本数,保证低谷时也能服务;剩下的卡作为共享池,按各模型当前的"压力"比例分配。压力可以用排队长度乘以平均处理时间来衡量,也就是系统里积压的总工作量。谁积压得多,谁多拿卡。
第三,怎么避免震荡。如果每个周期都重新分配,模型之间会来回抢卡,系统不稳定。常见的抑制手段有三种:一是加滞后带,只有压力差异超过一定比例才调整;二是加调整步长限制,每次最多增减一个副本;三是加冷却时间,刚调整过的模型在一段时间内不再动。这几个手段我在实际系统里都见过,效果最好的是步长限制加冷却时间组合。
第四,冷启动怎么处理。因为扩容有延迟,决策层不能等压力真的上来了才扩。论文里一般会用预测:根据历史流量模式,提前在高峰到来前把副本扩好。预测可以是简单的周期性模型(比如每天几点是高峰),也可以是时间序列模型。实操中,周期性规律对大多数业务已经够用,上复杂的预测模型反而容易过拟合。
3.3 与 Kubernetes 的对接细节
论文往往对 Kubernetes 的对接一笔带过,但这恰恰是落地时最花时间的地方。我补充几个关键点。
自定义指标怎么喂给决策层。Kubernetes 原生的 metrics server 只提供 CPU、内存这类基础指标,LLM 的排队延迟、GPU 利用率需要你自己采集并暴露。常见做法是用 Prometheus 采集,然后决策层直接查 Prometheus,或者通过 custom metrics API 把指标接进 HPA。如果决策逻辑复杂,建议不要硬塞进 HPA,而是自己写一个控制器,用 Operator 模式管理。
副本数怎么改。直接改 Deployment 的 replicas 最简单,但 Deployment 的滚动更新语义和 LLM 的优雅缩容需求不完全匹配。更精细的做法是用 StatefulSet 或者自定义 CRD,自己控制 Pod 的生命周期,在缩容前先给 Pod 发一个"准备下线"的信号,等它 drain 完再删。
GPU 怎么调度。多模型共享 GPU 时,调度器要保证每个 Pod 拿到的卡数符合模型需求。Kubernetes 默认调度器对 GPU 的支持是"整卡分配",如果你要做显存切分或者 MIG,需要额外的设备插件。论文里假设的是整卡分配,实操中如果你的模型小,可以考虑用 MIG 把一张卡切成几份,提高利用率,但这会引入新的复杂度。
4. 实操中绕不开的坑与调参经验
4.1 指标采集的坑:你以为的延迟不是用户感受的延迟
我踩过最深的坑,是指标口径和用户体验对不上。系统里统计的"处理延迟"是从请求进入推理引擎开始算的,但用户感受到的延迟是从发出请求到收到第一个 token。中间还隔着网关排队、负载均衡、网络传输。如果你只用引擎内的延迟做扩缩容,网关那一层的排队会被忽略,结果就是引擎看着很闲,用户却觉得慢。
正确的做法是端到端埋点。在网关层记录请求进入时间,在推理引擎记录首 token 输出时间,两者相减才是真实的 TTFT。扩缩容决策应该以这个端到端指标为准。这个改动不大,但效果立竿见影,我调过一次之后,高峰期的用户投诉明显下降。
另一个坑是指标聚合的粒度。如果你把所有副本的延迟简单平均,长尾会被掩盖。一个副本卡死,其他副本正常,平均值可能还是好看的。所以要看 P95、P99,甚至要看最大排队长度。论文里一般用 P99,实操中我建议同时监控 P99 和队列最大长度,两个都超阈值才触发扩容,避免误判。
4.2 缩容的坑:drain 超时设多少才合适
缩容比扩容难,因为扩容只是加资源,缩容要处理在途请求。核心参数是 drain 超时:给副本多长时间把手上的请求处理完。
设太短,比如 10 秒,长请求会被掐断,用户收到错误。设太长,比如 5 分钟,资源回收慢,腾挪效率低。我的经验是按 P99 处理时间的两倍来设。比如你的 P99 请求处理时间是 20 秒,那 drain 超时设 40 秒左右比较稳。同时要配合一个"停止派发"的机制:决定缩容某个副本后,先把它从负载均衡里摘掉,不再给它新请求,等它自然处理完。
还有个细节:流式请求的 drain 判断。LLM 很多是流式输出,一个请求可能持续几十秒一直在吐 token。判断它"处理完"不能只看连接是否关闭,要看是否输出了结束标记。这个在实现时要和推理引擎的协议对齐,否则会误判。
4.3 冷启动的坑:预热不只是加载权重
很多人以为冷启动就是加载模型权重,其实远不止。一个 LLM 副本从启动到能稳定服务,要经历:拉镜像、启动进程、加载权重到显存、编译计算图(如果用 TorchScript 或 TensorRT)、跑几个预热请求把 kernel 都激活。最后一步特别容易被忽略,如果不预热,第一批真实请求会特别慢,因为很多计算路径是懒加载的。
我的做法是在副本就绪探针里加预热逻辑。Pod 启动后,先自己发几个不同长度的请求跑一遍,确认延迟稳定了,再把就绪探针置为通过。这样负载均衡只会把流量发给真正准备好的副本。预热请求的长度要覆盖你的典型场景,短 prompt 和长 prompt 都跑一下。
4.4 震荡的坑:为什么你的副本数一直在跳
扩缩容震荡是共享服务里最烦人的问题。表现是副本数每隔几分钟就上下跳一次,GPU 利用率看着也不稳定。根因通常是决策周期和指标窗口不匹配。
举个例子,你的决策周期是 30 秒,指标窗口也是 30 秒。这一周期看到压力大,扩了一个副本;下一周期新副本还没完全接流量,压力看着还是大,又扩一个;再下一周期流量被消化了,压力骤降,一下缩两个。来回震荡。
解决办法是让指标窗口远大于决策周期。比如决策周期 30 秒,指标窗口用 5 分钟。这样每次决策看到的是平滑后的趋势,不会被瞬时波动带偏。再配合前面说的步长限制(每次最多动一个副本)和冷却时间(调整后 2 分钟内不再动),基本能压住震荡。我调这套参数花了大概两周,最后稳定下来的配置是:决策周期 30 秒,指标窗口 5 分钟,步长 1,冷却 120 秒。
4.5 多模型公平性的坑:别让大模型饿死小模型
共享池子里,大模型因为单副本吞吐低、排队容易积压,往往在抢卡时占优势。如果不加约束,小模型可能长期拿不到卡,服务质量崩掉。这就是公平性问题。
论文里一般会给每个模型设一个最小副本数作为保底,保证它在最坏情况下也能服务。但最小副本数设多少有讲究:设太高,低谷时浪费;设太低,高峰时抢不过别人。我的经验是按每个模型的基线流量来设,也就是它日常低谷时的请求量对应的副本数,再留一点余量。这样保底够用,又不至于占着卡不干活。
另外可以引入优先级。核心业务的模型给高优先级,抢卡时优先满足;非核心的可以容忍更高延迟。这个在决策算法里体现为一个权重,压力相同时优先给高优先级模型加卡。
5. 落地路线与效果验证思路
5.1 从零搭一套的最小可行路径
如果你现在就想动手,我建议分三步走,别一上来就追求论文里的完整方案。
第一步,先把指标打通。用 Prometheus 采集每个模型副本的排队长度、TTFT、GPU 利用率,在 Grafana 上画出来。这一步不涉及任何自动决策,但能让你看清流量的真实形态,为后面调参打基础。很多人跳过这步直接上自动扩缩容,结果参数全靠猜,效果很差。
第二步,单模型自动扩缩容跑通。先拿一个模型试,用自定义指标驱动副本数变化,把前面说的滞后带、步长、冷却都加上。这一步的目标是验证你的指标口径对不对、drain 机制好不好使、冷启动要多久。跑稳了再谈多模型。
第三步,引入共享池和跨模型腾挪。把多个模型放进同一个资源池,实现最小副本保底加共享池按压力分配。这一步最容易出问题的是资源约束,要确保决策层算出来的副本组合真的能在集群里调度出来,否则会出现"决策说扩,实际 Pending"的尴尬。
5.2 怎么验证扩缩容真的有效
验证不能只看 GPU 利用率上去了没有,那可能只是把卡占满了但服务质量崩了。要同时看两组指标。
一组是效率指标:整体 GPU 利用率、单位请求的 GPU 成本、资源闲置率。这些反映你省了多少钱。
另一组是质量指标:各模型的 P99 TTFT、P99 端到端延迟、错误率、超时率。这些反映用户体验有没有受损。
理想的结果是效率指标上升的同时,质量指标不劣化。如果质量指标变差,说明你的扩缩容太激进,缩得太多或者扩得太慢。这时候要回看决策参数,通常是保底副本数设低了,或者扩容触发阈值设高了。
我一般会做一个对照实验:固定副本数的静态部署 vs 自动扩缩容,跑同样的流量回放,对比两组指标。流量回放可以用线上的真实请求日志,去掉敏感信息后重放。这样对比最有说服力,也能帮你找到参数的最优区间。
5.3 这套方案不适合什么场景
最后说点泼冷水的话。跨模型自动扩缩容不是万能的,有些场景硬上反而添乱。
如果你的模型数量很少(比如就一两个),而且流量很平稳,那静态分配加简单的单模型 HPA 就够了,跨模型调度带来的复杂度不划算。
如果你的模型冷启动特别慢(比如几百 B 的模型,加载要十几分钟),那动态腾挪的意义不大,因为等你腾出来高峰都过了。这种场景更适合预留固定资源,用队列和批处理来削峰。
还有就是强隔离要求的场景。有些业务对延迟极其敏感,不能容忍和其他模型共享资源带来的干扰。这种就得单独划资源池,跨模型调度在这里是负优化。
判断标准很简单:你的 GPU 利用率是不是长期低于 50%,同时各模型的流量高峰是不是错开的。如果两个都满足,跨模型扩缩容值得一试;如果利用率本来就高,或者流量高峰高度重叠,那省下来的空间有限,不如把精力花在推理优化上。
我在实际项目里推这套方案时,最大的体会是:算法只是一半,另一半是工程细节。论文里的决策算法可能几十行公式就写完了,但把它落地成稳定的系统,指标采集、优雅缩容、冷启动预热、震荡抑制这些"脏活"占了八成工作量。所以别被论文的优雅迷惑,动手时把精力放在这些细节上,效果才出得来。