我付了整个 GPU 的钱,就要用满它:GPU 利用率高级指南
初创公司可获得高达 5 万美元的免费计算额度。立即申请
Use Cases PricingCustomersBlogDocsCompany Log In Sign Up All posts Back
Engineering 2025年2月24日 • 阅读时长20分钟
'我付了整个 GPU 的钱,就要用满它':GPU 利用率高级指南
Charles Frye@charles_irl
GPU 爱好者
GPU 利用率最大化者的典型装束。
图形处理器(Graphics Processing Units,GPUs)是自塑造了 20 世纪 90 年代声音的 FM 合成芯片 以来,最热门的数学协处理器。
像所有协处理器一样,当更灵活的通用硬件(如 x86 中央处理器(CPU))的性能不足时,就会选择它们。GPU 尤其 为解决以下问题而设计:CPU 无法实现所需的数学运算吞吐量(尤其是矩阵乘法)。
但是 GPU 并不便宜:高性能往往意味着高价格。
综合来看,GPU 应用的高价格、对性能的敏感性以及面向吞吐量的特性意味着,大量的工程师和技术领导者发现自己都在关注某种形式的 GPU 利用率 —— “我们花了很多钱,所以最好能充分利用我们所支付的东西”。
在 Modal,我们有自己的 GPU 利用率挑战需要解决,并且我们帮助我们的用户解决他们的问题。我们注意到,术语“GPU 利用率”被在堆栈不同部分解决问题的人用来表示非常不同的含义。因此,我们整理了这篇文章,分享我们思考堆栈中 GPU 利用率的框架,以及我们一路学到的技巧和窍门。
特别是,我们将讨论三个非常不同的、都被称为“GPU 利用率”的内容:
- GPU 分配利用率,即运行应用程序代码的 GPU 所占的比例;
- GPU Kernel 利用率,即应用程序在 GPU 上运行代码的时间比例;以及
- Model FLOP/s 利用率,即应用程序使用 GPU 的理论算术带宽来运行模型的比例。
我们将特别关注神经网络推理工作负载 —— 神经网络是因为它们是目前产生最大需求的工作负载,而推理是因为与训练不同,推理是收入中心而不是成本中心。我们押注于收入中心。
什么是利用率?
利用率 = 实现的输出 ÷ 支付的容量
利用率 将系统的可用容量与该系统的输出相关联。
在像 GPU 应用这样面向吞吐量的系统中,支付的容量通常是 带宽 (例如,算术带宽),而实现的输出则是 吞吐量 (例如,每秒浮点运算次数,FLOP/s)。
因为它是一个比率,所以利用率是无量纲的。这意味着 实际上有很多与 GPU 相关的量你可以称之为“GPU 利用率”,省略了容量和输出的隐式单位。这些不同的量跨越了多个数量级的时间,并跨越了不同的组织能力(例如,采购、DevOps 和底层性能工程)。
什么是 GPU 分配利用率?
GPU 分配利用率 = 运行应用程序代码的 GPU 秒数 ÷ 支付的 GPU 秒数
首先,考虑您已分配的 GPU 数量 —— 无论是位于您地下室(或数据中心)的本地固定 GPU 容量,还是云数据中心(或许多人的地下室)中租用的容量 —— 都在一段时间内。
我们使用术语 GPU 分配利用率 来表示在这些 GPU 秒数中,您运行应用程序代码所占的比例。这是“GPU 利用率”的最高级别概念。
GPU 分配利用率有两个关键限制:经济限制和开发者运营限制。
GPU 分配利用率的经济限制源于技术和市场限制的结合。购买、调试、停用和销售 GPU 的速度无法与应用程序需求的输出变化速度(以秒或分钟为单位)相提并论。
当然,对于其他硬件,我们很幸运地拥有高度虚拟化的数据中心平台(“云”),我们可以在其中虚拟地分配和取消分配 GPU 容量。即便如此,现有的定价模型和超过供应的需求使得提供商可以指定条款,例如为期数月或数年的承诺,这限制了给定服务质量的可实现利用率。
对于固定的、过度配置的 GPU 分配,利用率很低
(图表:时间序列图,显示了 GPU 需求、已配置的 GPU 数量和 GPU 数量。已配置的 GPU 数量远远高于需求。)
Modal 帮助组织解决这个问题。我们汇总了消费者对 GPU 的需求和提供商对 GPU 的供应,以提高 GPU 分配效率。
但是,GPU 分配利用率不仅仅是关于支付的 GPU 秒数,而是关于花费在运行应用程序代码上的 GPU 秒数。
这就是 DevOps 对 GPU 分配利用率的限制所在。即使在完全流动的 GPU 市场中,从购买或租用 GPU 的时间到 GPU 运行有用的工作之间也存在延迟 —— 配置操作系统、执行运行状况检查、复制应用程序代码等的时间。如果无法在大于该延迟的时间尺度上精确预测未来的需求,这将导致 GPU 分配利用率降低,服务质量降低,或者两者兼而有之!
如果分配速度慢,利用率和服务质量会受到影响
(图表:时间序列图,显示了 GPU 需求、已配置的 GPU 数量和 GPU 数量。已配置的 GPU 数量滞后于需求。)
为了实现高 GPU 分配利用率并满足服务质量目标,分配和启动到应用程序代码的速度需要足够快,以响应需求的增长。
通过快速、自动的分配,利用率和服务质量都可以很高
(图表:时间序列图,显示了 GPU 需求、已配置的 GPU 数量和 GPU 数量。已配置的 GPU 数量与需求紧密匹配。)
这是 Modal 解决的核心问题之一。我们管理着一个大型的多云 GPU 集群,受益于规模经济来解锁更好的工程解决方案,并集中度量以提高需求的可预测性。我们 构建了一个自定义容器堆栈(顺便说一句,是用 Rust 编写的),以减少来自非应用程序代码和系统配置的延迟。用户的 workload 启动速度更快,因为该容器执行系统的 serverless 运行时以应用程序代码而不是虚拟机维护来构建用户 workload。这使我们可以跳过创建虚拟机所需的重复的、无差别的的工作。这为我们解锁了新的工程优化,例如 内存快照和恢复,并且恰好使我们用户的应用程序工程变得更加容易。
我期望达到什么级别的 GPU 分配利用率?
现有的数字令人警醒。根据 2024 年人工智能基础设施规模状态报告,大多数组织在 达到峰值需求时 实现的 GPU 分配利用率不到 70% —— 更不用说总利用率了。即使是像前 Banana serverless GPU 平台 这样的老练玩家也是如此,其总利用率约为 20%。
使用 Modal,用户可以实现超过 90% 的 GPU 分配利用率 —— 是总利用率,而不仅仅是峰值利用率。
如果这引起了您的兴趣,请查看我们的 文档 和我们的 定价页面。
如果它没有引起您的兴趣,请继续阅读,了解充分利用 GPU 所需的软件工程 —— 在 Modal 或其他地方。
什么是 GPU Kernel 利用率?
GPU Kernel 利用率 = 运行 kernel 的 GPU 秒数 ÷ 支付的 GPU 秒数
仅仅因为已分配的 GPU 正在运行应用程序代码,并不意味着它正在 在 GPU 上 运行代码。在流行的 CUDA 编程模型中,“在 GPU 上运行的代码”的术语是“kernel”,因此我们将花费在 GPU 上运行代码的时间比例称为 GPU Kernel 利用率。
此利用率指标由其他工具报告,其中包括受人喜爱的 nvidia-smi
命令行工具,该工具包装了 NVIDIA 的管理库,因此通常会对其进行检查和引用。我们以该库使用的名称 向我们的用户公开它,“GPU utilization”。请注意,此名称可能略有误导,因为此指标不在乎我们在 GPU 上运行的代码是否正在发挥硬件的实际容量。
如果应用程序的 GPU 分配利用率较低,那么只要您考虑所有正在支付的 GPU 秒数,它必然会实现较低的 GPU Kernel 利用率:一个不运行应用程序代码的单元就无法运行 kernel。
为什么您还可能实现较低的 GPU Kernel 利用率?特别是,哪些模式会显示为每个 GPU 的 kernel 利用率较低?
首先,可能有大量工作支持您的应用程序但不使用 GPU,例如通过网络或磁盘移动输入或输出数据,下载基础模型的许多 GB 权重,或写入日志。
可以通过通常的方法来加速这些任务 —— 明智地应用延迟加载和立即加载、并行化、增加非 GPU 组件(如网络)的带宽,以及删除更多代码 YAGN。
其次,CPU 可能没有足够快地向 GPU 提供工作。典型的 GPU 加速程序就像高性能网络应用程序一样,是 CPU 执行关于必须完成什么工作的逻辑与可以实际完成工作的专用但愚蠢的硬件之间的并发舞蹈。例如,当乘以两个矩阵时,流行的 PyTorch 库需要确定这两个矩阵的形状和类型,然后查找适当的 kernel —— 有点类似于 JIT 数据库查询优化器在执行过程中选择物理运算符。如果您无法在 GPU 完成其先前任务之前完成此工作,则 GPU 将空闲。我们将此类问题称为“host overhead”。
通常,解决 host overhead 是重新编写 host 逻辑的问题 —— 防止缓慢的 host 工作(例如在 Python 中记录日志)阻止驱动 GPU 的 host 工作。但是在每个任务步骤的毫秒级规模上,Python 开始无法跟上,而在每个任务步骤的微秒级规模上,通过 CUDA C++ API 和驱动程序 将 kernel 调度到 GPU 上所需的延迟开始成为瓶颈。
在这两种情况下,都有两个基本的优化。首先,可以使用 CUDA Graphs 一次启动多个 kernel,这实际上将一系列 kernel 启动转换为只需要启动一次的 DAG。其次,应用程序可以为 GPU 聚合更多工作以完成给定单元的 host 工作 —— 例如通过 batching 请求 —— 以提高利用率,但可能会降低延迟。
可以从应用程序跟踪中识别出 GPU Kernel 利用率较低的代码区域,例如 PyTorch Profiler 生成的跟踪。具体来说,所有 CUDA 流为空的任何时间段都是 GPU Kernel 利用率为零的时间段,因此 GPU Kernel 利用率较低的应用程序在其跟踪中具有很大程度上空的 CUDA 流,如下所示。需要将这些静止期与 host 上的活动相关联,以确定应用程序代码的哪些部分导致了瓶颈。GPU 应用程序分析器和跟踪查看器通常支持这一点,例如通过显示 kernel 启动依赖项,如下面跟踪中的箭头所示。
在 GPU 应用程序的跟踪中,没有 kernel 运行的时间段在 CUDA 流的时间线中显示为空条(例如,上面跟踪中的流 7 7)。有关详细信息,请参阅 我们的文档。
我希望达到什么级别的 GPU Kernel 利用率?
GPU Kernel 利用率是本文中与更广为人知的 CPU 利用率最接近的指标。CPU 利用率跟踪 CPU 周期中代表您的程序执行指令所占的比例(而不是 CPU 空闲或运行其他程序)。
但是,对于 CPU 利用率,达到 90% 以上通常是不好的,甚至是触发警报的原因。但我们希望并且可以达到该级别的 GPU Kernel 利用率!
从根本上讲,这是许多 GPU 应用程序的可预测性更高的下游。如果查询模式或数量发生变化,以 90% CPU 利用率基线运行事务数据库副本会降低服务质量的风险。典型的 GPU 应用程序的差异要小得多 —— 对于数据库类似物,想象一下仅重复运行一个基本顺序扫描聚合查询,但每次都使用略有不同的参数 —— 因此具有更可控的服务质量。
什么是 Model FLOP/s 利用率 (MFU)?
Model FLOP/s 利用率 = 实现的 Model FLOP/s 吞吐量 ÷ 支付的 FLOP/s 带宽
在某种 galaxy-brained、CEO-math 级别上,在 GPU 上的支出实际上是在浮点运算带宽上的支出,因此要测量的最深层和最根本的利用率指标是该带宽与实现的吞吐量的比率。
此指标称为 MFU,根据您询问的对象,它表示“Maximum”或“Model”FLOP/s 利用率。我们选择“Model”,因为它更常见。
未运行应用程序代码或未运行 GPU kernel 的实例无法实现高 MFU,因此较低的 GPU 分配利用率或较低的 GPU Kernel 利用率意味着较低的 Model FLOP/s 利用率。
但是,在这些更抽象的级别上的高利用率并不意味着高 MFU。
首先,作为一个实现细节,GPU 之间的通信经常通过 GPU kernel 实现。这种通信,就像分布式系统中的大多数通信一样,容易出现故障(硬件故障、程序员故障、鲨鱼攻击故障),这些故障通常表现为死锁。从 GPU Kernel 利用率的角度来看,一个死锁在运行通信 kernel 中间的系统已得到充分利用(!),但它没有完成任何有用的工作。我们喜欢通过监控 GPU 功耗和热量 来捕获这个特殊问题。更一般地说,优化通信对于实现高 MFU 至关重要,特别是对于将单个任务分散在多个节点上的工作负载。
其次,浮点计算只是 GPU 完成任务必须做的一件事。最重要的其他任务是移动数据。计算只能在存储在 GPU 的 流式多处理器 的 寄存器文件 中的数据上进行,每个寄存器文件存储小于 1MB 的数据,而基础模型以 GB 为单位进行测量。应用计算的数据通常必须从 内存层次结构 中较慢、较大的区域移动。这种 内存的带宽 通常比设备的 FLOP/s 带宽低很多倍,尤其是在最近几代中。算法的 FLOP/s 吞吐量与其 byte/s 吞吐量的比率称为算术强度。
在延迟敏感型基础模型推理工作负载中,内存瓶颈是一个特殊的挑战,在这些工作负载中,算术强度较低(可能每个字节几个 FLOP)。除了算法重写以增加算术强度(例如 Flash Attention 中的在线 softmax)之外,主要的通用策略是 batching 更多工作,这会增加执行的 FLOPs,而不是移动大多数神经网络推理工作负载的内存字节,但通常会增加每个任务的延迟。
最后,必须仔细编写 GPU kernel 才能实现高 MFU。Si Boehm 的这个公开工作日志 概述了达到单个 kernel 的最新技术水平所需的工作。即使该工作日志也没有真正最大限度地提高 MFU,因为它解决了一个无法利用当代 GPU 中最快元素(即 Tensor Cores)的问题,而编写可以饱和 Tensor Cores 的 kernel 甚至更具挑战性 —— 请参阅 Pranjal Shankhdhar 的这个工作日志。因此,大多数团队通过 CuBLAS 等库或 PyTorch 和 vLLM 等框架使用高质量的开源 kernel。
可以使用 NVIDIA 数据中心 GPU 管理工具 dcgm
监控 GPU 应用程序实现的 FLOP/s 和内存吞吐量。通常,带有 DCGM_FI_PROF
前缀的指标是相关的。特别是,DCGM_FI_PROF_DRAM_ACTIVE
指标衡量 DRAM 到 SRAM 内存带宽的利用率。DCGM_FI_PROF_PIPE_TENSOR_ACTIVE
指标衡量提供最大 FLOP/s 带宽的 Tensor Core 的利用率。由于 Stas Bekman 的指南 此处 中很好地介绍了一些微妙的原因,这与 MFU 并不完全相同。
我希望达到什么级别的 Model FLOP/s 利用率?
首先,让我们注意,测量 Model FLOP/s 利用率很棘手。理论带宽可以从制造商的数据表中读取 —— 但请注意“具有稀疏性”等星号。另一方面,实现的模型吞吐量可能难以测量,特别是由于某些 FLOP 可能花费在其他计算上,例如训练中的激活重新计算。因此,通常基于算法的笔头分析和近似的“餐巾”数学来完成。
训练中 MFU 的最新技术是由 OpenAI、Google 和 Meta 等领先组织的基础模型团队实现的。其中,Meta 最开放,并在训练 LLaMA 3 405B 模型 时报告了 38-41% 的 MFU。DeepSeek 运行的较新的 DeepSeek-v3 训练实现了大约 20-30% 的 MFU(没有官方数字)使用具有更严格通信瓶颈的 GPU。
大部分不足是由于大型训练作业中需要节点间通信,这会造成推理应用程序中不存在的带宽限制。对于推理工作负载,MFU 可能会更高,更接近 原始矩阵乘法实现的 70% - 80% MFU,但我们不知道大规模部署的任何已发布结果。如果您错过了它们,请告诉我们!
对于上下文,考虑在 CPU 上运行的作业的 MFU 等效项也很有帮助。具体来说,考虑 One Billion Row Challenge,该挑战导致世界各地的团队竞相优化 CPU 上的大规模聚合问题。这个问题需要对十亿行中的每一行进行三次浮点运算,因此总 FLOP 计数为 30 亿。领先的结果在大约一秒钟内完成,因此实现的 FLOP/s 吞吐量约为 30 亿。如果我们假设用于挑战的硬件,32 核 AMD EPYC 7502P 机器中的八个核心,可以在 3.35 GHz 下运行,能够每个周期发出一个 FLOP,那么 FLOP/s 带宽约为 260 亿,MFU 约为 10%。但是,该 CPU 具有 AVX2 SIMD 向量指令,通道宽度为 256,因此,假设它可以每个核心每个周期发出 16 个 FLOP,则 FLOP/s 带宽实际上约为 4200 亿,导致 MFU 低于 1%。
如何提高 GPU 利用率?
如果您没有使用 Modal,这是一个很好的起点!尤其是对于 GPU 分配利用率。
除此之外,我们建议,如果您想提高 GPU 利用率,请更深入地研究基于 GPU 的计算。
我们编写了 GPU 术语表,将我们对最重要术语的定义收集在一起,并附带了一些我们最喜欢的用于了解更多信息的资源的链接。尝试从那里开始!
在这些资源中,有几个脱颖而出,例如 PyTorch 团队的 Horace He 的演讲 和 Coding Confessions 的 Abhinav Upadhyay 的这篇密集博客文章。我们还强烈推荐 Stas Bekman 的 ML 工程开放书,以获取整个堆栈中的深入分析和有用的代码段。
我们要感谢 PyTorch & GPU_MODE Discord(加入!)的 Mark Saroufim 和 Pig 的 Erik Dunteman 对这篇文章草稿的评论。
在几分钟内发布您的第一个应用程序。
Get Started $30 / 月免费计算
'/%3e%3cpath%20d='M109.623%2064L73.2925%201.07001C72.0925%201.76001%2071.0825%202.76%2070.3625%204L1.0725%20124C-0.3575%20126.48%20-0.3575%20129.52%201.0725%20132L33.4026%20188C34.1126%20189.24%2035.1325%20190.24%2036.3325%20190.93L109.613%2064H109.623Z'%20fill='url(%23paint1_linear_342_139)'/%3e%3cpath%20d='M183.513%2064H109.613L36.3325%20190.93C37.5325%20191.62%2038.9025%20192%2040.3325%20192H104.993C107.853%20192%20110.492%20190.47%20111.922%20188L183.513%2064Z'%20fill='%2309AF58'/%3e%3cpath%20d='M365.963%20132C366.673%20130.76%20367.033%20129.38%20367.033%20128H294.372L258.042%20190.93C259.242%20191.62%20260.612%20192%20262.042%20192H326.703C329.563%20192%20332.202%20190.47%20333.632%20188L365.963%20132Z'%20fill='%2309AF58'/%3e%3cpath%20d='M225.083%200C223.653%200%20222.283%200.380007%20221.083%201.07001L294.362%20128H367.023C367.023%20126.62%20366.663%20125.24%20365.953%20124L296.672%204C295.242%201.53%20292.603%200%20289.743%200H225.073H225.083Z'%20fill='url(%23paint2_linear_342_139)'/%3e%3cpath%20d='M258.033%20190.93L294.362%20128L221.083%201.07001C219.883%201.76001%20218.873%202.76%20218.153%204L183.513%2064L255.103%20188C255.813%20189.24%20256.833%20190.24%20258.033%20190.93Z'%20fill='url(%23paint3_linear_342_139)'/%3e%3cdefs%3e%3clinearGradient%20id='paint0_linear_342_139'%20x1='155.803'%20y1='80'%20x2='101.003'%20y2='-14.93'%20gradientUnits='userSpaceOnUse'%3e%3cstop%20stop-color='%23BFF9B4'/%3e%3cstop%20offset='1'%20stop-color='%2380EE64'/%3e%3c/linearGradient%3e%3clinearGradient%20id='paint1_linear_342_139'%20x1='8.62251'%20y1='174.93'%20x2='100.07