数据科学

在 NVIDIA Dynamo 中借助阴影引擎恢复在数秒内恢复 LLM 推理能力

当 LLM 引擎进程失败时,标准恢复路径涉及冷重启。这需要从存储中将权重加载到 HBM 中,编译内核并捕获 NVIDIA CUDA 计算图。对于大型模型,初始化可能需要几分钟的时间,在此期间,幸存的工作者必须吸收位移的流量。

阴影引擎恢复是 NVIDIA Dynamo 中的预览功能,可将大部分恢复工作从服务路径中移出。它可以使完全初始化的阴影引擎在与活动引擎相同的 GPU 上保持空闲状态。GPU 内存服务 (GMS) 在引擎之间共享现有权重,而无需在 HBM 中创建其他副本。如果活动进程失败,阴影会在几秒钟内出现。重新初始化完全在服务路径的后台进行。

我们通过在两名员工的 GLM-5.2 部署中故意终止一名员工来衡量影响。在没有影子引擎恢复的情况下,剩下的工作人员在长达 283 秒的冷重启期间为所有传入流量提供服务,从而增加了 TTFT,并降低了整个停机期间的每个用户解码率。借助影子引擎恢复,另一名工作人员在 7.3 秒内恢复服务,速度提高了近 39 倍,从而更大限度地减少了对服务质量的干扰。

LLM 推理恢复缓慢的原因:两个核心问题

生产级 LLM 引擎通常会遇到可恢复的软件错误,包括进程崩溃、可恢复的 CUDA 错误和瞬态集合故障。在这些情况下,硬件、驱动程序和节点将保持正常运行状态;只有保持损坏状态的进程会丢失,并且替换引擎通常可以在相同的 GPU 上启动。

那么,为什么这个全新的引擎不能跳过初始化成本呢?目前存在两个问题:

  • 权重与引擎过程相关联。GPU 显存与引擎的 CUDA 上下文相关,而 CUDA 上下文本身与引擎进程相关。当进程退出时,驱动程序会释放所有资源,包括驻留在 GPU 显存中的权重。因此,更换发动机的过程必须重复完整的权重加载过程。
  • 某些初始化状态不可转让。NCCL 和 torch.distributed 通信器与特定正在运行的进程绑定,并且 CUDA 计算图固定在捕获期间出现的虚拟地址上。这些状态无法从之前的引擎中传递,必须在每次重启期间重新创建。

影子引擎恢复通过定向优化解决了每个问题:将权重生命周期与引擎进程解,并在故障发生前完成不可迁移的初始化。

阴影引擎恢复的工作原理

Shadow engine Recovery 将持久的 GPU 显存、预热备用引擎和工作人员级别的协调相结合,无需冷重启即可恢复。

GPU 显存服务:用于 LLM 推理的持久 GPU 显存

GPU 显存服务 (GMS) 可独立于引擎进程管理特定显存区域 (如权重) 。通过使用不同于引擎的流程来拥有这些区域,即使重启引擎,权重也会保留在内存中。因此,同一 GPU 上的新引擎可以连接到现有显存。

GMS 是一种基于 GPU 的边车,代表推理引擎拥有物理 GPU 显存。它基本上处于休眠状态,没有自己的 CUDA 上下文;它会分配物理页面,向它们分发句柄,并仲裁哪些引擎可以随时读取或写入。引擎在自己的 CUDA 上下文中以虚拟地址连接、导入处理和映射底层页面。地图构建只在启动时发生一次,并且 GMS 不涉及任何后续访问。

此功能基于 CUDA 虚拟内存管理 API 构建。借助此 API,物理 GPU 内存及其关联的虚拟地址可以具有独立的生命周期。由于物理分配是参考计数的,只要任何进程保持映射,它们就会继续存在。两个映射相同权重张量的引擎访问相同的物理字节,每个引擎都使用其上下文本地的虚拟地址。读取权重的内核将普通指针解引用到该权重无论如何都会占用的相同 HBM 中,因此 GMS 支持的读取成本不会超过引擎分配的成本。

此架构具有两个优势。首先,权重不会超过引擎故障。虽然内核会删除发生故障的引擎的 CUDA 上下文,但 GMS 参考可确保物理页面保持驻留状态,以便新的引擎可以立即映射它们。其次,权重可以在并发引擎之间共享;因此,同一 GPU 上的二级引擎产生的边际权重成本为零。

将 GMS 集成到推理框架中只需进行有限的更改。vLLM、SGLang 和 NVIDIA TensorRT-LLM 各自通过绑定到权重内存池的自定义 torch.cuda.CUDAPluggableAllocator 集成 GMS。在引擎内部,权重仍然是普通的 torch.Tensor。采用 GMS 就像在启动时翻转标志一样简单。

GMS 不限于权重。当前预览版不支持将 GMS 用于 KV 缓存,但该功能正在积极开发中。目标是使用提升的阴影来映射输出引擎的缓存,而不是在流量到达时重建它。

阴影引擎:预初始化待机模式,零边际权重成本

影子引擎是一个完全初始化的引擎进程,它保持空闲并与活动引擎共同驻留在同一 GPU 上。权重共享使这种配置成为可能:如果没有权重共享,第二个引擎将需要另一个完整的权重副本,从而大大减少可用于处理请求的内存。

阴影运行的启动路径与活动引擎相同。在每个 GPU 上,它连接到本地 GMS 并导入权重映射、建立通信器 (用于工作节点之间 KV 传输的 NCCL 和 NIXL) 、捕获 CUDA 计算图,并执行任何必要的预热。在启动结束时,它就可以开始服务了。然后,它不再服务,而是会停车:它会释放内存中可实现的部分和块,等待轮到它。

阴影在停车之前预先计算的内容:

  • CUDA 上下文、截取的图形和通信程序。当阴影引擎激活时,这些不可转让的组件已准备就绪,因为它们无法从其他进程继承。
  • 权重映射。 GMS 句柄已导入,因此唤醒阴影只需将其重新映射到初始化期间建立的虚拟地址即可。

它延迟的内容:

  • KV 缓存实现。 KV 缓存是引擎拥有的最大可回收分配。阴影在停车时保留其地址范围,而无需物理支持,并在升级时实现缓存。

因此,停车阴影仅保留其 CUDA 上下文、捕获的图形、通信器和权重映射,没有单独的权重副本,也没有 KV 缓存。此占用空间足够小,阴影可以与活动引擎一起保留在同一设备上,从而在几秒钟内恢复。

工作者:单个可部署单元

下图显示了这些组件在每个工作节点中的适配情况,以及在共享路由器后的扩展情况。

这些基础集成到一个 POD 中。工作节点拥有两个引擎容器,一个用于调节 GPU 显存访问的 GMS Sidecar,以及一个用于选择活动引擎的共享锁。

在稳定状态下,一个引擎保持锁定并保持唤醒状态,连接到 GMS,保持物化 KV 缓存,并在前端路由器上注册。另一个仍保持完全初始化并连接到 GMS,但处于休眠状态,没有 KV 缓存,并等待锁定。

深度恢复

以下各节将追踪恢复序列,并解释使其可靠的同步和内存管理机制。

序列

工作节点需要经历四个阶段,然后才能恢复到稳定状态。

  • T₀ 稳健。引擎 A 按住该锁并唤醒,在路由器上注册。引擎 B 处于休眠状态,锁定状态。
  • T₁ 失败。引擎 A 的进程之所以退出,要么是因为它彻底崩溃,要么是因为存活探针发现它挂起并将其杀死。无论采用哪种方式,核函数都会在进程收获时释放其锁。工作线程暂时无法路由,直到阴影注册。
  • T₂ 切换。引擎 B 获取锁定、唤醒、通过 GMS 重新映射权重、实现其 KV 缓存,并通过路由器重新注册。引擎 A 的容器由编排器重新启动。
  • T = 重启。引擎 A 完成初始化并进入阴影状态。系统恢复稳定状态,交换角色。

阴影的优势在于,它进入 T = 已初始化。关键路径上的唯一工作是获取锁、重新映射权重和实现 KV 缓存。

同步

工作者既需要相互排除,确保一次只有一个引擎处于唤醒状态,又需要可靠的释放,以确保备用引擎在处于活动状态的引擎发生故障时接管工作。共享文件上的 POSIX flock 提供了这些保证。当活动进程因关机、segfault 或 SIGKILL 而退出时,内核会获取其文件描述符,阴影引擎则获取锁以开始服务。

因此,每个引擎的启动路径都是短暂的领导者选择:

await engine.initialize()  # weight load, torch.compile, autotune, CUDA graph capture
...
# put the engine to sleep while we wait on the lock
await engine.sleep()
lock = FlockFailoverLock(lock_path)
await lock.acquire(engine_id=engine.id)  # wait on the lock to wake
await engine.wake()

进程仍处于活动状态的死锁引擎落入 Kubernetes liveness 探针,该探针级联至 SIGKILL,并访问同一内核管理版本。

内存会计

在不耗尽 HBM 资源的情况下,在一个 GPU 上安装两个引擎进程需要仔细考虑整个生命周期。

  • 权重。由 GMS 分配一次,并由工作节点中的每个引擎只读映射;永远不会重复。
  • KV 缓存。目前仅由有源引擎持有:在其唤醒时实现,在其死亡时释放,从而释放阴影区域。
  • 缓冲区和图形。NCCL 缓冲区、CUDA 上下文和截取的图形。即使在休眠状态下也由每个引擎支撑,并且停车阴影的全部固定成本。

基准测试结果:GLM-5.2 上的阴影引擎恢复与冷重启

为了量化收益,我们将影子引擎恢复与双工人车队引擎故障后的冷重启进行了比较。

设置

我们在 NVIDIA B200 节点上运行了两个为 GLM-5.2 量化为 NVFP4 的 worker:每个节点一个 worker,TP = 8,最大上下文为 20 万个,以及一个 FP8 KV 缓存。单个前端在二者之间分配循环请求。负载是合成的:每个请求有 32000 个输入令牌和 1000 个输出令牌,达到每秒 0.7 个请求。

双臂运行相同的引擎构造和配置;唯一的区别是影子引擎。基准关闭后,死亡的工人就会冷重启。在 Shadow Engine Recovery 配置中,每个 worker pod 都托管一个预初始化的 shadow 引擎,该引擎可以在活动引擎发生故障时接管。

只有在工作负载达到稳定状态操作点后,我们才会注入故障:向两个工作节点中的一个注入 SIGKILL,然后进行 600 秒的观察。车队中只有两名工人,失去一名工人后,幸存者会处理所有请求,直到伙伴返回。

结果

指标 基准:冷重启 阴影引擎恢复
第二个工作者再次服务所需的时间 283 秒 7.3 秒
TTFT p50,故障发生后 23815 毫秒 1311 毫秒
故障发生后的解码速率 p50 12 tok/s/ 用户 46 tok/s/ 用户
对第一个 token 的请求超过 5 秒 201 个,共 399 个 1 个,共 398 个
请求低于 20 tok/s/ 用户 226 个,共 399 个 0 个,共 398 个
表 1. 冷重启与窗口中两个 worker 之一死亡后的阴影引擎恢复

与基准的冷重启 ( 283 秒) 相比,阴影引擎恢复只需 7.3 秒 ( 1.7 秒用于检测故障,5.6 秒用于推广阴影) 。这显著改善了故障发生后的 TTFT 和解码率,并使我们能够在很大程度上避免违反 SLA 的行为。

当前范围和后续步骤

在验证了在 Kubernetes 上快速恢复推理工作负载是可行的之后,我们正在努力稳定实施,并将支持扩展到更广泛的工作负载。Shadow Engine Recovery 将在未来几个月逐步推出。

当前预览版存在一些限制和部署要求。

  • 影子引擎恢复可解决常见的引擎进程故障,但不涵盖硬件、节点或多节点故障,这些故障仍然依赖于标准重新调度。
  • 它需要在 Kubernetes 上进行动态资源分配 (DRA) ,因此集群需要 Kubernetes 1.34 或更高版本,启用 DRA 并安装 NVIDIA GPU DRA 驱动程序。
  • Dynamo Snapshot 可以与恢复功能结合使用,以在服务时初始化阴影时最大限度地减少争用。
  • 由于提升阴影始于空的 KV 缓存,因此裁剪后的 TTFT 会出现轻微的凹凸。在提升过程中传输缓存状态 (包括前缀缓存索引和缓存内存本身) 是活跃的工作线。
  • vLLM 是受支持的主要后端。

要尝试 Shadow Engine Recovery,请从 Kubernetes 快速入门 开始,创建正在运行的部署。然后,遵循 Shadow Engine Recovery 部署工作流,并使用 vLLM 故障转移示例 作为完整清单。访问 ai-dynamo/ Dynamo 资源库,提出问题、报告问题或做出贡献。

标签