返回 文章 build CMS 文章

NVIDIA Dynamo Snapshot:Kubernetes 推理工作负载的快速启动方案

用检查点/恢复替代冷启动,让 Kubernetes 上的推理副本在数秒内就绪。

NVIDIA DynamoKubernetes推理工作负载冷启动
成长分 / 100 75 综合收获、行动、留存与影响

NVIDIA Dynamo Snapshot:Kubernetes 推理工作负载的快速启动方案
为什么值得读理解推理工作负载在 Kubernetes 上冷启动耗时数分钟的根本原因及其对 SLA 的影响。

学习如何组合 CRIU 与 cuda-checkpoint 对完整推理工作节点状态进行序列化与恢复。

关键洞察
  1. 冷启动延迟由容器/镜像拉取、引擎初始化(权重加载、内核预热、图编译)和分布式运行时启动构成,总计可达数分钟。
  2. CRIU 负责宿主机侧进程树状态,cuda-checkpoint 负责 GPU 设备状态,两者组合可完整检查点/恢复推理工作节点。
  3. 通过 cuMemUnmap 和 cuMemRelease 释放 KV 缓存物理内存但保留虚拟地址,可将 B200 上 Qwen3-0.6B 的检查点从约 190 GiB 降至约 6 GiB。
转成行动

深入阅读

正文与原文对照

原文保真覆盖:全文原文字符:21605

冷启动问题

在生产环境的推理部署中,需求会随时间波动,要求推理副本能够弹性伸缩。然而,在 Kubernetes 上冷启动推理工作负载可能需要数分钟。在此期间,GPU 已被分配却处于空闲状态,既不生成 token,也不处理请求。

这种延迟会增加流量高峰期间违反服务级别协议(SLA)的风险,因为系统无法足够快地扩容来吸收突然增长的需求。

对于单 GPU 的 vLLM(v0.20.0)工作负载,冷启动延迟的构成如下:

堆叠条形图,展示单 GPU 冷启动的时间构成,包括容器/镜像拉取、引擎初始化(权重加载、内核预热、计算图编译)和分布式运行时启动几个部分,总计数分钟。
图 1. 单 GPU 推理 worker 的冷启动延迟构成

为了大幅缩短启动时间,我们推出了 NVIDIA Dynamo Snapshot,这是我们面向 Kubernetes 上 AI 推理工作负载的检查点/恢复(checkpoint/restore)方案。本文介绍我们早期原型背后的设计选择与优化,该原型在单 GPU 工作负载上实现了接近光速(speed of light)的启动时间。

这是 Dynamo 快速启动系列的第一篇文章。

CRIU 与 cuda-checkpoint

一个正在运行的推理 worker,其可做检查点的状态由两部分组成:

  • 设备状态(GPU 侧):CUDA 上下文、流、设备内存、虚拟地址映射等。这部分对主机不可见。为了序列化这部分状态,我们使用 CUDA 驱动的检查点能力(cuda-checkpoint 命令行工具也暴露了该能力),把设备状态转储到拥有各 CUDA 上下文的进程所在的 CPU 内存中。
  • 主机状态(CPU 侧):CPU 内存、线程、文件描述符、命名空间等。Linux 内核拥有序列化这部分状态所需的全部记录信息。我们使用开源工具 CRIU(Checkpoint/Restore in Userspace,用户态检查点/恢复)遍历 Linux 内核的记录信息,并将进程树的状态序列化到磁盘。

这两个工具可以顺畅地组合,实现对推理 worker 完整状态的检查点与恢复。做检查点时:

  1. cuda-checkpoint 将所有设备状态转储到 CPU 内存中。
  2. CRIU 将所有主机侧的进程树状态转储到存储中的一个文件夹。

恢复时(在同一节点或不同节点上):

  1. CRIU 根据从 NFS/SMB 等分布式存储中读取的序列化状态恢复进程树,这使我们可以从另一个节点获取检查点产物。
  2. 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 检查点恢复到它的命名空间中。随后由恢复出来的 worker 接管执行。

每个代理在各自的本地节点上独立运行,使检查点与恢复可以在集群中自然并行。我们自行构建了这一方案,而没有依赖 runc 中 Kubernetes 原生的检查点/恢复支持,后者同样委托给 CRIU。DaemonSet 方案完全可移植,不依赖云服务商对检查点/恢复功能开关(feature gate)的支持。

它还让我们能更严格地控制 CRIU 以做性能调优,并允许检查点产物存放在灵活的存储后端,而不是嵌入 OCI 镜像中。

示意图,展示 NVIDIA Dynamo Snapshot 在 Kubernetes 上检查点与恢复的端到端生命周期:snapshot-agent DaemonSet 等待工作负载的就绪探针,运行 cuda-checkpoint 和 CRIU 将产物写入共享存储,然后启动占位 pod,并在同一节点或不同节点上恢复 worker 的完整状态。
图 2. Dynamo Snapshot 在 Kubernetes 上的检查点与恢复生命周期

Dynamo Snapshot:工作负载

Dynamo 推理 worker 分两个阶段启动:

  1. 引擎初始化:启动所配置的推理引擎:初始化通信器、加载权重、预热内核、编译/捕获计算图等。到这一阶段结束时,worker 已完全预热。它可以处理请求,但在自己的 pod 之外还无法被任何组件发现。
  2. 分布式运行时启动:worker 连接到 Dynamo 控制平面,并向发现后端注册自己,使路由器和计算图中的其余部分能够找到它。从这一刻起,worker 就处于“在线”状态:它与控制平面之间存在打开的连接,集群中的其他组件也知晓该 worker 的 pod 身份。

如果我们天真地实现检查点/恢复,而工作负载并不知道自己正在被做检查点,那么检查点作业的就绪探针对应的就是一个已完全初始化、已注册到发现平面的分布式运行时,这意味着存在活跃的 TCP 连接,而 CRIU 无法捕获这些连接。

解决这一问题的通用模式是静默/恢复钩子(quiesce/resume hooks):工作负载确保自己处于静默状态,并阻塞等待一个外部信号,该信号在恢复完成时触发。对检查点/恢复而言,这是一个强大的抽象,因为:

  1. 它让工作负载在做检查点之前清理自己的资源,从而优化最终检查点的大小(进而缩短恢复时间)。
  2. 它允许工作负载在恢复后重建无法做检查点的资源。这对多 GPU 和多节点检查点(计划在未来版本中支持)尤其重要:用于 RPC 的出站 TCP 连接无法以已建立的状态做检查点,因为 pod IP 在检查点与恢复之间会改变;RDMA 注册和 NIC 状态也需要在恢复后重建。

在 Dynamo Snapshot 中,我们通过把就绪探针定义为“已准备好做检查点”信号文件是否存在来实现这些钩子。worker 在引擎初始化之后、分布式运行时启动之前写入该文件。

此时,worker 进入一个轮询循环,等待另一个“恢复完成”信号文件,而 snapshot 代理则在外部对它做检查点。检查点可以发生在该轮询循环内的任意一条指令上。

由于 CRIU 会在做检查点的那条指令处恢复执行,worker 会直接在轮询循环内继续运行,检测到信号文件,并继续进行分布式运行时初始化,无需额外的同步。

优化 #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 显存越小),收益越明显。

NVIDIA B200 上 Qwen3-0.6B 的 GPU 显存布局前后对比。取消映射前:检查点约 190 GiB,包含一个大的 KV 缓存缓冲区。执行 cuMemUnmap 和 cuMemRelease 后:检查点约 6 GiB,仅保留权重、CUDA 图和其他缓冲区,KV 缓存的虚拟地址范围得以保留,但物理内存已被释放。
图 3. KV 缓存的取消映射与释放可缩小检查点体积

优化 #2:加速 CRIU

此时,恢复时间仍远远达不到可接受的水平。对于更大的模型,恢复时间甚至超过冷启动,使检查点/恢复失去了全部意义。

条形图,对比三个模型(Qwen3-0.6B、Qwen3-8B、gpt-oss-120b)的冷启动时间与上游 CRIU 恢复时间。对于较大的模型,恢复时间明显超过冷启动时间,说明基于串行 preadv 的内存恢复是瓶颈。
图 4. 使用上游 CRIU 的恢复时间超过冷启动时间

主要原因在于,CRIU 和 cuda-checkpoint 的内存拷贝速度达不到光速(SOL)水平。Linux 进程中有两类内存:匿名内存(进程的堆、栈等)和共享内存(进程之间共享)。CRIU 负责恢复这两类内存,而对于大模型,两者都会成为显著的瓶颈。本节介绍我们为大幅加快进程内存恢复而对 CRIU 做的优化。

注:这些 CRIU 优化目前尚未随 Dynamo Snapshot 发布,待合并进上游 CRIU 后才会提供。

注 2:我们做基准测试的工作负载,其 overlay 文件系统非常小(<100 MiB),在恢复耗时中可以忽略不计,因此未计入。

优化 #2.1:并行 memfd 恢复

vLLM 的 sleep()/wake_up() 路径和 SGLang 的 torch_memory_saver(我们在静默/恢复钩子中调用)会把带权重标记的 GPU 分配移入锁页(pinned)的 CPU 影子缓冲区。这是实现高带宽主机到设备/设备到主机(H2D/D2H)内存拷贝的常见做法。CUDA 以共享匿名内存支撑这些分配,再通过 NVIDIA 驱动将其锁页。在 Linux 内核内部,它们表现为 memfd:由内存支撑的匿名文件,可通过 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 的带宽;而在网络附加存储上,每次读取在下一次开始之前还要付出一次往返延迟。

时间线示意图,展示 CRIU 恢复期间的顺序 preadv 调用。每个读取请求一次只发出一个:恢复器要等上一个读取完成后才提交下一个,使存储设备在请求之间处于空闲,可用带宽未被充分利用。
图 5. 串行 preadv 内存恢复(同一时刻只有一个读取在途)

我们用 Linux 原生 AIO 替换了 preadv 循环。CRIU 预先构建一份读取作业列表。每个作业是一个 iocb,描述文件偏移、字节数以及指向目标 VMA 页面的 iovec。恢复器创建一个 AIO 上下文,它可以同时容纳许多不同的读取事务,使存储设备能在其内部通道上并发执行这些事务。恢复器创建 AIO 上下文,用 io_submit 提交一批 iocbs,并保持最多 128 个读取在途的窗口。随着完成事件通过 io_getevents 返回,新的提交会补满窗口,直到所有作业完成。

时间线示意图,展示基于 AIO 的 CRIU 恢复流水线。恢复器维持一个滑动窗口,最多有 128 个通过 io_submit 提交的并发 iocb 读取请求。随着完成事件经由 io_getevents 到达,新的请求补满窗口,使存储设备在整个恢复过程中保持饱和。
图 6. Linux 原生 AIO 内存恢复(最多 128 个读取并发在途)
Direct I/O 与页缓存

在存储后端支持的情况下,匿名内存和共享内存的读取都使用 O_DIRECT。恢复基本上是从检查点文件到目标内存的一次性流式读取,因此把输入页缓存在内核页缓存中通常是浪费。没有 direct I/O 时,大规模恢复可能会暂时用检查点数据塞满页缓存,同时还要分配目标 shmem 页面,从而加剧内存压力,并挤掉其他工作负载的有用数据。

更重要的是,Linux 原生 AIO 只有在以 O_DIRECT 打开的文件上才是真正异步的。在 O_DIRECT 不可用或不可靠的文件系统上(例如某些 NFS 部署),恢复会退回到带顺序预读的缓冲 I/O,这样内核仍能看到可预测的流式访问模式,但 AIO 带来的收益会大幅降低。

结果

在同样的环境下,我们看到 CRIU 恢复时间有了巨大改善,现在从检查点恢复推理 worker 已明显快于冷启动:

表 1. 上游 CRIU 与优化后恢复路径的恢复时间对比。Linux 原生 AIO 和并行 memfd 恢复显著降低了恢复延迟,在各种模型规模下都接近光速(SOL)水平。

模型 检查点大小 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
条形图,对比 Qwen3-0.6B、Qwen3-8B 和 gpt-oss-120b 的冷启动时间、上游 CRIU 恢复、使用 AIO 的 CRIU,以及使用 AIO 加并行 memfd 恢复的 CRIU。优化后的 CRIU 恢复现在对这三个模型都明显快于冷启动,并接近光速(SOL)。
图 7. 采用 Linux 原生 AIO 和并行 memfd 优化后的 CRIU 恢复时间

此时 CRIU 的恢复时间已经更接近 SOL,但端到端恢复时间仍然主要耗费在把大型模型权重依次从 PVC 经由主机内存搬运到 GPU 上。这个过程本质上是串行的:在 CRIU 将权重还原到主机内存之前,cuda-checkpoint 无法恢复 GPU 显存。由于这些权重占据了检查点的绝大部分体积,把它们留在 CRIU 镜像内就为恢复速度设下了硬性上限,也阻碍了更快的直达 GPU 的传输通道。

优化 #3:GPU 内存服务

为了消除这一瓶颈,我们开发了 GPU Memory Service(GMS)。GMS 利用 CUDA 虚拟内存管理(VMM)API,把大型模型权重与推理 worker 的进程生命周期解耦,将进程内存的大部分卸载到一个独立的 GMS 产物中。

通过把权重从核心 CRIU 检查点中移出,GMS 使我们能够利用不同的内存带宽通道,并发地完成进程状态恢复和权重恢复,而不是串行进行。权重恢复现在还可以使用最快的可用路径,例如 GPUDirect Storage(GDS)或对等 GPU 的 RDMA/NVLink。CRIU 检查点也大幅缩小,只包含容器进程树的主机侧状态和少量双缓冲的杂项缓冲区,而 GMS 权重产物则承载了进程内存的大部分,可以更快地恢复。

表 2. 将权重解耦到 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

即使权重恢复走的是 NFS,我们也看到恢复时间大幅加快:

分组条形图,对比 Qwen3-0.6B、Qwen3-8B 和 gpt-oss-120b 在使用与不使用 GMS 时的恢复时间,权重通过 NFS 恢复。GMS 解耦使 CRIU 进程恢复与权重恢复可以并发进行,与基线的串行方式相比,显著缩短了端到端总恢复时间。
图 8. 通过 NFS 并采用 GMS 权重解耦的端到端恢复时间。恢复时间从统一的恢复触发时间戳开始计算,不含容器启动时间。

当权重通过另一条独立通道恢复时,这种解耦方式才真正大放异彩:权重恢复可以并行完成,甚至在 CRIU 恢复完成之前就已结束(前提是权重传输机制足够快)。下面是一个概念验证权重恢复后端的结果,它把权重条带化分布在 8 块本地 NVMe SSD 上,恢复过程在 5 秒内完成。最终结果是,gpt-oss-120b 的启动时间缩短了 21 倍。

条形图,展示 gpt-oss-120b 使用 GMS 的端到端恢复时间,所用的概念验证后端将模型权重条带化分布在 8 块本地 NVMe SSD 上。权重恢复与 CRIU 进程恢复并行完成,使端到端总启动时间低于 5 秒。 *图 9. 使用 GMS 和条带化本地 NVMe SSD 实现 5 秒内恢复

注:GMS 的完整架构,以及适用于其他场景(如弹性容错)的详细特性,将在后续博文中介绍。*

可用性与路线图

我们现在已有早期证据表明,Kubernetes 上推理工作负载的快速启动是切实可行的,我们正在努力稳定实现,并将支持扩展到更广泛的工作负载。Dynamo Snapshot 将在未来几个月内逐步推出。

目前,实验性版本通过不含 GMS 的检查点/恢复路径,支持单 GPU 的 vLLM 和 SGLang 工作负载。

我们目前正在集成以下功能:

  • 支持可插拔后端(GDS、UCX 等)的 GMS 恢复路径,目前取决于一个待发布的 CUDA 驱动补丁
  • TensorRT-LLM 支持
  • 通过面向 PyTorch、NCCL、NIXL 等的静默/恢复钩子,支持多 GPU 和多节点