跳到正文
Modal Blog· Jonathon Belotti·· 2025-01-28精选AI 评分62

Modal 推出内存快照功能,容器冷启动提速 2.5 倍

Memory snapshots: Checkpoint/restore for sub-second startup

AI 导读

Modal 发布内存快照功能,为无服务器 GPU 容器提供 checkpoint/restore 能力,冷启动速度约为标准容器启动的 2.5 倍。官方数据显示,Stable Diffusion 推理函数从约 13 秒降至 3.5 秒,import torch 示例从约 5 秒降至 p50 约 1.05 秒、p0 约 0.69 秒。

推荐理由

Modal 官方披露内存快照的恢复机制与实测数据,读者可据此判断无服务器冷启动优化的可行路径。

正文 · AI 翻译

Modal 是一个无服务器 GPU 容器运行时,设计上可以从零开始扩展。我们运行我们的 worker 集群,而我们的用户 Functions 则保持精简。如果有闲置容量,我们的目标是削减它。这意味着,如果额外的负载进入某个 Function,我们通常需要启动额外的容器来服务请求。当请求等待容器启动时,就会出现冷启动延迟,而我们的客户讨厌这一点。

我们也讨厌它!幸运的是,随着内存快照恢复的引入,用户 Functions 上的冷启动延迟可以减少一半以上!

什么是内存快照?

Modal 内存快照是几个文件,它们代表了 Linux 容器在即将接受请求之前的整个状态。我们捕获容器的文件系统变更及其整个进程树。该树中的每个进程都有状态,包括其内存映射、文件描述符表、寄存器、环境变量、进程 ID 等等!这是一场派对,每个人都受邀了。

Diagram of process component state that is saved to disk (credit Tristan Hume)

所有捕获的状态允许完整恢复一个 Python 程序,但有一个明显的例外:实时网络连接和 NVIDIA GPU 状态(下文讨论)。

容器快照功能是“用户空间检查点/恢复”(CRIU)内核技术的产物。其要点是,你可以对 Linux 容器进行检查点(保存),并在稍后恢复它,甚至可以在不同的计算机上恢复。这种功能至少从 1999 年的 VMWare Workstation 产品开始就存在于 Linux 虚拟机中,但众所周知,容器直到 2009 年左右才“成为一件事”。CRIU 于 2011 年由一些“疯狂的俄罗斯人”首次提出,作为一种在物理服务器之间实时迁移容器的方法,并随着时间的推移证明了自己是一种便捷的技术,用于减少无服务器冷启动。

CRIU 是为 runc 容器运行时开发的,但出于安全原因,Modal 使用 gVisor 容器运行时 runsc(“run Sandboxed Container”的缩写)。虽然 runc 让容器在主机内核上运行,但 gVisor 实现了一个用户空间内核,并让客户容器访问该内核。这对容器快照有显而易见的影响。虽然 runc 快照解决方案与主机 Linux 内核协作以保存容器状态——内核通过 /proc VFS 暴露有关进程内存映射、打开文件、子进程的详细信息——但 gVisor 控制着为快照容器客户提供服务的用户空间内核。

Timeline showing when Linux, LXC, CRIU, and gVisor were released

因此,虽然 gVisor 的检查点/恢复功能是在 CRIU 之后很多年才开发的,但它实际上很像 CRIU 之前的解决方案,那些方案涉及定制 Linux 内核(即“内核空间中的容器恢复”)。gVisor 的核心 kernel.go 文件包含检查点/恢复代码,并且至少有十八个系统组件在 save_restore.go 文件中实现了检查点/恢复功能。如果你在用户空间重新实现 Linux 内核,你可以进行很多检查点/恢复特定的内核定制!

性能:检查点/恢复 vs. 惰性加载

用 gVisor 的 runsc restore 恢复容器快照为什么会比标准的 runsc run 容器启动快得多,这并不明显。

主要原因是 Python 的导入系统基于文件系统,需要执行数千个缓慢、顺序的文件系统操作,才能准备好执行有用的工作。

之所以是数千次,是因为即使在 Python 中仅仅导入 torch 就会执行 26,000 次系统调用!它慢是因为容器的文件系统与实际文件数据之间存在几层间接访问。Modal 容器的文件系统是一个 OverlayFS 文件系统,其中只读下层是一个基于 FUSE 的懒加载文件服务器,这意味着每次文件读取都会产生一些开销。

最快的代码是从未运行的代码,因此快速的容器启动主要在于懒惰、避免工作。基本上,就是在不需要的地方不做事。Modal 上几乎每个容器在 /usr/share/doc 中都有数千个文件,但我们的用户程序中约等于零的程序实际需要读取这些文件。所以我们不加载它们。

关于我们懒加载容器文件系统的更多细节,请参见 Modal.com 中的快速、懒加载容器。

尽管通过懒加载消除了大量急切的文件 I/O,导入 torch 仍然会执行 26,000 次系统调用,在调用方、内核和 FUSE 服务器之间进行上下文切换。Python 基于文件系统、系统调用繁重的模块加载实在太慢了。因此我们转向检查点/恢复,它将数千次系统调用变成(大致)一次文件加载,直接重建进程的内存映射,而不是重新运行 Python 导入系统以及容器启动期间执行的所有其他应用程序代码。

一次文件加载?

Diagram showing the simplified restore problem, where Python process memory mappings are served from FUSE via gVisor.

容器恢复过程是一个召唤与集体编排的狂热过程,但大部分性能的成败取决于“主”进程的内存映射能以多快的速度被带入操作系统的页缓存。这些内存映射通常为 100MiB-10GiB,并存储在一个快照“pages”文件中,引用虚拟内存系统的 4KiB 页(或大页)。

恢复时,gVisor 无需等待将整个“state”文件读入内存后才允许恢复的容器继续运行。相反,它可以在后台读取被恢复进程的页,优先处理被恢复进程阻塞等待的那些页。

这个后台加载的 pages 文件通过与我们标准容器加载相同的分布式、基于 FUSE 的文件服务系统提供给 gVisor。为了确保 FUSE 系统在 gVisor 请求页时不会让它等待,我们尽早地将整个 pages 文件积极地预加载到页缓存中。在最坏的情况下,恢复进程发生缺页,gVisor 发现 FUSE 文件服务器内存中还没有该页,主机磁盘上也没有。因此,恢复进程被阻塞,等待 FUSE 服务器完成一次网络文件读取,这需要数十毫秒。

只关注 pages 文件的加载忽略了很多东西,但 80/20 法则中的 80 就在这里。gVisor 的优先级后台页加载和我们 FUSE 文件系统的积极预加载相互配合,以最小化恢复中的客户进程的总体缺页延迟。

那么,这一切够快吗?

性能:快 2.5 倍

我们发现内存快照恢复比标准容器启动快约 2.5 倍。一个通常需要约 13 秒的 Stable Diffusion 推理函数恢复仅需 3.5 秒。一个简单的 import torch 示例程序,据指出会执行 26,000 次系统调用,通常冷启动约需 5 秒。使用快照恢复后,p50 约为 1.05 秒,p0 为 0.69 秒!

虽然恢复已经比现状快得多,我们仍可以做得更好。当前的恢复实现受限于主进程虚拟内存(有时达数 GiB 数据)从磁盘(或通过网络)加载到内存的速度。

我们观察到容器恢复在急切加载数十万个 4KiB 页面时产生 CPU 压力。CPU 停顿高达 900/ms/s,这表明我们的容器 cgroup 资源管理需要更好地调优,以应对启动时的激进资源使用。这不像拉着手刹那么糟,但有点像费力地骑死飞上坡。

我们还可以优化将客户机虚拟内存页面预加载到宿主机页面缓存的方式。有时恢复文件通过网络提供,虽然我们期望宿主机达到约 2GiB/s 的网络下载速度,但在尾部我们观察到的有效吞吐量要低得多。

所以仍有工作要做,随着我们进行优化,你应该会看到上面的恢复线进一步左移并变得更平直!

底层原理:快照生命周期

启用了内存快照的已部署函数只会按需创建快照,而非主动创建。此外,单个已部署函数版本会被快照多次,原因很有趣。

当 Modal 的调度器发现它打算将某个函数放置到的 Modal 工作主机上没有可用的现有活动函数快照时,就会按需创建快照。

必须将函数快照与特定工作主机匹配,是单个函数版本会被快照多次的原因之一。例如,AWS g6.12xlarge 实例类型不支持 pclmulqdq Perform a Carry-Less Multiplication of Quadword 指令,因此它无法接受在支持该指令的主机上创建的任何快照。我们的用户可没料到要调试 Invalid Opcode 异常!

除了 CPU 特性集兼容性问题外,内存快照还对 NVIDIA 驱动版本和容器运行时版本的变化敏感。这类兼容性问题是 Modal 代表用户控制快照生命周期的主要原因,以确保在动态且不断演进的工作主机集群中恢复始终成功。

权衡:或者说,真的那么简单吗?

内存快照显著减少了冷启动,但也降低了简单性。Andrew Morton 曾试图警告我们,但我们站到了俄罗斯人一边,有点疯了。我们在容器执行中途将其冻结并序列化到磁盘。

‘用户空间检查点/恢复’往往需要的主要麻烦是客户机程序的配合。程序需要理解它们将被暂停不确定的时间,并在另一台计算机上恢复,可能具有不同的 IP 地址。它们还需要理解,在快照之前建立的状态将被反复重用——对熵的期望可能会被打破。

团队已经努力使 Modal 客户端与检查点/恢复配合。当棘手情况导致恢复失败时(这确实会发生),我们会自动回退到标准容器启动。我们建议在启用内存快照将函数部署到生产环境之前,先在生产环境之外进行测试。

有关管理棘手问题的更多信息,请参阅指南页面。

接口:采用内存快照加速

那么,你如何使用它呢?

Modal Functions 可以使用 enable_memory_snapshot=True 参数开启快照,而 生命周期方法 可以通过 snap=True 参数选择启用快照。

这是一个仅导入 torch 的“hello world”示例。

上述 Modal Function f 在部署并在生产环境中运行若干次后,将被创建快照。(我们目前是按需创建快照,而非主动创建。)

该快照捕获了 import torch 全局导入以及在全局作用域中执行的任何其他内容,例如 rootfs 磁盘变更。它本质上是在即将获取请求并将该请求送入 f 之前暂停并保存。

这使我们能够在恢复时直接跳回 f(x),如下所示。

Diagram showing the snapshot restore process

每当 Modal 检测到重新部署使 f 的现有快照失效时,就会创建新的快照。

处理 GPU 状态

上文提到生命周期函数可以被快照,这对于管理 GPU 状态很重要。由于内存快照尚不支持将 GPU 状态保存到文件,GPU 状态必须在恢复后创建。

上述 GPT-2 推理 Function 有一个 snap=True 生命周期方法,用于在 CPU RAM 中设置模型以进行快照,还有一个 snap=False 生命周期方法,用于在恢复后立即从 CPU RAM 移动到 GPU vRAM。

这个演示推理 Function 使用内存快照后恢复速度提升了 2.5 倍 🏎️。

致谢

非常感谢 gVisor 团队 创建了 gVisor 及其 Checkpoint/Restore 功能。还要感谢 Luis Capelo、Colin Weld 和 Matt Nappo 在内存快照相关方面的工作和设计讨论。

如果你有兴趣为云构建快速、可靠且重负载的系统,Modal 正在招聘。

来源:Modal Blog · modal.com