冷启动问题
在生产推理部署中,需求会随时间波动,因此推理副本需要弹性伸缩。然而,在 Kubernetes 上冷启动推理工作负载可能需要数分钟。在此期间,GPU 虽已分配却处于空闲状态,既不生成 token,也不处理任何请求。
这种延迟增加了流量高峰期间违反服务等级协议(SLA)的风险,因为系统无法足够快速地扩容以吸收需求的突然增长。
对于单 GPU 的 vLLM(v0.20.0)工作负载,冷启动延迟的构成如下:

图 1. 单 GPU 推理工作节点的冷启动延迟分解
为了显著缩短启动时间,我们推出了 NVIDIA Dynamo Snapshot,这是我们针对 Kubernetes 上 AI 推理工作负载的检查点/恢复方案。在本文中,我们将介绍早期原型背后的设计选择与优化,该原型使单 GPU 工作负载的启动时间接近光速。
这是关于 Dynamo 快速启动系列文章的第一篇。
CRIU 与 cuda-checkpoint
运行中的推理工作节点可被检查点的状态包含两个部分:
- 设备状态(GPU 侧):CUDA 上下文、流、设备内存、虚拟地址映射等。这些对宿主机不可见。为了序列化此状态,我们使用
CUDA 驱动的检查点能力(该能力也通过
cuda-checkpoint
命令行工具暴露)将设备状态转储到拥有每个 CUDA 上下文的进程的 CPU 内存中。- 宿主机状态(CPU 侧):CPU 内存、线程、文件描述符、命名空间等。Linux 内核具备序列化此状态所需的全部簿记信息。我们使用开源工具 CRIU(Checkpoint/Restore in Userspace,用户空间检查点/恢复)来遍历 Linux 内核的簿记信息,并将进程树的状态序列化到磁盘。
这两个工具可以干净地组合起来,实现对完整推理工作节点状态的检查点/恢复。检查点时:
cuda-checkpoint
将所有设备状态转储到 CPU 内存中。- CRIU 将所有宿主机侧进程树状态转储到存储中的一个文件夹。
恢复时(同一节点或不同节点):
- CRIU 根据来自 NFS/SMB 等分布式存储的序列化状态恢复进程树,使我们能够从不同节点获取检查点产物。
cuda-checkpoint
将 CPU 内存中序列化的 GPU 状态恢复到新的 GPU 上。
CRIU 本质上是一种冻结与解冻机制。当进程被恢复时,执行会从检查点时的确切指令处继续,完全感知不到检查点或恢复曾经发生。
因此,检查点之前所需的任何协调(例如使工作负载静默),或恢复之后所需的任何协调(例如重新建立外部状态),都必须通过编排器或工作负载特定的钩子从外部处理。我们将在以下各节中描述这些机制。
Dynamo Snapshot:Kubernetes
在 Kubernetes 中,工作负载在 Pod 内的容器中运行。由于 CRIU 检查点包含对容器可写文件系统层的引用,我们在容器级别进行检查点,以便进程树状态和文件系统一起保存。
我们提供了一个特权 DaemonSet,snapshot-agent
,可通过 Helm chart 安装。每个节点上运行一个代理,处理由 runc
管理的容器的检查点和恢复,而无需修改 runc
本身。
在检查点时,代理会等待工作负载的就绪探针,然后从主机侧调用 cuda-checkpoint
和 CRIU,再将产物写入共享存储。工作负载可能在容器本地(即 overlay 文件系统)创建/删除了文件,代理也会在 CRIU 阶段之后对这些文件进行检查点。
在恢复时,代理会启动一个轻量级占位 Pod,恢复 overlay 文件系统,并将 CRIU/CUDA 检查点恢复到其命名空间中。恢复后的工作进程随后接管执行。
每个代理在其本地节点上独立运行,使得检查点和恢复可以在集群中自然并行化。我们构建这个方案,而不是依赖 runc
中 Kubernetes 原生的检查点/恢复支持,后者也委托给 CRIU。DaemonSet 方法完全可移植,不依赖于云提供商对检查点/恢复功能门控的支持。
它还让我们能更紧密地控制 CRIU 以进行性能调优,并允许检查点产物存放在灵活的存储后端中,而不是嵌入到 OCI 镜像中。

图 2. 使用 Dynamo Snapshot 的 Kubernetes 检查点和恢复生命周期
Dynamo Snapshot:工作负载
Dynamo 推理工作进程分两个阶段启动:
- 引擎初始化:启动配置的推理引擎:初始化通信器、加载权重、预热内核、编译/捕获图等。在此阶段结束时,工作进程已完全预热。它可以服务请求,但尚未被其自身 Pod 之外的任何东西发现。
- 分布式运行时启动:工作进程连接到 Dynamo 控制平面,并向发现后端注册自身,以便路由器和图中的其他部分可以找到它。从此时起,工作进程是“活跃的”——与控制平面有打开的连接,集群中的其他组件知道此工作进程的 Pod 身份。
如果我们天真地实现检查点/恢复,而工作负载不知道它正在被检查点,那么检查点作业的就绪探针将对应于一个已完全初始化并注册到发现平面的分布式运行时,这意味着存在 CRIU 无法捕获的活动 TCP 连接。
解决这个问题的一般模式是静默/恢复钩子:工作负载确保自己处于静默状态,并阻塞在一个外部信号上,该信号在恢复完成时触发。这是一个强大的检查点/恢复抽象,因为:
- 它让工作负载在被检查点之前清理其资源,从而优化最终检查点的大小(并因此缩短恢复时间)。
- 它允许工作负载在恢复后重建无法被检查点的资源。这对于多 GPU 和多节点检查点(计划在未来版本中推出)尤为重要:用于 RPC 的出站 TCP 连接无法以已建立状态被检查点,因为 pod IP 在检查点和恢复之间会发生变化,而 RDMA 注册和 NIC 状态也需要在恢复后重建。
在 Dynamo Snapshot 中,我们通过将就绪探针定义为“可进行检查点”信号文件的存在来实现这些钩子。工作进程在引擎初始化之后、分布式运行时启动之前写入该文件。
此时,工作进程进入一个轮询循环,等待另一个“恢复完成”信号文件,同时快照代理从外部对其进行检查点。检查点可以发生在该轮询循环中的任意指令处。
由于 CRIU 在检查点发生的确切指令处恢复执行,工作进程会直接在轮询循环内部恢复,检测到信号文件,并继续分布式运行时初始化,而无需额外的同步。
优化 #1:KV 缓存取消映射与释放
减少检查点大小的一项优化是在检查点之前释放 KV 缓存内存。在测量了权重、CUDA 图以及其他缓冲区/激活值已分配时的峰值 GPU 内存使用量之后,推理引擎将剩余的 GPU 内存分配为一个大 KV 缓存缓冲区。
然而,由于我们的检查点是在副本尚未处理任何请求之前的静止状态下获取的,因此这个 KV 缓存缓冲区根本不需要被检查点。但我们需要保持这个 KV 缓存的虚拟地址稳定,因为它已被固化到 CUDA 图中。这意味着我们通过 CUDA 虚拟内存管理 API(cuMemCreate
和 cuMemMap)分配 KV 缓存缓冲区;然后,在保持虚拟地址稳定的同时释放底层物理分配,只需调用 cuMemUnmap
和 cuMemRelease
,而不调用 cuMemAddressFree
即可。幸运的是,这一功能在 vLLM(通过 sleep()
和 wake_up()
)以及 SGLang(通过 torch_memory_saver
)中已经原生可用。
取消映射并释放 KV 缓存可将 B200 上 Qwen3-0.6B 的总产物大小从约 190 GiB 减少到约 6 GiB。对于大 KV 缓存大小(即相对于 GPU 大小而言较小的模型权重),收益最为显著。

图 3. KV 缓存取消映射与释放减小了检查点大小
优化 #2:加速 CRIU
此时,恢复时间仍远不能令人满意。而对于更大的模型,恢复时间实际上超过了冷启动时间,使检查点/恢复的整个目的落空。

图 4. 使用上游 CRIU 的恢复时间超过冷启动时间
主要原因是,CRIU 和 cuda-checkpoint
并非以光速(SOL)复制内存。在 Linux 进程中,有两种类型的内存:匿名内存(进程的堆、栈等)和共享内存(进程间共享)。CRIU 负责恢复这两种类型的内存,而两者都成为大型模型的显著瓶颈。在本节中,我们概述了为 CRIU 开发的优化,以显著加快进程内存的恢复速度。
注意: 这些 CRIU 优化尚未作为 Dynamo Snapshot 的一部分发布,将在合并到上游 CRIU 后提供。
注意 #2: 我们基准测试工作负载的覆盖文件系统非常小(<100 MiB),在恢复时间中可以忽略不计,因此省略。
优化 #2.1:并行 memfd 恢复
vLLM 的 sleep()
/wake_up()
路径和 SGLang 的 torch_memory_saver
(我们在静默/恢复钩子中调用)将标记为权重的 GPU 分配移动到固定的 CPU 影子缓冲区中。这是高带宽主机到设备/设备到主机(H2D/D2H)内存拷贝的常见做法。CUDA 将这些分配背后的是共享匿名内存,然后通过 NVIDIA 驱动程序固定。在 Linux 内核内部,这些表现为 memfd:匿名的、由 RAM 支持的文件,可以用 MAP_SHARED
映射。
对于 gpt-oss-120b
,这些缓冲区消耗了超过 120 GiB,分成许多独立的 2 GiB 或更小的缓冲区。上游 CRIU 串行恢复这些缓冲区:它创建一个 shmem 支持的对象,调整其大小,映射它,从检查点镜像读取其内容,然后才继续下一个对象。
我们修改了 CRIU,首先枚举所有唯一的 shmem 支持的对象,然后启动线程池并行恢复它们。每个工作线程独立分配其缓冲区并从检查点读取,允许恢复使用可用的存储带宽和 CPU 并行性,而不是一次处理一个缓冲区。
优化 #2.2:用于匿名内存的 Linux 原生 AIO
在 CRIU 恢复了共享资源(文件、套接字、shmem 对象、memfd 等)之后,它仍然必须填充每个进程的私有内存:堆页、栈、匿名映射和写时复制的私有文件映射。这些页面不共享;它们属于一个进程,需要落在检查点之前的确切虚拟地址上。
在上游 CRIU 中,该填充路径是一个同步的 preadv
循环。恢复器从列表中拉取一个作业,交给 preadv
,然后等待。内核将该单个读取发送到存储设备,设备将字节 DMA 到目标 VMA 页面,然后 preadv
返回。只有这时,恢复器才继续下一个作业。在任何时刻,恰好有一个读取在飞行中,这使得存储设备在请求之间空闲。单个阻塞流无法饱和快速 NVMe 带宽,而在网络附加存储上,每次读取还需要支付往返延迟,然后下一个才能开始。

图 5. 串行 preadv 内存恢复(一次只有一个读取在途) 我们将 preadv
循环替换为 Linux 原生 AIO。CRIU 会提前构建一个读取作业列表。每个作业是一个 iocb
,描述文件偏移量、字节数以及指向目标 VMA 页面的 iovec
。恢复器创建一个 AIO 上下文,该上下文同时持有许多不同的读取事务,允许存储设备在其内部通道上并发执行它们。恢复器创建一个 AIO 上下文,使用 io_submit
提交一批 iocbs
,并保持最多 128 个读取在途的窗口。当完成事件通过 io_getevents
返回时,新的提交会回填窗口,直到所有作业完成。

图 6. Linux 原生 AIO 内存恢复(最多 128 个读取并发在途) #### 直接 I/O 与页缓存
在存储后端支持的情况下,匿名内存和共享内存读取都使用 O_DIRECT
。恢复主要是从检查点文件到目标内存的单遍流,因此将输入页面缓存在内核页缓存中通常是浪费的。如果没有直接 I/O,大型恢复可能会临时用检查点数据填满页缓存,同时还要分配目标 shmem 页面,增加内存压力并驱逐对其他工作负载有用的数据。
更重要的是,Linux 原生 AIO 仅在使用 O_DIRECT
打开的文件上才是真正异步的。在 O_DIRECT
不可用或不可靠的文件系统上,例如某些 NFS 部署,恢复会回退到带顺序预读的缓冲 I/O,这样内核仍然能看到可预测的流式访问模式,但 AIO 带来的收益会显著降低。
结果
在相同的设置下,我们看到 CRIU 恢复时间有了巨大改进,现在从检查点恢复比冷启动推理工作进程要快得多:
| 模型 | 检查点大小 | CRIU(上游) | CRIU(AIO) | CRIU(AIO + 并行 memfd) | 相对上游加速 | SOL |
|---|---|---|---|---|---|---|
| Qwen3-0.6B | 6.2 GiB | 6.8 s | 2.9 s | 2.4 s | 2.8x | 0.95 s |
| Qwen3-8B | 26 GiB | 24 s | 11 s | 4.7 s | 5.1x | 1.8 s |
| gpt-oss-120b | 129 GiB | 119 s | 54 s | 15 s | 7.9x | 11 s |
表 1. 上游 CRIU 与优化恢复路径的恢复时间比较。Linux 原生 AIO 与并行
memfd
显著降低恢复延迟,并在各种模型规模下接近光速(SOL)性能。
图 7. 在 Linux 原生 AIO 和并行
memfd
优化后的 CRIU 恢复时间。此时 CRIU 恢复时间已更接近 SOL,但端到端恢复时间仍主要受制于将大型模型权重从 PVC 依次经过主机内存移动到 GPU 的过程。这一过程本质上是串行的:cuda-checkpoint
在 CRIU 将权重实体化到主机内存之前无法恢复 GPU 内存。由于这些权重在检查点大小中占主导地位,将它们保留在 CRIU 镜像内会对恢复速度形成硬性上限,并阻碍更快、直接到 GPU 的传输通道。
优化 #3:GPU 内存服务
为消除这一瓶颈,我们开发了 GPU 内存服务(GMS)。GMS 使用 CUDA 虚拟内存管理(VMM)API 将大型模型权重与推理工作进程的生命周期解耦,将大部分进程内存卸载到单独的 GMS 工件中。
通过将权重从核心 CRIU 检查点中移除,GMS 允许我们利用不同的内存带宽通道并发执行进程状态恢复和权重恢复,而非串行执行。权重恢复现在还可以使用最快可用的路径,例如 GPUDirect Storage(GDS)或对等 GPU RDMA/NVLink。CRIU 检查点也大幅缩小,仅包含容器进程树的主机侧状态和少量双缓冲杂项缓冲区,而 GMS 权重工件现在持有大部分进程内存,可以更快地恢复。
| 模型 | CRIU 检查点大小(基线) | CRIU 检查点大小(使用 GMS) | GMS 权重工件 |
|---|---|---|---|
| Qwen3-0.6B | 6.2 GiB | 4.3 GiB | 1.2 GiB |
| Qwen3-8B | 26 GiB | 4.8 GiB | 15 GiB |
| gpt-oss-120B | 129 GiB | 6.7 GiB | 74 GiB |
表 2. 将权重解耦到 GMS 后各模型检查点工件大小的细分
即使权重恢复通过 NFS 进行,我们也看到显著的恢复时间加速:

*图 8. 在 NFS 上使用 GMS 权重解耦的端到端恢复时间。恢复时间从共同的恢复触发时间戳开始测量,不包括容器启动时间。*当权重通过另一个独立通道恢复时,解耦方法的优势才真正显现——权重恢复可以并行完成,甚至在 CRIU 恢复完成之前就已完成(假设权重传输机制足够快)。以下是概念验证权重恢复后端的结果,该后端将权重条带化分布在 8 块本地 NVMe SSD 上——恢复过程在 5 秒内完成。最终结果是 gpt-oss-120b 的启动时间减少了 21 倍。

图 9. 使用 GMS 和条带化本地 NVMe SSD 实现 5 秒以内的恢复
注意:GMS 适用于其他用例(如弹性)的完整架构和详细功能将在后续博客文章中描述。## 可用性与路线图
我们现在已有早期证据表明,在 Kubernetes 上为推理工作负载实现快速启动是可行的,我们正在努力稳定实现并扩展对更广泛工作负载的支持。Dynamo Snapshot 将在未来几个月内逐步推出。
目前,实验性版本通过非 GMS 检查点/恢复路径支持单 GPU 的 vLLM 和 SGLang 工作负载。
我们目前正在集成以下功能:
- 具有可插拔后端(GDS、UCX 等)的 GMS 恢复路径,目前受限于待定的 CUDA 驱动程序补丁
- TensorRT-LLM 支持
- 通过 PyTorch、NCCL、NIXL 等的静默/恢复钩子实现多 GPU 和多节点支持。
