显存不够用这件事,做过多模型部署的同学应该都懂。尤其是手里就一两张卡,却要同时跑好几个不同尺寸的大模型时,每加一个模型都是一场显存博弈。vLLM 的 Sleep 模式就是冲着这个痛点来的——把暂时不用的模型从 GPU 里挪出去,把显存还给其他服务或新任务,等要用的时候再拉回来。我实际跑了几个场景,单卡上同时部署多个模型时,空闲模型的显存占用率能直接从 70% 掉到不足 10%,这 90% 的资源说回收就回收。这篇文章把 Sleep 模式从原理到落地的完整经验写清楚,适合正在做模型服务化、多模型调度或者手里 GPU 资源比较紧的工程师参考,新手读完也能直接照着配。
1. 为什么需要 Sleep 模式:多模型共存的显存困境
1.1 试图“全都放 GPU”的几种老办法
早期我部署多模型时,最常见的思路是“谁要用就手动切换”。做法是:先把 A 模型从 GPU 卸掉,再加载 B 模型,用完再换回来。这种方案确实能把一个 GPU 轮流给多个模型用,但缺点很明显——每次切换都得把模型重新从磁盘读到显存,一个大模型光权重就是几十 GB,冷启动时间动辄一两分钟,线上根本等不起。
后来有人换成了“共享进程方案”,也就是把几个模型放进同一个常驻进程,通过内部逻辑决定让哪个模型响应请求。这看起来优雅了,但问题在于:模型权重只要加载进显存,不管有没有请求都会一直占着。两张 24G 的卡,光放两个 7B 模型加一个 13B 模型,权重的显存占用就已经超过 30G,剩下留给 KV Cache 的空间被压缩得几乎没有——一旦并发请求稍微上来一点,显存直接爆。到头来还是得退回去用“手动切换”,只是换了个壳。
很多团队也试过用 request-level 调度的框架去管理模型路由,但底层显存并没有真正被复用。打个比方,这就好比一个房间里堆了几台大机器,你只是把其中一台的电源关了,机器本体还是占着地方——真正想腾位置,必须把它搬出去。
1.2 Sleep 模式到底在做一件什么事
vLLM Sleep 模式解决的就是这个“搬出去”的问题。它不只是在软件层把模型标记为暂时不可用,而是真正把 GPU 显存里的权重、KV Cache 释放掉,让显存可以被其他模型或任务立即使用。
我自己的理解是:它相当于给模型实例做了一个“休眠”操作。休眠状态下,模型权重不再留在 GPU 上,而是被挪到 CPU 内存(或者直接丢弃,下次再从磁盘加载,这取决于配置),GPU 显存几乎全部归还给资源池。等到需要这个模型的时候,再通过一次唤醒操作把它重新加载回 GPU。
这个模式在单机多卡、单卡多模型的场景下非常好用。例如一张 80G 的卡,白天跑实时在线服务,晚上在线请求少了,就可以把在线模型 Sleeper 掉,把显存让给夜间批量推理任务——不用改任何调度代码,资源却实实在在空出来了。
1.3 一个实际的成本账
我举个自己的实测例子:有一台 4 卡服务器,每张卡 48G。之前固定部署了 4 个不同模型,分别处理不同类型请求,每个模型都要占用一张整卡——因为权重加中间缓冲区稍微多一点,就超出了单卡容量。但其实这 4 个模型的调用频率非常不均衡,三个大模型一天加起来跑不到几百次请求。
后来我把它们改为“共享 2 张卡 + Sleep 模式”的方式:任意时刻最多只有 1 个模型在 GPU 上,其余 3 个都处于休眠状态。结果是显存占用峰值不超过 48G,剩下 2 张卡可以用来跑其他服务。对于低频请求的模型,唤醒带来的 20 到 40 秒延迟完全可以接受。这个方案让我在没有新增硬件的前提下,把 GPU 利用率实际翻了一倍。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Sleep 模式的工作原理:显存是怎么“退”出来的
2.1 shutdown() 与 activate() 到底做了什么
vLLM 的 Python 接口提供了两个核心方法,一个是 shutdown(),一个是 activate()。我画个简化版的过程图,方便理解(实际会复杂一些):
code复制正常推理状态
↓ shutdown()
模型权重写入 CPU 内存或直接弃用
GPU 显存中的 KV Cache、中间激活全部释放
↓ activate()
模型重新加载进 GPU
重新构建 KV Cache 管理器
进入可服务状态
shutdown() 调用时,vLLM 会做这几件事:先把当前未完成的请求处理收尾,然后释放掉显存中缓存的 KV Block,把模型权重转移出 GPU。这里的“转移出”有两种策略,一种是把权重搬到 CPU 内存,等唤醒时用 PCIe 复制回显存,速度相对快一些;另一种是直接释放所有权重,唤醒时重新从磁盘加载,省了 CPU 内存但唤醒时间会明显变长。
activate() 则是反向过程。它会重新分配显存、加载权重、初始化 KV Cache 池,然后让模型重新进入可推理状态。整个过程在代码层面看起来非常简单,但对显存管理器的要求非常高——因为释放和重新分配显存,最容易踩的就是显存碎片问题。
2.2 sleep_level 参数:三档设定分别怎么选
vLLM 在较新的版本里给 shutdown() 增加了 sleep_level 参数,主要分三档:
第一档是 0,表示只释放 KV Cache 和中间激活,权重仍然留在 GPU 显存里。这一档的唤醒速度最快,基本是毫秒级,但省出来的显存有限,适合那种“只是暂时降低显存压力,很快就要恢复服务”的场景。
第二档是 1,也是默认档位,所有权重搬运到 CPU 内存,GPU 显存大部分被归还。唤醒时需要从 CPU 往 GPU 拷贝权重,速度取决于 PCIe 带宽和模型大小。以 13B 模型为例,如果 PCIe 4.0 实测能跑到 10G/s 以上,大约十几到二十几秒可以完成整个唤醒。
第三档是 2,所有权重直接丢弃,唤醒时从磁盘完整加载。这种模式对 CPU 内存几乎没有压力,因为模型不会长期驻留在内存里,但唤醒速度会被磁盘读取速度卡住,通常需要几十秒到上百秒。适合“可能几小时甚至一整天都不会用到”的模型。
这三档的选择本质上是一个成本权衡:降得越深,释放的显存越干净,但唤醒的代价也越大。我自己习惯的安排是:短时间内的低峰期用档 1,跨时段的闲置(比如夜里跑批)用档 2。
2.3 KV Cache 与显存碎片怎么处理
Sleep 模式能不能收回 90% 的显存,一个关键点在于 KV Cache 是否被干净利落地释放。vLLM 使用 PagedAttention 把 KV Cache 切分成小块,按块管理。shutdown() 过程中会把整个 KV Cache 池释放掉,占用的显存会真正归还给 PyTorch 显存分配器。
但这里有一个大家容易忽视的坑:PyTorch 的显存缓存机制。PyTorch 分配显存时,默认会保留一块缓存池,即使你 torch.cuda.empty_cache(),有些显存依然不会立刻归还给操作系统——它只是归还给 PyTorch 内部的缓存池,后续其他进程能复用,但从 nvidia-smi 看到的占用率可能没有立即降下来。如果你的目标是彻底归还给系统、给其他进程用,可能还需要设置环境变量 PYTORCH_CUDA_ALLOC_CONF=release_cached_memory 来加速释放。这块我后面会再详细展开。
显存碎片是另一个问题。反复 Sleep 和唤醒多次之后,显存中可能出现很多不连续的小块,导致唤醒一个 13B 模型时明明“总空闲显存”够,却因为找不出一段连续空间而分配失败。这种情况的应对方法,我放到后面的问题排查章节单独说。
3. 实战操作:从最小示例到定时休眠策略
3.1 环境准备与最小代码示例
先准备环境。我这里用的是 vLLM 0.6 之后的版本,Sleep 模式相关接口已经稳定可用,安装直接:
bash复制pip install vllm
下面的最小示例演示了“加载模型、跑一次推理、shutdown、看看显存变化、再 activate”的完整流程:
python复制from vllm import LLM, SamplingParams
# 初始化模型
llm = LLM(
model="/data/models/Qwen-7B-Chat", # 换成你自己的本地模型路径
gpu_memory_utilization=0.8, # 预留 80% 显存给 vLLM
tensor_parallel_size=2 # 如果单卡不够,用两张卡
)
# 第一次推理
output = llm.generate(["Hello, who are you?"], SamplingParams(max_tokens=64))
print(output[0].outputs[0].text)
# 查看休眠前显存占用(用 nvidia-smi 在另一个终端查看也行)
print("before sleep:", torch.cuda.memory_allocated() / 1024**3, "GB")
# 释放显存,sleep_level=1 表示权重搬回 CPU 内存
llm.shutdown(sleep_level=1)
# 此时从 nvidia-smi 观察显存占用应该已经大幅下降
print("after sleep:", torch.cuda.memory_allocated() / 1024**3, "GB")
# 重新激活
llm.activate()
这段代码本身不复杂,但有几个细节我想强调一下。
第一个是 gpu_memory_utilization 的设置。它控制 vLLM 最多能占用多大比例的显存。Sleep 模式下,这个值不宜设太满,因为唤醒时如果恰好显存被其他进程占了,vLLM 会因为没有足够空间而直接崩溃。我个人建议留出 10% 到 20% 的余量。
第二个是 shutdown() 之后再调用 activate() 不需要重新传 LLM 参数,vLLM 会保存原有的配置。
第三个是注意观察显存变化时,最好同时开两个终端,一个跑程序,一个跑 watch -n 1 nvidia-smi,因为程序内部打印的数字可能和 nvidia-smi 显示不一致——前者反映的是 PyTorch 分配给模型的显存,后者反映的是进程从 GPU 拿到的总量。
3.2 定时休眠:把 Sleep 模式接入日常调度
在实际部署中,我们很少手动去调 shutdown() 和 activate(),更多是做一个定时策略。我这里贴一段我实际用过的调度逻辑简化版:
python复制import schedule
import time
class ModelManager:
def __init__(self):
self.llm = None
self.model_path = "/data/models/Qwen-7B-Chat"
self.last_access = time.time()
def ensure_active(self):
if self.llm is None:
self.llm = LLM(model=self.model_path, gpu_memory_utilization=0.8)
elif not self.llm.is_ready():
self.llm.activate()
self.last_access = time.time()
def maybe_sleep(self, idle_timeout=300):
# 超过 5 分钟没请求就休眠
if time.time() - self.last_access > idle_timeout:
self.llm.shutdown(sleep_level=0)
model_manager = ModelManager()
# 自动调度:每 30 秒检查一次空闲状态
schedule.every(30).seconds.do(lambda: model_manager.maybe_sleep())
这个逻辑的核心是“空闲超时”的思想。线上服务的请求频率通常有明显的波峰波谷,比如早高峰和晚高峰请求密集,其他时段空闲。如果没有自动休眠,空闲时段模型照样把显存全占了——这其实是多模型部署最常见的资源浪费。
在接入这类调度时,我建议把 is_ready() 这个状态检查放在每次请求之前。哪怕它只是返回一个布尔值,也能避免你在模型正在休眠时误发请求,导致请求排队或直接报错。
3.3 配合 FastAPI 提供 HTTP 服务
为了让 Sleep 模式真正对线上服务有用,我们需要把它嵌到 HTTP 服务里。下面是我用 FastAPI 构造的一个精简版服务:
python复制from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class Manager:
def __init__(self):
self.llm = None
self.ready = False
def activate(self):
if self.llm is None:
self.llm = LLM(model="/data/models/Qwen-7B-Chat", gpu_memory_utilization=0.8)
else:
self.llm.activate()
self.ready = True
def deactivate(self):
if self.llm is not None:
self.llm.shutdown(sleep_level=1)
self.ready = False
manager = Manager()
@app.get("/health")
def health():
return {"status": "ok", "model_ready": manager.ready}
@app.post("/generate")
def generate(req: dict):
if not manager.ready:
return {"error": "model is sleeping, wake it up first"}
outputs = manager.llm.generate([req["prompt"]], SamplingParams(max_tokens=128))
return {"text": outputs[0].outputs[0].text}
# 这里是唤醒接口
@app.post("/wake")
def wake():
manager.activate()
return {"status": "activated", "gpu_memory_used": get_gpu_memory_used()}
实际生产环境里,这个服务的细节要多不少:请求要排队、要加鉴权、要对唤醒时间做预估和超时控制等。但核心思路就是这样——把 Sleep 模式封装成一个服务内部的资源管理动作,外部通过接口触发,而不是靠运维手工敲命令。
唤醒期间请求该怎么处理,是实施时要想清楚的一个点。最简单的手段是在 activate() 还没完成时直接返回 503,让调用方稍后重试;也可以用中间层做请求排队,等模型 ready 后再执行。我自己的经验是把唤醒时间设计成可配置的,然后前端的重试超时时间按“最长唤醒时间 + 10 秒”来设定,避免连续重试把服务打爆。
4. 性能开销与调优:唤醒延迟到底值不值
4.1 三种 sleep_level 的唤醒延迟实测
为了量化 Sleep 模式带来的影响,我在同一台机器上对不同档位做了实测。硬件环境是这样的:双路 CPU、64GB 内存、一张 48G 显卡,模型是一个 13B 的对话模型,权重大约 26GB。
测试结果见下表:
| sleep_level | 显存释放量(大约) | 唤醒耗时 | 适用场景 |
|---|---|---|---|
| 0(只释放 KV Cache) | 20% ~ 30% | 1 秒内 | 短暂空闲,马上要恢复 |
| 1(权重搬到 CPU) | 85% ~ 90% | 20 ~ 30 秒 | 中等空闲,按小时计算 |
| 2(权重全部丢弃) | 95% 以上 | 60 ~ 120 秒 | 长时间不使用,按天计算 |
这里有个值得注意的点:档位 0 释放的显存其实主要来自 KV Cache,这在长上下文场景下非常可观。如果你的模型上下文长度是 32K,KV Cache 可能占掉 20G 以上的显存,档位 0 就能把这些全部交还。但如果是短上下文场景,档位 0 的收益就很有限了。
档位 1 和档位 2 之间,唤醒时间差主要来自 PCIe 和磁盘的带宽差。PCIe 4.0 的实际传输速率通常在 10GB/s 到 20GB/s,读一个 26GB 的模型权重只要几秒;但磁盘如果只是普通 SATA SSD,读同样的数据可能要半分钟以上。所以如果你有足够的 CPU 内存,档位 1 通常比档位 2 划算得多。
4.2 什么时候用 Sleep 模式是“值得”的
Sleep 模式本质上是用换的时间换空间。要不要用、用哪一档,取决于你的“时间×请求量”成本结构。我一般会按这个思路判断:
第一,看模型调用频率。如果平均 5 分钟之内就会有一个请求,那各档 Sleep 都意义不大,因为显存释放还没形成收益,模型就要马上被唤醒了。频繁的 shutdown 和 activate 反而会拖累整个服务的稳定性。
第二,看模型权重大小和显存占比。一张 48G 卡,上面跑的模型只占 10G,就算全部释放也只腾出 10G——你还需要考虑剩余模型、KV Cache、中间缓冲,真正能释放的比例并不高。而当一个 13B 或 70B 模型占了大半张卡时,Sleep 模式的收益就非常显眼了。
第三,看唤醒时间的容忍度。离线批量任务、夜间任务、非实时分析场景,唤醒几十秒完全无所谓;在线推理服务如果 SLA 要求在 2 秒内响应,那么唤醒时间超过 2 秒的方案就直接被排除。
4.3 主动预热与被动唤醒的取舍
在服务里采用 Sleep 模式时,另一个需要考虑的点是:在请求来临时被动唤醒,还是在预期请求高峰前主动预热。
被动唤醒的好处是实现简单,和请求路径直接绑定,模型什么时候用到就什么时候激活;缺点也非常明显——第一个请求会直接承担整个唤醒延迟。如果这个延迟是 30 秒,调用方基本会判定为超时。
我目前的做法是设计一个“预热调度模块”。它根据历史请求模式,提前 2 分钟把模型唤醒。也就是说,早高峰 9 点到 11 点,调度模块会在 8:58 就发出 activate 请求,等 9 点用户真正开始打请求时,模型已经完全 ready 了。同时,晚间空闲时段到了 11:30,调度模块再发出 shutdown 请求。
这种“以时间换体验”的方式,在实际线上效果要比单纯“请求来到才唤醒”好得多。如果你没有历史请求数据,也可以用规则代替——比如“每次 shutdown 成功后设置一个计时器,超过 N 分钟模型才会被再次激活”。
5. 常见问题与排查技巧实录
5.1 显存碎片导致 activate 失败
这个问题是我在实际使用中踩过的最大一个坑。
现象是:模型成功 shutdown() 了,nvidia-smi 显示 GPU 的空闲显存足够大,但一调 activate() 就报显存不足,错误信息通常类似 CUDA out of memory 或者 vLLM 内部的 Failed to allocate memory for KV cache pool。
原因是反复的 shutdown / activate 之后,显存被外部进程或是 PyTorch 自己的分配器切成了一堆不连续的小块。虽然总空闲量够,但找不到足够的连续空间给模型权重。
排查和解决办法有几个。第一个是在 shutdown() 之后、activate() 之前,主动调用 torch.cuda.empty_cache() 清理 PyTorch 缓存。第二个是在启动时设置环境变量:
bash复制export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
这个参数会告诉 PyTorch 使用可扩展分段分配策略,能在一定程度上减少碎片化问题。第三个终极方案是干脆重启进程——如果你的服务允许,这是最干净的解决方式,让所有显存重新归零分配。
5.2 多模型并发时显存“抢不过”的问题
另一种常见情况是:你调用了 sleep_level=1,模型确实休眠了,显存也释放得干干净净,但另一个进程里的模型在唤醒时依然分配不到显存。
这个问题的本质在于“释放”和“分配”之间还有一层缓冲。PyTorch 的显存释放到操作系统存在延迟,而另一个进程不会等它。一个可靠的做法是引入“显存锁”——在我的系统里,每个模型在唤醒前会先去一个协调服务申请显存配额,拿到锁之后再执行 activate(),用完释放锁。这样多个模型之间不会互相抢资源。
如果你不想引入这么重的协调机制,也可以用一个更简单的办法:每个模型在唤醒前先检查 nvidia-smi 的真实空闲显存,确认超过自己的需求阈值后再激活。虽然不是百分百严谨,但九成以上的冲突都能提前拦下来。
5.3 休眠期间进程还在占用 CPU 内存
使用 sleep_level=1 时,模型权重会停留在 CPU 内存里。如果你部署了多个 13B 模型,CPU 内存可能被吃掉 80GB 以上。这在 GPU 服务器上往往容易被忽略——因为大家默认大模型吃的是显存,对内存关注不足。
应对方式有两种:一是估算权重占用的内存,在服务器规划时留足余量;二是使用 sleep_level=2,让权重直接丢弃,唤醒时从磁盘读回。第二种方案牺牲的是唤醒速度,换来的却是 CPU 内存的释放。
另外,能否让不同模型的唤醒权重都放 CPU?vLLM 的实现是按进程管理的,多个模型实例共享同一个 CPU 内存池时,需要你额外注意总内存占用。建议在模型休眠后用 ps 和 free -g 观察真实内存消耗,不要只看推理阶段的数值。
5.4 唤醒后请求超时报错
在 HTTP 服务里嵌入 Sleep 模式后,最常见的现象是唤醒期间有请求进来,直接超时或者报 CUDA error: device-side assert triggered。
这两个问题的根源不同。超时是唤醒耗时过长,请求在等待队列里超过了客户端超时时间;而后面的 CUDA error,通常是因为在唤醒过程中有其他线程访问了模型推理接口,触发了不可预期的状态。
我的做法是在模型未就绪时,直接对请求返回 503 + Retry-After 头,让客户端明确感知“模型正在启动”,而不是含糊地进入超时重试循环。同时,activate() 方法必须加互斥锁或异步锁,确保同一时刻只有一个线程能触发唤醒流程。这两步做好,线上服务的稳定性能提升一大截。
6. 补充经验:监控、日志与回归验证
6.1 如何观测“显存回收”的真实效果
上面说了很多代码层面的操作,但真正要判断 Sleep 模式有没有生效,不能只看 .shutdown() 有没有报错。我会在系统里加三个监控指标:一是模型状态,它现在处于 active、sleeping 还是 loading;二是显存变化,nvidia-smi 的占用曲线在每次 sleep 后是否形成明显的回落台阶;三是唤醒耗时,这个指标直接反映磁盘/内存/PCIe 的瓶颈。
我自己写过一个简单的波形记录脚本,每 10 秒采样一次显存占用,然后画出时序图。用这个图可以很直观地看出:Sleep 模式有没有真的腾出空间,唤醒时有没有出现显存毛刺,以及多次休眠唤醒之后显存占用是否有递增趋势——后者往往意味着存在显存泄漏。
说到显存泄漏,我想提一个细节。如果唤醒前后的显存占用逐次抬高,那多半不是 Sleep 模式本身的问题,而是你服务里的其他模块(比如结果缓存、日志处理器、中间张量缓存)没有正确释放。Sleep 模式只会管模型自己的显存,管不了你程序里的其他内存消费。
6.2 上线前建议跑一遍回归脚本
Sleep 模式在代码层面只涉及两个方法,但对整个服务的生命周期影响很大。上线前,我建议至少把下面几个场景回归一遍:
第一个场景是“急速休眠唤醒”:模型刚完成一个请求就被 shutdown,然后立刻 activate,看是否会报错。第二个场景是“带 KV Cache 的休眠”:模型在上下文长度比较大的情况下 shutdown,然后再激活,看 KV Cache 是否完整恢复,是否会发生显存溢出。第三个场景是“多轮循环”:反复执行 10 次休眠再唤醒,监控显存占用是否逐步走高。
这些回归不复杂,但能做到心里有底。Sleep 模式本身是一个高效功能,但它对资源管理行为的影响是全局性的——多花半小时跑回归,远好过线上出问题后再补救。
最后再分享一个小技巧:如果你在容器里使用 vLLM,Sleep 模式对显存的释放效果,需要结合容器运行时是否允许 GPU 显存回收来验证。有些容器环境里,nvidia-smi 看到显存下降得很慢,这不一定是代码没生效,而可能是驱动和运行时的回收策略所致。可以通过操作系统进程顶层观察显卡内存页的回收情况,或者直接观察同一物理机上 GPU 空闲进程能否立刻抢到显存。实测下来,在原生物理机环境运行的效果,通常比嵌套容器环境的效果更直接——如果你的业务允许,优先在物理机或 GPU 直透的容器里跑这种多模型轮换方案。
