状态:草稿(2026-10-27),实测。 使用一张 8GB 级笔记本 GPU(Ada 架构)、vLLM 0.31.0、Qwen3-Reranker-0.6B 于 2026-10-27 进行测量。在休眠模式测试期间,有一个容器因内存超限而被终止,该事件也作为结果的一部分记录。关于指标名称在不同版本中发生变化的内容,请参考《开源贡献记录》第 3 篇;关于 OpenTelemetry 追踪的内容,请参考《Toss ML/AI Infra 准备》第 25 篇。 2026-10-27 新增。 同日,使用 vLLM 0.32.0 重新测量了身份验证路径、启动时间和休眠模式,结果已添加至各小节的“0.32.0 重新测量”中。0.31.0 的数值保持不变。 编写规则:以实测数据为基础,无法测量的标注为“未实测”,未能确认的标注为“未确认”。
目录
1. 休眠模式:不关闭服务器释放 GPU
如果想让单张 GPU 在多个模型或训练任务之间交替使用,就必须在闲置期间释放 GPU 内存。关闭服务器后重新启动需要数十秒(第 2 节)。休眠模式在保留进程、CUDA 上下文和编译图的同时,仅释放大部分内存。
| 阶段 | 权重 | KV 缓存 | 唤醒时 | 使用场景 |
|---|---|---|---|---|
| 第 1 阶段 | 迁移至 CPU 内存保存 | 丢弃 | 从 CPU 恢复 | 准备切换回相同模型时 |
| 第 2 阶段 | 丢弃 | 丢弃 | 必须重新加载权重(reload_weights) |
需要更换为其他权重时(更新 RLHF 权重)、CPU 内存不足时 |
启用方法有两种:服务器选项和开发模式环境变量。由于休眠和唤醒是通过 HTTP 路径开放的,因此仅在开发模式下可用。
VLLM_SERVER_DEV_MODE=1 vllm serve <모델> --enable-sleep-mode
curl -X POST 'localhost:8000/sleep?level=1'
curl -X POST 'localhost:8000/wake_up'
# 2단계: 가중치 자리만 깨우고, 가중치를 다시 읽고, KV 를 깨운다
curl -X POST 'localhost:8000/sleep?level=2'
curl -X POST 'localhost:8000/wake_up?tags=weights'
curl -X POST 'localhost:8000/collective_rpc' -H 'Content-Type: application/json' -d '{"method":"reload_weights"}'
curl -X POST 'localhost:8000/wake_up?tags=kv_cache'
--enable-sleep-mode 启用后,vLLM 的 CuMem 分配器会将权重和 KV 捕获为带有标签的虚拟内存,只释放实际物理内存并在需要时重新挂载。vLLM 博客(2025-10-26,vLLM 0.11.0)报道称,在 A100 上使用第 1 阶段唤醒 Qwen3-0.6B 耗时 0.26 秒,第 2 阶段耗时 0.85 秒,比全新启动快 18~20 倍。同一篇文章指出,在 A4000 上唤醒耗时从 0.1 秒到 0.8 秒不等(厂商发布数据)。
亲自动手测量
- 环境:单张 8GB 级笔记本 GPU(Ada 架构),vLLM 0.31.0,Qwen3-Reranker-0.6B(权重 1.12GiB),
VLLM_SERVER_DEV_MODE=1 --enable-sleep-mode. - 方法:首先发送一次使用贪婪解码生成 100 个 token 的请求并记录输出哈希,然后依次执行:进入休眠 → 唤醒 → 重复四次相同的请求。记录各阶段的 HTTP 调用时间、
nvidia-smi的 GPU 内存以及容器 cgroup 的匿名内存。先进行第 2 阶段,后进行第 1 阶段。
| 项目 | 第 2 阶段(4次) |
|---|---|
| 休眠调用 | 42ms(42、42、42、50) |
| 休眠期间的 GPU 内存 | 6,168MiB → 606MiB |
唤醒(权重位置 → reload_weights → KV 位置) |
中位数 0.39 秒(首次 0.85 秒,后续 0.40·0.38·0.38 秒) |
| 唤醒后的首次请求(100个 token) | 569ms(休眠前 579ms) |
| 输出是否与休眠前一致 | 4 次全部一致 |
| 容器匿名内存 | 保持 2,750MiB 不变 |
| 项目 | 第 1 阶段 |
|---|---|
| 初次尝试(容器限额 4GiB,休眠前匿名内存 2,704MiB) | 休眠调用期间 容器发生 OOMKilled |
| 重试(新容器,限额 4.5GiB,匿名内存 2,750MiB) | 再次 OOMKilled |
解读方式如下。
- 对于该模型而言,第 2 阶段几乎没有成本。 即使从磁盘(页面缓存)重新读取 1.12GiB 的权重,也能在 0.4 秒内唤醒,唤醒后的首次请求在耗时和输出上均与休眠前一致。这比相同配置的重启(第 2 节,32 秒)快约 80 倍。这是因为 CUDA 上下文和编译图得以保留。
- 休眠期间仍残留 606MiB。 这是未由 CUDA 上下文、CUDA 图以及休眠分配器管理的缓冲区。可分配给其他任务的 GPU 内存总量需扣除这一部分。
- 第 1 阶段超出了容器内存限额。 由于第 1 阶段将权重备份到 CPU 的锁定内存中,容器内存会相应增加。理论计算上 2.75GiB + 1.12GiB 应在 4.5GiB 的限制之内,但两次都超出了。推测除了权重之外还有其他数据被迁移,或者在复制过程中短暂存在两份副本,确切数量未确认。虽然这次 OOM 仅导致单个 Pod 终止且未影响节点,但在生产环境中如果发生同样的情况,正在提供服务的引擎将整体崩溃。 若要使用第 1 阶段,必须在容器限额中预留两倍于权重大小以上的余量并先行测试。 0.32.0 重新测量(2026-10-27 0.31.0 测量 → 0.32.0 重新测量)。 0.32.0 版本发布说明记载,休眠功能已进行修复,使其能够释放工作区和 KV 初始化状态。使用相同的模型和相同的 4.5GiB 限额进行了重新测量。
| 项目 | 0.31.0 | 0.32.0 |
|---|---|---|
| 第 2 阶段休眠调用(4次中位数) | 42ms | 43ms |
| 休眠期间的 GPU 内存 | 6,168 → 606MiB | 6,170 → 606MiB |
| 第 2 阶段唤醒(4次中位数) | 0.39秒 | 0.41秒 |
| 唤醒后的首次请求,输出一致 | 569ms, 4/4 | 569ms, 4/4 |
| 第 1 阶段 | 容器 OOMKilled(两次) | 再次 OOMKilled(休眠前匿名内存 2,693MiB) |
即使版本更换,第 2 阶段的结果也几乎没有变化,休眠期间残留的 606MiB 也没有减少。第 1 阶段的内存问题依然存在。三次 OOM 均仅影响单个 Pod,对节点无影响。
- 官方文档也发出了同样的警告。文档指出,第 1 阶段将权重备份到 CPU,因此必须保证 CPU 内存充足;若 CPU 内存不足,则应使用第 2 阶段并通过
reload_weights重新读取。本次实测表明,所谓的“充足”要大于权重的大小。
2. 启动时间:编译缓存缩短了一半时间
在同一个 Pod 中多次启动服务器,并记录 /health 返回 200 为止的时间。
| 情况 | 直到就绪 | 其中 torch.compile 耗时 |
|---|---|---|
| 新 Pod 首次启动(默认配置) | 65秒、72秒(两个 Pod) | 19.6秒 |
| 相同配置再次启动 | 30~34秒 | 0.15秒(读取磁盘结果) |
| 首次使用的新配置(更改选项) | 46~62秒 | 约 19 秒 |
| 开启投机解码 | 78秒、96秒 | 19~36秒 |
vLLM 将 torch.compile 结果保留在 ~/.cache/vllm 目录下,如果模型、配置、相关环境变量、torch 版本以及 GPU 均保持一致,则会直接读取。日志中会打印 Directly load AOT compilation from path … 。由于容器新建时该缓存会消失,因此文档建议,对于需要频繁重启的服务器,应将缓存文件夹挂载到卷(Volume)中或直接打包到镜像内。此外,0.31.0 文档还列出了用于跳过内存测量的 --kv-cache-memory、完全不进行编译的 --enforce-eager(但会导致解码变慢)、以及从优化级别 -O0 到 -O3 的参数(默认 -O2)作为缩短启动时间的手段。
抛开启动时间不谈,即使服务器返回“就绪”状态后,在遇到首次遇到的请求形态时仍会触发编译。正如第 4 篇中所见,由于在首个采样批次中编译 Triton 内核耗费了数秒,日志中留下了 jit_monitor 警告。为安全起见,就绪检查最好不要仅依赖 /health ,而是使用实际的请求形态预热一次后再对外提供服务。
0.32.0 重新测量。 在新容器中首次启动耗时 60 秒,使用相同配置再次启动耗时 30 秒,表现与 0.31.0 保持一致。0.32.0 日志中新增了“JIT kernel warmup”阶段,但首个采样批次的内核编译警告以及延迟(对话模型 6.2 秒,重排模型并发 16 时 5.3 秒)依然存在。
3. /metrics:输出哪些内容
/metrics 采用 Prometheus 格式。默认服务器输出的以 vllm: 开头的指标有 38 种,投机解码服务器为 41 种,KV 卸载服务器为 53 种。我们将最适合优先加入运维仪表板的指标与实测服务器的实际值进行了汇总。
| 关注的问题 | 指标 | 本次实测中的数值 |
|---|---|---|
| 当前运行数量与等待数量 | vllm:num_requests_running, vllm:num_requests_waiting, vllm:num_requests_waiting_by_reason |
将等待原因区分为 capacity(无空位)和 deferred(临时限制) |
| KV 缓存的使用率 | vllm:kv_cache_usage_perc |
0 到 1 之间的比率 |
| 前缀缓存是否生效 | vllm:prefix_cache_hits_total / vllm:prefix_cache_queries_total |
基准服务器测试全程共 94,592 / 330,084 token |
| 提示词 token 的来源 | vllm:prompt_tokens_by_source_total{source=...} |
local_compute, local_cache_hit, external_kv_transfer(第 7 篇的 CPU 卸载) |
| 延迟 | vllm:time_to_first_token_seconds, vllm:inter_token_latency_seconds, vllm:e2e_request_latency_seconds, vllm:request_queue_time_seconds |
直方图。自 0.32.0 起可通过 --custom-histogram-buckets 调整区间 |
| 是否发生抢占(Preemption) | vllm:num_preemptions_total |
本次测试为 0 |
| 结束原因 | vllm:request_success_total{finished_reason=...} |
stop 372, length 807 |
| 投机解码的命中率 | vllm:spec_decode_num_accepted_tokens_total / ..._draft_tokens_total、按位置划分的 ..._per_pos_total |
第 3 篇 |
| 工具调用解析器输出了什么 | vllm:tool_call_parser_invocations_total{outcome=...} |
第 5 篇 |
| 是否处于休眠状态 | vllm:engine_sleep_state{sleep_state=...} |
awake, weights_offloaded(第 1 阶段)、 discard_all(第 2 阶段) |
| 模型 FLOPs 利用率 | vllm:estimated_flops_per_gpu_total 等 |
默认值为 0,通过 --enable-mfu-metrics 开启 |
指标名称在不同版本中屡有变更。例如,V0 引擎的 vllm:gpu_prefix_cache_hit_rate 已在 0.10 版本中移除,取而代之的是上述的 hits 和 queries 两个计数器。每次升级版本时,仪表板和告警规则都必须与当前服务器输出的名称进行核对。本次使用的方法很简单:通过 curl -s localhost:8000/metrics | grep '^# HELP vllm:' 提取名称列表并与规则文件进行比对。
4. --api-key 仅保护 /v1
vLLM 的安全文档明确指出:“--api-key 仅保护以 /v1, /v2, /inference, /cohere 开头的路径”,并列举了无需认证即可开放的路径。我们对此进行了实际验证。启动服务器并传入 VLLM_API_KEY(仅在此次测试中使用的随机值),在不提供密钥的情况下调用了各个路径。
| 路径 | 无密钥时 | 含义 |
|---|---|---|
GET /v1/models |
401 | 受到保护(提供密钥时返回 200) |
POST /v1/completions |
401 | 受到保护(提供密钥时返回 200) |
POST /invocations |
200 | 兼容 SageMaker 的路径。 /v1/completions 等生成功能 无密钥即可运行 |
POST /tokenize |
200 | 分词。无需密钥即可使用提示词处理开销 |
GET /metrics |
200 | 模型名称、吞吐量、队列、缓存状态 |
GET /version, GET /load |
200 | 版本信息、负载 |
POST /pause, POST /sleep |
404 | 在此版本、此设置下没有路径(休眠仅在开发模式下可用) |
0.32.0 重新测定(2026-10-27 0.31.0 测定 → 0.32.0 重新测定)。 在 0.32.0 官方容器中再次运行相同的测试后,14 个路径的状态码没有任何变化。 /invocations 在 0.32.0 中也是无需密钥即可返回 200。
/invocations 得出一个结论。 如果将此服务器直接暴露在互联网或其他团队的网络中,即使提供了密钥,任何人也都可以运行模型。 文档也 --api-key 建议不要只依赖它,而应在前方部署仅允许通过受信任路径的反向代理或网关。根据同一文档,池化模型的 /score, /rerank, /pooling, /classify 也是 /v1 没有的版本将不进行身份验证。
开发模式(VLLM_SERVER_DEV_MODE=1)则更加危险。它会开放休眠(拒绝服务)、清空缓存以及在引擎内部调用任意方法的 /collective_rpc 。文档中写明切勿在生产环境中开启。若要在生产环境中使用第 1 节的休眠模式,必须通过网络策略阻止外部访问该路径,并仅允许编排器(Orchestrator)调用。
0.32.0 将默认 CORS 从“允许所有来源”更改为“不允许任何来源”。如果是从浏览器直接调用的服务,在升级版本时必须显式指定 --allowed-origins (基于发布说明,未实际测定)。
5. 运维方面的经验总结
- 睡眠功能建议从第 2 阶段开始测试。如果想使用第 1 阶段,请在容器内存限制中加上权重大小两倍以上的余量再进行测试。在本次实际测量中,没有该余量的容器两次都直接触发 OOM 被杀掉了。
- 将编译缓存放在卷(Volume)上,可以使相同配置的重启时间缩短一半。如果更换版本或更改选项,由于缓存不匹配将重新进行编译。
- 就绪检查(Readiness Probe)应当在用实际请求形态预热(Warm-up)之后再发出。
/metrics名称需要逐个版本进行核对。- vLLM 前方必须配置负责路由选择的网关。
--api-key仅遵循/v1的规范。
6. 尚未验证的内容
- 睡眠模式下,张量并行(多 GPU)和大模型的唤醒时间尚未实际测量。
- 0.32.0 版本中的睡眠改进(工作区及 KV 初始化状态释放、CUDA 图形卸载)以及 CORS 默认值的更改尚未实际测量。
/pause·/resume在此版本中返回 404 的条件(是否存在所需单独选项)尚未确认。
参考资料
- vLLM 文档, Sleep Mode, Optimization and Tuning (Faster Startup), Production Metrics, Security (0.32.0): https://docs.vllm.ai/en/latest/
- vLLM 博客, 《Zero-Reload Model Switching with vLLM Sleep Mode》, 2025-10-26: https://vllm.ai/blog/2025-10-26-sleep-mode
- vLLM 0.32.0 发布说明 (CORS, 睡眠, 直方图区间): https://github.com/vllm-project/vllm/releases/tag/v0.32.0
- 往期文章:「开源贡献记录」第 3 篇 vLLM 删掉的指标名称、「Toss ML/AI Infra 准备」第 25 篇使用 OpenTelemetry 观测 LLM 服务