跳转至
推理系统手册 生产与生态 下载 notebook

生产部署与运维:Kubernetes、模型加载、弹性与发布

前面的章节关心"一个实例怎样跑得快";上线之后,更多的问题出在实例之外:GPU 节点怎样调度、几百 GB 的权重怎样快速加载、实例什么时候算"就绪"、怎样在不中断服务的情况下发布新版本、扩缩容看什么信号、出了问题从哪里查。这一章把这些运维问题按"部署 → 加载 → 探活 → 发布 → 扩缩容 → 可观测"的顺序过一遍,并用一个小模型比较几种发布策略的时间和容量代价。

自测:能答上来就可以跳过本章

  1. 一个跨两台机器的推理实例(比如 TP=8 × PP=2),在 Kubernetes 上怎么部署?
  2. 大模型的实例为什么要单独配置启动探针?就绪探针应该检查什么?
  3. 滚动发布时 maxUnavailable 和 maxSurge 怎么选?
  4. 自动扩缩容为什么不看 GPU 利用率?
  5. 怎样把冷启动从几分钟缩短到几十秒?
自测参考答案(先自己答,再展开对照)
  1. 用 LeaderWorkerSet 这类工作负载:一个实例是一组 Pod(一个 leader 加若干 worker),配合 gang 调度,要么两台机器都调度上、要么都不调度;再用拓扑标签和亲和性让它们落在网络上相邻的位置,配好 RDMA 网卡和本地 NVMe 的权重缓存。
  2. 大模型加载权重、编译、录制 CUDA Graph 要几分钟,存活探针在这期间会误判它死了、反复重启;启动探针给足时间。就绪探针应该发一个真实的小请求,确认能正常生成,而不只是检查端口开着。
  3. 同时下线的实例数(maxUnavailable)不能超过容量的余量,否则剩下的实例扛不住流量;有余量或有资源时用先加后减(maxSurge 大于 0、maxUnavailable 为 0),新实例就绪之后再下线旧的。
  4. 推理服务的 GPU 利用率几乎总是很高(连续批处理会把 GPU 塞满),不能反映是否过载;要看排队长度、KV 使用率、TTFT 的趋势。扩得快、缩得慢,最小实例数不为 0。
  5. 本地 NVMe 缓存权重(不从远端拉)、流式并行加载、预先切分好的权重(按 TP 切)、预先量化、编译缓存和 CUDA Graph 缓存,把冷启动从几分钟降到几十秒。

在 Kubernetes 上部署

LeaderWorkerSet:一个实例 = leader + workers leader Podrank 0 workerrank 1 workerrank 2 workerrank 3 组内按稳定的主机名互相找到:TP / PP 跨 Pod 建通信组 整组一起创建、一起重启(gang),少一个都不算就绪 PD 分离的形状 网关 / 路由(选实例、配比) prefill 实例 ×P decode 实例 ×D KV 经 RDMA 从 P 传到 D 一个请求怎么找到实例:Service 做四层负载均衡只会轮询,推理要按前缀缓存、按负载选实例,所以前面要有自己的路由(网关) 有状态的东西:模型权重(挂 PVC 或对象存储 + 本地缓存)、KV 卸载用的本地盘、路由表——都不该随 Pod 的生死丢掉 扩缩容的单位是整个实例(一组 Pod),不是单个 Pod;滚动发布要按组、配合探针,别让半个实例接流量
图:多机实例在 Kubernetes 上的形状——LeaderWorkerSet 与 PD 分离

这一节只是速览

Kubernetes 本身(核心对象与控制器、调度器、GPU 怎么被管起来、探针与滚动发布、多机多卡与分离式部署、Operator 与排障)在本书的 Kubernetes 与推理平台 一篇里有六章展开,并在本地 k3s 上逐条验证过。这里只列推理服务落到集群上时要做的决定。

问题 做法
GPU 资源 NVIDIA GPU Operator(驱动、设备插件、DCGM 监控);Pod 按整卡申请 nvidia.com/gpu;节点打上型号、NVLink 拓扑、网卡的标签,用亲和性调度
多机实例 一个实例跨多台机器(大 TP × PP、大规模 EP)时,用 LeaderWorkerSet 这类"一组 Pod 作为一个整体"的工作负载:一起创建、一起重启、一起扩缩;配合 gang 调度(Kueue、Volcano),避免只调度上一半的 Pod 占着卡等另一半
网络 机间的 RDMA 网卡通过设备插件或 SR-IOV 暴露给 Pod;NCCL / 传输引擎的环境变量(网卡、GID)在 Pod 规格里固定下来
存储 权重放在节点本地的 NVMe 缓存(首次从对象存储拉取),多个实例共享同一台机器上的缓存;不要每次启动都从远端下载
流量入口 推理网关(缓存感知路由,见全局调度);Kubernetes 的 Gateway API 推理扩展、llm-d 等提供了按前缀缓存和负载选择后端的能力

模型加载

冷启动的大头是加载权重。以 70B 模型(BF16 约 140 GB)为例,从对象存储按 1 GB/s 拉取要两分多钟,本地 NVMe 约 20 秒,从已经在运行的实例经 RDMA 拉取不到 1 秒(系统设计参考答案里算过)。常见的加速手段:

  • safetensors + 内存映射:按需读取、零拷贝,多个进程共享页缓存;
  • 流式并行加载:边读边往 GPU 拷贝、多线程并行读取多个文件(vLLM 的 --load-format runai_streamer,以及 tensorizer);
  • 预切分:按目标 TP 度数预先切好保存(vLLM 的 sharded_state 格式),每个 rank 只读自己的那一份;
  • 预量化:直接保存量化后的权重,省掉加载后的在线量化;
  • 缓存编译产物:CUDA Graph、torch.compile、DeepGEMM 等 JIT kernel 的编译结果放在持久化的缓存目录里,否则每次启动都要重新编译几十秒。

探活与优雅退出

  • 启动探针要给足时间(加载 + 编译 + 预热可能好几分钟),否则 Kubernetes 会在加载到一半时把容器杀掉重启,陷入循环;
  • 就绪探针不能只看进程和端口:要发一个真实的小请求(生成几个 token),确认模型加载完、CUDA Graph 录好、能正常出结果,才接流量;
  • 存活探针要保守:推理引擎在处理长请求时可能几秒不响应健康检查,误杀的代价是一次冷启动;
  • 优雅退出:收到终止信号后先从负载均衡里摘除、不接新请求,等在途请求跑完(terminationGracePeriodSeconds 要大于最长请求的时长),再退出。长的流式请求可以设一个上限,超时的由网关用已生成的内容续写。

发布策略

推理实例的冷启动很慢、每个实例都很贵,发布策略要在时间、容量和额外 GPU 之间权衡。16 个实例、流量是满容量的 80%、冷启动 3 分钟、排空在途请求 2 分钟:

import math

N, LOAD, COLD, DRAIN = 16, 0.8, 180, 120        # 16 个实例;流量是满容量的 80%;新实例冷启动 3 分钟;旧实例排空在途请求 2 分钟

for name, unavail, surge in (("逐个替换(maxUnavailable=1)", 1, 0), ("一次替换 4 个(maxUnavailable=4)", 4, 0),
                             ("先加后减(maxSurge=2)", 0, 2), ("蓝绿(整套新实例)", 0, N)):
    step = unavail + surge                                         # 每一轮替换几个
    rounds = math.ceil(N / step)
    minutes = rounds * (COLD + DRAIN) / 60
    low = (N - unavail) / N                                        # 替换过程中的最低容量
    extra = surge / N                                              # 需要额外准备的 GPU 比例
    verdict = "不过载" if low >= LOAD else "过载"
    print(f"{name}:{rounds} 轮、约 {minutes:.0f} 分钟;最低容量 {low:.0%}(流量 {LOAD:.0%},{verdict});额外 GPU {extra:.0%}")
输出
逐个替换(maxUnavailable=1):16 轮、约 80 分钟;最低容量 94%(流量 80%,不过载);额外 GPU 0%
一次替换 4 个(maxUnavailable=4):4 轮、约 20 分钟;最低容量 75%(流量 80%,过载);额外 GPU 0%
先加后减(maxSurge=2):8 轮、约 40 分钟;最低容量 100%(流量 80%,不过载);额外 GPU 12%
蓝绿(整套新实例):1 轮、约 5 分钟;最低容量 100%(流量 80%,不过载);额外 GPU 100%
  • 逐个替换最省 GPU 但很慢(一个多小时),发布期间出了问题回滚也同样慢;
  • 一次替换太多会让剩下的实例过载——流量 80% 时,同时下线的实例不能超过 20%;
  • 先加后减(maxSurge)需要一点额外的 GPU,但容量始终不降,是 GPU 有余量时的首选;
  • 蓝绿最快、回滚最简单,但要双倍的 GPU,通常只在低峰期或小规模时使用。

模型版本发布还要额外注意:先灰度一小部分流量,对比新旧版本的质量指标(线上评测、用户反馈)和延迟指标,再逐步扩大;前缀缓存和 KV 缓存池必须按模型版本隔离(KV 依赖权重);PD 分离时 prefill 和 decode 两侧要用同一个版本,发布时成对切换。

扩缩容

扩缩容的信号用排队长度、KV 使用率、TTFT 的趋势,而不是 GPU 利用率——decode 时 GPU 的 SM 利用率天然不高,按它扩容会永远扩不出去或者乱扩。实现上用 KEDA 或 HPA 读取引擎暴露的指标:vLLM 的 vllm:num_requests_waiting、vllm:kv_cache_usage_perc,SGLang 的 sglang:num_queue_reqs、sglang:token_usage。策略:

  • 按日内曲线提前扩容,突发流量靠热备实例吸收(热备的数量按冷启动时间内的流量涨幅算);
  • 扩得快、缩得慢:缩容要等一段稳定期,并且只缩空闲的实例;
  • 最小实例数不为 0(大模型的冷启动太慢,缩到 0 等于停服)。

可观测性

层次 指标 用途
用户体验 TTFT、TPOT / ITL 的 P50 / P99,goodput,错误率,按模型、租户、提示词长度分桶 SLO 与告警
引擎 运行中 / 排队中的请求数、KV 使用率、抢占次数、前缀缓存命中率、每步的 batch 大小和耗时 扩缩容信号、容量规划、调参
资源 GPU 显存、功耗、温度、NVLink / 网卡的吞吐与错误(DCGM)、XID 错误 硬件故障的早期发现
请求级 每个请求的时间线:排队、prefill、首 token、各段 decode、传输(OpenTelemetry 追踪) 定位个别慢请求

GPU 的硬件故障(ECC 错误、NVLink 降速、XID)在大规模集群里是常态:要有自动的节点隔离和替换流程,否则一张坏卡会让它所在的整个 TP / EP 组变慢或挂掉。

怎么讲清楚

"怎样上线一个 70B 模型并保证不中断",按顺序讲:部署(多机实例用 LeaderWorkerSet + gang 调度、拓扑感知)、加载(本地 NVMe 缓存、流式并行加载、预切分、编译缓存,把冷启动从分钟级降到几十秒)、探活(启动探针给足时间、就绪探针发真实请求、优雅退出排空在途请求)、发布(先加后减,同时下线的比例不超过余量,模型版本灰度并隔离缓存)、扩缩容(看排队和 KV 使用率,不看 GPU 利用率,扩快缩慢、最小实例数不为 0)。

练习

1. 为什么就绪探针要发真实请求? 进程已经在监听端口了,还可能出什么问题?

参考答案

端口可用只说明 HTTP 服务起来了,模型可能还在加载、CUDA Graph 还在录制、某张卡初始化失败(多卡实例里一个 rank 挂掉,其他 rank 会卡在第一次集合通信上)、权重加载错了版本或者量化配置不对导致输出乱码。发一个固定的小请求并检查输出(比如固定提示词的贪心输出等于预期),能发现这些问题,避免把流量打到"看起来活着、实际不可用"的实例上。

2. 发布期间的前缀缓存。 滚动发布新版本模型时,新旧版本的实例并存。路由器应该怎样处理前缀缓存的索引?

参考思路

索引按模型版本分开:同一个前缀在旧版本实例上的缓存对新版本无效。路由时先按灰度比例决定请求走哪个版本,再在该版本的实例里做缓存感知路由;会话最好在一个版本内完成(会话粘性),否则中途切到新版本,整个上下文都要重新 prefill。灰度结束后,旧版本的索引随实例下线一起删除。

小结

  • Kubernetes 上:GPU Operator、拓扑标签与亲和性;多机实例用 LeaderWorkerSet + gang 调度;RDMA 网卡与本地 NVMe 权重缓存。
  • 冷启动靠本地缓存、流式并行加载、预切分、预量化和编译缓存缩短。
  • 启动探针给足时间,就绪探针发真实请求,优雅退出排空在途请求。
  • 发布:同时下线的实例不能超过容量余量;有余量用先加后减,模型版本灰度发布并按版本隔离缓存。
  • 扩缩容看排队、KV 使用率和 TTFT 趋势,扩快缩慢;可观测性分体验、引擎、资源、请求四层,硬件故障要自动隔离。

本章练习题(1 道)

在浏览器里直接写代码、跑测试;也可以在本地用 python practice/judge.py 判题。

发现错误或有更好的写法?在 GitHub 上编辑此页提交问题