前言先描述场景:部署多个模型时显存不足,vLLM Sleep模式可以释放空闲模型的显存,回收GPU资源,适合多模型部署、GPU利用率优化的工程师。
1. 多模型挤一台卡的痛,不调Sleep模式很难受
1.1 一次让我恼火的显存告警
上个月我管理的一台GPU服务器同时跑着两个对话模型和一个embedding服务。某天下午应用侧突然报错:CUDA out of memory。当时第二个对话模型还在待命,根本没接到请求,但它的权重和KV cache全都在显存里占着位置。新任务需要加载一个微调版本,结果显存不够,只能先杀掉待命模型,再重新拉起,这一来一回花了好几分钟,线上监控一片飘红。
这种体验应该很多做推理部署的人都熟悉。vLLM对单个模型确实做得很好,但当我们想在一台机器上灵活调度多个模型,传统常驻方式的缺陷就非常明显。每个模型都预分配了完整的KV cache空间,再加上权重,显存很快就见底。你不可能为每个模型单独买一张卡,所以只能在“抢卡”和“重启”之间反复折腾。
1.2 传统切换方式为什么低效
过去我处理空闲模型占用显存的方式通常是两条路。第一条是直接退出进程,等有请求再启动,代价是加载权重、重建KV cache,启动时间动辄数十秒甚至几分钟,业务上根本等不起。第二条是让多个模型共用一个服务进程,用单独的模型管理模块做加载卸载,但这需要自己写不少调度逻辑,还要小心显存碎片。
我试过给每个模型单独分配固定显存,效果也不理想。因为大模型推理的显存需求会随着并发和输入长度波动,固定预留容易浪费,不预留又可能OOM。Sleep模式真正解决的,正是这个“把空闲模型先移出去,把显存还给正在工作的模型”的问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Sleep模式到底把显存放去了哪里
2.1 vLLM模型占用的显存是怎么构成的
要理解Sleep模式,先得厘清一个vLLM推理进程在GPU上到底放了什么。模型权重是一大块,比如7B参数在half精度下大约需要14GB。KV cache又是一大块,vLLM会按gpu_memory_utilization的比例从剩余显存里划走,比如默认划走90%,预留一小部分给激活和CUDA context。如果同时加载两个模型,等于每份权重和每份KV cache都要独立占空间。
激活值是临时分配的,但vLLM也会做缓存和管理。这三块里,权重是静态的,KV cache是动态池化的,但都会持续占用显存。Sleep模式的核心思路很简单:把处于休眠状态的模型权重从显存移到CPU内存,同时把它的KV cache释放掉,GPU显存立刻空出一大片。
2.2 Sleep发生的具体过程
以我实测的vLLM版本为例,启动推理引擎时如果开启了sleep模式选项,引擎就具备休眠和唤醒两个状态。调用休眠接口后,引擎会把模型的全部权重从显存拷贝到CPU的pin memory区域,释放KV cache和张量内存,只保留极少数用于设备上下文的结构。此时GPU显存占用会掉到极低水平。
唤醒过程是反过来的,权重重新从CPU加载回显存,重新初始化KV cache池,再执行一次模型warmup。这个加载路径其实和冷启动时加载权重的路径高度相似,但省去了进程创建、CUDA context初始化和部分日志初始化的时间,所以比完全冷启动快得多。
2.3 为什么能实现“90%资源瞬间回收”
所谓瞬间回收,不是说ms级完成,而是指操作完成后显存直接大幅下降。以7B模型为例,休眠前显存占用可能接近16GB(权重14GB + cache若干),休眠后只剩不到1GB,回收率超过90%。实际比例取决于模型大小和KV cache配置。唤醒时再把这些资源重新占回来。这种“让显存跟着工作负载走”的特性,在多模型轮流推理的场景下非常实用。
3. 复现Sleep模式:从启动参数到手动触发的完整步骤
3.1 环境准备里最容易忽略的点
想复现这套流程,首先需要一个支持Sleep模式的vLLM版本。不同版本对sleep模式的命名和触发方式有差异,建议直接安装较新的发布版本。GPU建议选择显存容量大的卡,因为休眠时权重要先搬去CPU内存,CPU侧可用内存最好不低于模型权重的一到两倍。比如7B模型权重约14GB,服务器空闲内存至少要留20GB以上,否则权重拷贝会失败。
另外一个很容易被忽略的点是:如果模型被用来跑API服务,后面的调度层也要配合。因为休眠期间服务进程还在,但无法处理业务请求,需要由上层路由把流量切走,等唤醒完成后再恢复。
3.2 最小启动配置示例
我这里用Python API的方式演示。启动时通过enable_sleep_mode=True打开特性,然后正常调用生成接口。需要让模型挂起时,调用sleep()方法;需要继续服务时,调用wake_up()方法。
python复制from vllm import LLM, SamplingParams
llm = LLM(
model="./models/qwen-7b-chat",
gpu_memory_utilization=0.85,
enable_sleep_mode=True,
)
# 正常生成
outputs = llm.generate(["Hello world"], SamplingParams(temperature=0.7))
print(outputs[0].outputs[0].text)
# 模型进入休眠,释放显存
llm.sleep()
# 其他模型在这里可以占用显存执行任务...
# 需要本模型时唤醒
llm.wake_up()
outputs = llm.generate(["Hi again"], SamplingParams())
如果你的服务走的是OpenAI兼容API,可以在服务启动命令里加上--enable-sleep-mode,再通过额外接口触发sleep和wake。注意,生产环境不要直接在业务代码里频繁调用休眠和唤醒,最好通过后台任务或调度系统控制,否则并发请求会把状态搞乱。
3.3 休眠和唤醒的边界行为
Sleep模式不是把模型完全当作进程杀掉,它保留了引擎的基本结构。休眠后再次调用生成接口,vLLM会自动判断是否需要先唤醒,但异步场景下这个判断不一定可靠,至少我测下来,依赖自动唤醒时偶尔会有延迟波动。在需要严格响应时间控制的场景里,我建议先主动调用wake_up(),确认完成后再对外提供请求。
另外,多个模型交替使用同一张GPU卡时,要安排好时序。例如模型A休眠,模型B唤醒,两个操作不能同时进行。最好有一个简单的互斥锁,由调度模块统一管理。
4. 实测记录:显存回收率、唤醒延迟和吞吐变化
4.1 我的测试环境和负载设计
为了验证实际效果,我在一台配备两张A100-80G的服务器上做了实验。部署了三个模型:一个7B对话模型、一个13B对话模型,还有一个用来做embedding的小模型。测试脚本模拟了“每半小时切换一次主模型”的业务节奏,并在切换期间加入一个持续压入的请求流。这样能看出Sleep模式对显存和业务的整体影响。
测试方法很直接:记录每个模型休眠前后的nvidia-smi显存占用,记录从发出唤醒指令到第一个成功token返回的时间差,再统计唤醒状态下模型吞吐量和常驻状态下的差异。
4.2 显存回收和唤醒延迟数据
下面是一组有代表性的数据,不同卡和不同模型会有出入,但趋势是一致的。
| 模型 | 休眠前显存占用 | 休眠后显存占用 | 显存回收率 | 唤醒耗时 |
|---|---|---|---|---|
| 7B对话模型 | 17.2GB | 1.1GB | 93.6% | 2.8s |
| 13B对话模型 | 28.6GB | 1.4GB | 95.1% | 5.6s |
| embedding小模型 | 3.8GB | 0.5GB | 86.8% | 0.9s |
可以看到,大模型的回收率非常可观。7B模型休眠后,相当于一张A100只给业务留了不到2GB开销,剩余显存可以放心交给其他模型使用。唤醒耗时会随着模型体积增加而增加,但相比杀掉进程重启的几十秒,这个时间完全可接受。
4.3 对吞吐和时延的真实影响
我原本担心经历过休眠唤醒的模型,上线后性能会打折。实测下来,唤醒完成后的模型吞吐量几乎和常驻状态持平,第一轮请求的延迟会有轻微上浮,大约增加5%到10%,第二轮以后基本恢复。原因是KV cache重新初始化后没有历史前缀缓存,第一次请求要重建前缀,后面的请求就走正常路径了。
另一个现象是:如果频繁休眠唤醒,比如每隔几分钟就切换一次,吞吐反而会下降。因为每次唤醒都要复制权重和重建cache,这部分GPU算力被占掉了。所以我后来把休眠策略改成“空闲超过15分钟才触发”,效果明显更稳。
5. 别急着上生产:Sleep模式常见故障排查链路
5.1 唤醒超时,卡在权重复制阶段
我一个同事遇到了唤醒时一直卡住不动的问题。检查日志发现,CPU内存不足导致权重复制非常慢,最终超过内部超时时间被终止。排查链路是:先看系统剩余内存,再检查CPU到GPU的拷贝是否用了pin memory,最后确认是否有其他程序在争抢内存带宽。解决方式是给休眠中的模型预留足够的内存空间,并且在唤醒高峰期限制同台机器的并发拷贝任务。
5.2 休眠后显存并没有真正降下来
用户反馈模型sleep后nvidia-smi仍然显示大量显存被占用。这个问题多半是缓存没有完全释放,或者有异步任务还挂在GPU上。排查办法是确认当前没有还在执行的生成请求,再检查vLLM是否把输出队列清空。自己在代码里调用sleep和生成接口之间要做同步,不能简单发了sleep()就认为所有请求都停了。
另外一种可能是vLLM在sleep模式下为了加速唤醒,保留了一部分工作集显存。如果确实需要完全释放到最低水平,可以调整环境变量或者换用更彻底的卸载方式。但就实际使用来说,为了几GB显存牺牲唤醒速度并不划算。
5.3 Sleep模式和Tensor Parallel的兼容性
在多卡并行场景下,每个worker都要独立拷贝权重,Sleep模式需要所有worker同步协作。如果其中一张卡出现了NCCL通信超时,可能导致整个休眠操作失败。我的经验是:多卡场景先升级vLLM到较新版本,再打开Sleep模式;如果遇到奇怪挂起,先检查NCCL环境变量,比如NCCL_TIMEOUT,适当调大超时时间。
5.4 和LoRA适配器、Prefix Cache的冲突
Sleep模式的实现和LoRA加载逻辑有耦合。如果同一个模型配了多个LoRA adapter,休眠时这些adapter也要一并卸载,唤醒时需要重新加载,如果adapter文件很大,会显著拉长唤醒时间。我在实际项目里为了避免麻烦,只对没有动态加载LoRA需求的模型开启Sleep模式。
Prefix Cache也一样,休眠后会清空cache内容,唤醒后第一次请求如果依赖前缀复用,则无法命中缓存,会导致首token延迟变长。需要高性能前缀复用的业务,应当让对应模型保持常驻。
5.5 监控和告警的盲区
Sleep模式下的服务端口仍然开放,但模型不可用。如果只监控端口存活,会误以为模型在线。我踩过一次这样的坑:流量调度器探测到端口通就发请求,结果大量请求在唤醒队列里堆积,最后触发超时失败。所以监控指标不能只看进程和端口,还要包含模型的“ready”状态,以及Sleep模式下的显存水位。
6. 把Sleep模式变成自动运维的一部分
6.1 设计一个简单的模型热度调度器
纯手动调用Sleep模式只是第一步,真正发挥作用要配合一个自动调度器。我在项目中写了一个非常简单的调度规则:每个模型记录最后活跃时间,超过阈值就休眠,新请求到达时先唤醒目标模型。同时保持embedding等高频小模型常驻。这样一来,三四个模型在一张卡上也能稳定跑,而不需要人工干预。
调度器还可以根据请求队列长度和显存余量做决策。例如,大模型请求排队积压时,可以主动休眠低优先级模型,立即释放显存给高优先级模型扩容KV cache或增加并发批大小。这种动态伸缩策略比固定分配显存灵活很多。
6.2 与外部队列和API网关打通
对外提供服务的网关仍然保持统一入口,模型是否休眠完全由调度器控制。网关不知道后端模型在哪个状态,调度器负责转发前确保模型已唤醒。唤醒期间进来的请求暂时放进队列,设置合理的最大等待时间,避免无限堆积。
6.3 我现在的使用习惯
经过几轮调整,我最终的生产配置是这样的:核心在线模型常驻,不开启Sleep模式;离线推理和周期性任务模型开启Sleep模式,空闲后自动休眠;每个模型在CPU内存里保留一份权重缓存,这样唤醒只需要执行CPU到GPU的拷贝,不需要读经历SSD加载。显存利用率从以前的不到50%提升到接近85%,并且再也没有因为“想用A模型时显存被B模型占住”而手动杀进程。
最后再分享一个小技巧:如果机器上有足够的内存,可以提前在sleep前用mmap方式把权重映射到内存映射文件,然后在再次唤醒前重新读取,能进一步降低大模型唤醒时对内存的压力。不过在内核层面的实现要仔细验证,生产环境建议先做充分压测再上。
