LLM 冷启动治理生产实战:用 Local NVMe Cache、Weight Streaming 与 Warm Pool 缩短模型拉起时间
很多团队优化 LLM Serving 时,把重点放在 HPA、KEDA、队列长度、GPU 利用率和副本数。这些机制解决的是「什么时候扩容、扩多少」,但在大模型场景里,一个新 Pod 被调度到 GPU 节点之后,距离真正进入 Ready 往往还隔着一条很长的启动链路:
Pod Scheduling -> Container Image Ready -> Model Weights Fetch -> Weights Read / Deserialize -> CPU to GPU Transfer -> Runtime Initialization -> CUDA Graph / Kernel Warmup -> Readiness
因此,真正应该度量的不是单一 Pod startup time,而是完整的冷启动总时长:
cold_start_total = scheduling_time + image_prepare_time + weight_fetch_time + weight_load_time + runtime_init_time + warmup_time
对几十 GB 甚至数百 GB 的模型来说,weight_fetch_time 和 weight_load_time 经常才是主导项。如果每次扩容都从对象存储或 Hugging Face Hub 重新下载模型,那么即使 Kubernetes 在几秒钟内完成调度,业务看到的仍然是分钟级的可服务延迟。
这也是冷启动治理和自动扩缩容治理的边界:Autoscaling 决定「要不要新副本」,Cold Start Engineering 决定「新副本多久能真正提供推理」。
核心原则:让权重离 GPU 更近,让加载路径更少串行等待
1. 用 Local NVMe 把「远程下载」变成「节点内读取」
KServe 的 Local Model Cache 直接针对这一问题:提前把模型权重缓存到 GPU 节点的本地磁盘,典型介质就是 NVMe。InferenceService 启动时,如果目标节点已经存在对应模型,就可以从本地缓存读取,而不是重新从远程仓库下载。
KServe 当前的模型缓存设计包含 LocalModelCache、LocalModelNamespaceCache、LocalModelNodeGroup 和 LocalModelNode 等资源。其中 namespace-scoped cache 还能用于多租户隔离,避免所有命名空间无边界共享缓存。
一个简化配置示意如下:
apiVersion: serving.kserve.io/v1alpha1
kind: LocalModelCache
metadata:
name: qwen-production-model
spec:
sourceModelUri: "hf://your-org/your-model"
modelSize: 40Gi
nodeGroups:
- gpu-workers
真正落地时,重点不只是「有没有缓存」,而是要定义清楚:
- 哪些模型必须预热到所有 GPU 节点;
- 哪些模型只在部分节点缓存;
- 节点磁盘不足时谁先被淘汰;
- 新版本权重发布后,旧缓存何时失效;
- 模型文件校验失败时是否允许回源。
如果没有这些规则,本地缓存很容易从冷启动优化手段变成磁盘容量事故源。
2. 用 Weight Streaming 缩短「读文件 → CPU → GPU」的串行链路
即使权重已经在对象存储或高速文件系统里,传统加载方式仍可能经历多个阶段:先完整下载或 mmap,再由 CPU 读取,最后逐步送入 GPU。
vLLM 已支持 Run:ai Model Streamer。其核心思路是并发读取 tensor,并把权重流式送入 GPU;同时支持从 S3、GCS、Azure Blob 等对象存储直接加载,并可调节读取并发度、CPU buffer 大小以及 distributed streaming。
例如:
vllm serve s3://llm-models/qwen-prod \
--load-format runai_streamer \
--model-loader-extra-config '{"concurrency":16}'
对于多卡模型,还可以评估 distributed streaming 或预分片 checkpoint,使每个 rank 更接近「只加载自己真正需要的权重」。
这里要避免一个常见误区:Weight Streaming 不等于任何环境下都一定更快。 实际收益取决于对象存储吞吐、网络带宽、CPU 内存带宽、GPU H2D 带宽、checkpoint 格式和并发参数。生产上线前应按真实模型、真实节点和真实存储做基准,而不是照搬固定 concurrency。
3. Warm Pool 不应只有「GPU 常驻」一种形态
完全 Scale-to-Zero 最省钱,但它把所有冷启动阶段都留给首个请求;所有模型都常驻 GPU 又会造成显著闲置成本。更实用的做法是把模型划成多级温度状态:
| Tier | 状态 | 成本特征 |
|---|---|---|
| Tier 0 | Active GPU Replica | 最高成本,最快启动 |
| Tier 1 | CPU-resident / Sleeping Replica | 中等成本,较快唤醒 |
| Tier 2 | Node-local NVMe Model Cache | 低成本,需重新装载 |
| Tier 3 | Remote Object Storage | 最低成本,最慢回源 |
不同模型根据 SLO、调用频率和启动成本进入不同层级。例如,高频核心模型维持少量 GPU Warm Replica;中频模型可以保留容器与 CPU 侧权重;低频长尾模型只保留节点 NVMe 缓存;极低频模型才完全回落到远端对象存储。
vLLM 的 Sleep Mode 为 Tier 1 提供了一个很有价值的实现思路。Level 1 会把模型权重 offload 到 CPU 并丢弃 KV Cache,从而释放大量 GPU 内存,之后可以重新 wake up,而不必从远端重新获取同一套权重。
但要注意,vLLM 文档明确说明在线 Sleep/Wake 控制端点需要开发模式开启。生产环境不要把这类端点直接暴露到公网或租户侧,而应由内部生命周期控制器调用,并配合权限、超时和状态机。
4. 调度时考虑「模型在哪里」,而不只是「哪张 GPU 空闲」
冷启动优化最容易被忽略的一层,是 placement。假设集群里有两台空闲 GPU 节点:
- Node A 已经在本地 NVMe 缓存目标模型;
- Node B 完全没有该模型;
- 两台机器 GPU 型号、显存和当前利用率完全相同。
传统调度器可能随机选择 B;从 GPU 资源角度看没有问题,但从模型启动时间看,这是明显的次优选择。
ServerlessLLM 的一个关键设计就是 startup-time-aware scheduling:根据 checkpoint 在不同服务器上的本地性状态,选择预计启动时间更短的服务器,而不是只看抽象计算资源。
在 Kubernetes 环境里,这个思想可以落成一个简单规则:
Score(node, model) = GPU_fit_score + model_locality_score - remote_fetch_penalty - cache_pressure_penalty
模型本地性应成为调度信号,而不是启动之后才被动发现的事实。
工程落地:建立一条可观测的冷启动状态机
建议把每个模型副本的启动过程拆成可观测状态,而不是只记录一个 Pod Ready 时间:
PENDING_GPU -> IMAGE_READY -> CACHE_LOOKUP -> FETCHING_WEIGHTS -> LOADING_WEIGHTS -> RUNTIME_INIT -> WARMING_UP -> READY
至少采集以下指标:
cold_start_total_seconds:从扩容决策到 Ready 的总时间;model_fetch_seconds:远程获取权重时间;model_load_seconds:权重读取与装载时间;local_model_cache_hit_rate:节点本地模型缓存命中率;remote_weight_bytes:每次启动从远端读取的权重字节数;warm_pool_hit_rate:请求触发扩容时是否命中 Warm Pool;sleep_wakeup_seconds:sleep replica 恢复到 Ready 的时间;cold_start_failure_rate:下载、校验、OOM、初始化失败占比。
如果只监控 pod_startup_latency,你只能知道「慢」;拆开以后才能知道究竟是 Scheduler、Registry、S3、NVMe、CPU 读取、H2D 还是 runtime warmup 在拖延。
一个推荐的生产策略
可以按模型热度和 SLO 做三级策略。
| 层级 | 策略要点 |
|---|---|
| 高频核心模型 | 保留 1~N 个 Active Warm Replica;所有目标 GPU 节点预缓存模型到 NVMe;扩容优先调度到 cache hit 节点;重点优化 p95/p99 冷启动时间,而不是追求 Scale-to-Zero。 |
| 中频模型 | 允许 GPU Scale Down;尽量保留 CPU-resident / Sleeping 状态;节点 NVMe 保留完整权重;需要时快速重新占用 GPU。 |
| 长尾模型 | 不保留 GPU Warm Replica;只缓存最常用版本到部分 NVMe 节点;对极低频模型允许回源对象存储;给调用方暴露更宽松的冷启动 SLO。 |
这种策略本质上是在 GPU 成本、CPU 内存、NVMe 容量、网络流量和启动延迟之间做分层交换,而不是用一个统一的 minReplicas 参数解决所有模型。
适用场景
这套方法尤其适合以下场景:
- 一个 GPU 集群托管大量不同基础模型;
- 模型权重较大,扩容后加载时间明显高于 Pod 调度时间;
- 需要 Scale-to-Zero 或低保有量运行长尾模型;
- 对象存储与 GPU 节点之间网络距离较远;
- 模型版本更新频繁,需要可控的缓存预热和失效机制;
- 业务有明确 TTFT / availability SLO,但不愿长期保留大量空闲 GPU。
如果只有一个固定模型、流量长期稳定且所有 GPU 都常驻,那么冷启动治理的收益反而有限,优先优化运行态吞吐和成本更合理。
常见误区
误区一:把 minReplicas 调大就是解决冷启动。 它只能降低「冷启动被用户撞到」的概率,不能解决节点重建、故障迁移、模型版本切换和突发扩容时的权重加载问题。
误区二:只优化容器镜像。 大模型服务中,容器镜像通常远小于模型权重。Image Pull 优化重要,但不要把几百 MB 镜像问题当成几十 GB 权重问题。
误区三:共享网络盘等于本地缓存。 共享盘避免了重复从互联网下载,但不代表具备本地 NVMe 的数据路径。并发扩容时,网络存储本身可能成为新的热点。
误区四:缓存不做版本治理。 如果模型 URI、revision、tokenizer、config 与权重文件版本没有一起管理,很容易出现「权重命中缓存,但实际版本不一致」的隐蔽问题。缓存键至少应包含模型标识和不可变 revision。
误区五:Readiness 过早。 进程启动不代表模型可服务。Readiness 应在权重加载、runtime 初始化和必要 warmup 完成后才置为成功,否则流量会被送到一个「活着但还不能推理」的实例。
误区六:把开发模式 Sleep/Wake 端点直接对外。 Sleep/Wake 是生命周期控制能力,不是业务 API。生产环境必须放在内部控制面后面,并限制调用主体。
上线检查
上线前建议至少完成以下检查:
- 模型权重是否使用不可变版本号或 revision;
- GPU 节点 NVMe 容量是否有高水位与淘汰策略;
- 是否能统计 Local Model Cache hit / miss;
- cache miss 时的回源路径是否限流;
- 模型加载并发是否压测过 CPU、网络和对象存储;
- readiness 是否覆盖完整 warmup;
- Warm Pool 是否设置最大数量和最大空闲时间;
- 调度器是否优先 cache hit 节点;
- 冷启动阶段是否能定位具体耗时分段;
- 新模型发布前是否支持提前预热节点缓存;
- 缓存文件是否有 checksum / 完整性校验;
- 发生 OOM、下载中断或版本不一致时是否能自动回滚。
FAQ
LLM 冷启动为什么不能只靠增加 minReplicas 解决?
因为 minReplicas 只是在运行时保留更多常驻副本。只要发生节点故障、版本切换、突发扩容或新模型首次部署,权重获取和加载成本仍然存在。真正的治理需要同时处理模型本地性、加载路径和温度分层。
本地 NVMe 缓存和共享网络盘有什么本质区别?
本地 NVMe 的优势不是「文件存在磁盘」,而是数据已经位于目标 GPU 节点附近。共享网络盘仍然要经过网络和共享存储层,在大规模并发加载时可能发生带宽竞争。
vLLM Sleep Mode 能直接当生产 Warm Pool 吗?
可以借鉴其 GPU 内存释放和快速恢复机制,但生产上应由内部控制面封装,而不是直接对外暴露开发模式端点。还需要额外处理副本状态、并发唤醒、失败恢复和资源配额。
参考资料
- USENIX OSDI 2024 — ServerlessLLM: Low-Latency Serverless Inference for Large Language Models:https://www.usenix.org/conference/osdi24/presentation/fu
- KServe — Local Model Cache:https://kserve.github.io/website/docs/next/model-serving/generative-inference/modelcache/localmodel
- vLLM — Loading models with Run:ai Model Streamer:https://docs.vllm.ai/en/v0.18.0/models/extensions/runai_model_streamer/
- vLLM — Sleep Mode:https://docs.vllm.ai/en/v0.13.0/features/sleep_mode/