基础设施

基于数周常驻 VRAM 数据的本地 GPU 容量规划

规格表告诉你的是显卡的显存,而不是你的容器在静止时实际占用的容量。以下是通过数周的常驻 VRAM 读数为单个本地 GPU 确定大小的过程。

本文由 AI 模型从英文原文翻译而来,措辞可能与原文有出入。 阅读英文原文

当你将一个机器学习推理工作负载从云端迁移到一台本地机器时,第一个问题就是一块 GPU 是否足够。在这里,规格表上的答案是没用的。决定因素是你的容器在空闲时实际占用的内存,以及它在负载下达到的峰值。我通过读取三块云端 GPU 的常驻显存(VRAM)来确定本地 GPU 的大小,这些云端 GPU 已经运行同一个镜像长达数周,而这些数据让选择变得显而易见。

常驻内存是下限,而非规格表

一个在启动时加载模型的推理容器,在作业之间不会释放内存。它会将整个模型集保持为常驻状态,这样下一个请求就无需承受冷加载的开销。因此,决定你下限的数字不是显卡大小,也不是请求期间的峰值,而是容器空闲时稳定的常驻内存占用。

有三样东西构成了这一内存占用。在从磁盘读取任何一个权重之前,每个进程的 CUDA 上下文就会耗费几百 MiB。接下来是模型权重,对于一个为多个模型提供服务的镜像来说,权重是其中的主要部分。然后是分配器:像 PyTorch 这样的框架会缓存从驱动程序获取的大块内存,因此一个被释放的张量会返回到该缓存中,而不是返回给驱动程序。从外部看,容器似乎从不归还内存,因为它确实不会。

这就是为什么常驻集会攀升一段时间然后达到平稳,而不是从启动开始就保持不变。每一个新的请求形状,比如更长的输入或更大的批次,都可能迫使分配器划分一块它尚未持有的内存块,而这块内存会一直保留。在经过数天混合流量的冲击后,常驻集会稳定在进程处理过的所有形状所达到的高水位线附近。一个在启动五分钟后测量的容器尚未遇到那些形状,因此读数会偏低,导致你预估不足。

按进程测量,而非按显卡。设备范围内的已用数据会混入所有其他进程,包括你即将移除的桌面会话。

# Per-process resident VRAM. This is the number that sizes the card.
nvidia-smi --query-compute-apps=pid,used_gpu_memory --format=csv
# Device totals, for the headroom check.
nvidia-smi --query-gpu=memory.total,memory.used,memory.free --format=csv

报告的总量与规格表上的数据并不相符。一张标称 24 GB 的显卡并不会提供 24 GB 的可寻址内存:驱动程序会保留一部分,并且在启用了 ECC 的部件上,纠错位会占用更多空间。两张同属 24 GB 级别的显卡,其报告的总量可能相差一个 GB,而当你的裕量只有几个 GB 时,这一点就至关重要了。

读数显示了什么

云端的三块 GPU 运行着完全相同的镜像,每块都是 23 GB 的显卡。我读取了这三块 GPU 在空闲时推理容器的常驻 VRAM:

GPU 常驻 VRAM(空闲) 备注
A 15,322 MiB 单个长期运行的进程,正常运行时间超过 6 周
B 17,382 MiB 观测到的最高值
C 13,676 MiB 观测到的最低值

在完全相同的镜像上,从 13.7 GB 到 17.4 GB 的差距就是碎片化效应。这三个进程的权重没有任何不同,因此这 3,706 MiB 的全部差距都源于分配器的历史记录:即每个实例处理了哪些形状的请求,以及处理的顺序。最高读数比最低读数高出 27%。

这就是为什么平均值是个陷阱。这三者的平均值为 15,460 MiB,比 GPU B 实际占用的值低了 1.9 GB。在一块有六七 GB 预留空间的显卡上,如果根据平均值来规划容量,那么在第一个请求到达之前,就已经消耗掉了三分之一的余量。

比任何常驻内存数值更重要的是那些没有发生的事情。在数周的生产环境中,基于该常驻集合的峰值推理始终保持在 23 GB 显卡的容量之内:没有发生内存不足(OOM)终止、没有分配器重试风暴、也没有重启。这一个事实就涵盖了真实用户发送的每一种请求形状,包括那些没人会放进基准测试里的长尾请求。

无头启动夺回了桌面环境窃取的资源

这台本地部署的机器是一台工作站,它会启动进入一个完整的桌面环境。在推理容器启动前,显示管理器及其合成器就已经占用了数百 MiB 的 GPU 显存。在一台无人登录的工作站上,这纯粹是浪费,而且浪费的恰恰是你正试图规划容量的资源。

几百 MiB 对于一张 24 GB 的显卡来说,听起来像个舍入误差,而相对于显卡本身,它确实是。但相对于可用空间而言,就不是了。如果容器在最坏情况下占用 17.4 GB,那么你所管理的裕度就更接近 6.6 GB,而登录管理器占用的 500 MiB 则是其百分之八。衡量浪费要看裕度,而不是看总容量。

# Boot to a console, and stop the greeter from claiming VRAM.
sudo systemctl set-default multi-user.target
sudo systemctl disable --now gdm3   # or lightdm, sddm

切换后,GPU 进程减少到只剩下推理容器。要进行验证,不要想当然:重启并确认 compute-apps 查询只列出一个条目。所有测量都应在设备实际运行的状态下进行,因为在桌面环境启动的情况下测得的数值,描述的是一台不存在的机器。

容量决策

本地部署的显卡是 24 GB GPU,比已经证明能适应工作负载的 23 GB 云端显卡大约 1.5 GB。计算过程如下:观测到的最差常驻内存为 17,382 MiB,而云端显卡运行该容器数周,在此基础上还有大约 5.6 GB 的余量,且从未耗尽显存。本地部署的显卡报告的容量多了 1.5 GB,因此同一个镜像在同样的最差情况下,能获得大约 7.1 GB 的余量,而且这台机器上桌面环境已不再占用资源。

这个论据是基于“占优”,而非“预测”。我从未测量过推理峰值,也无需这样做。新显卡严格大于那块已经承受了数周此工作负载、此镜像和真实流量的显卡,这就足够了。任何之前能容纳的峰值现在也能容纳,并且还有 1.5 GB 的富余。

成本的不对称性让决策变得容易。第二块 GPU 不仅仅是一块显卡的价格:它需要一个空闲插槽、电源的余量以及机箱内的散热,而机箱可能这三者都没有,此外还需要拆分假定只有一个设备的服务路径。反之,如果容量不足,则会导致生产停滞。有了“占优”这个论据,这两种风险都无需进行成本评估。一块显卡就足够了,数周的云端数据比我一下午能跑的任何基准测试都更有信心地证明了这一点。

购买前要测量什么

该通用方法适用于任何常驻内存占用大的推理工作负载:

  • 读取一个已经处理了数天真实流量(而非刚启动)的容器中每个进程的常驻显存。取相同实例中的最大值,并将其视为下限而非上限。
  • 确认在生产环境中,常驻内存加上推理峰值内存的总量仍在已知良好的显卡容量范围内。如果你要购买的显卡报告的总量比经过验证的显卡更大,那么容量选择就确定了。
  • 比较报告的 memory.total,而不是市场宣传的容量。同一档次的两张显卡容量可能相差一个 GB。
  • 如果机器是工作站,请在关闭显示管理器的情况下进行测量,这样桌面环境就不会占用你指望的预留空间。
  • 购买容量能轻松覆盖你最坏情况下的常驻内存加峰值内存,并有富余空间的显卡,然后就到此为止。你不需要的 GPU 是机架中最昂贵的闲置硬件。

该方法基于一个假设:工作负载是常驻内存占用大的,并且其内存占用是稳定的。训练会打破这个假设,因为激活值和优化器状态会随着批次大小和序列长度而扩展,因此峰值内存远超闲时基线内存,必须直接根据峰值来确定容量。那些根据每个请求加载和卸载模型的服务从另一方面打破了该假设,它们的常驻内存集很小,而峰值内存才是关键。模型集不断增长的多租户服务也会打破该假设,因为这个季度的最坏情况不是下个季度的最坏情况。对镜像的任何更改也是如此:新模型或框架升级会改变基线内存,使你的测量数据失效。

常见问题

我应该根据平均驻留内存读数还是最大值来确定容量?

根据最大值,并在此之上留出余量。相同实例之间的差异来自分配器历史记录,因此最高的读数属于处理了最广泛请求类型的实例。只有少数几个实例时,你只是对该分布进行了采样,而没有确定其边界。

如果请求类型发生变化,驻留内存基线会移动吗?

会。该基线会跟踪进程处理过的每种分配类型的高水位线,因此,提高最大批处理大小或上下文窗口会在该进程的生命周期内推高该基线。如果在确定容量后更改服务参数,那么你之前的读数所描述的容器就不再是你正在运行的那个了。

我可以通过限制内存来代替购买更大的显卡吗?

部分可以。调整分配器可以减少碎片并限制进程缓存量,从而降低驻留集大小,但它无法缩小权重,并且将块强制返还给驱动程序会以吞吐量换取余量。如果差距是几百 MiB,这种交换通常是值得的。如果差距是几 GB,你就需要一张更大的显卡或一套更小的模型。

如果我在一块 GPU 上运行两个推理容器会怎么样?

它们的驻留集会相加,而不会重叠。每个进程都会占用几百 MiB 的自有 CUDA 上下文,并保留自己的一份权重副本,因此,两个 15 GB 镜像的容器需要 30 GB 而不是 15 GB。只有当每个模型都很小,并且显卡容量远大于它们基线之和时,将多个进程打包到一张卡上才划算。