GPU 实际是如何工作的
**算术便宜、取数昂贵**这一不对称性解释了GPU的一切设计;性能优化的本质就是**提高每字节工作量**(>300 ops/byte 计算受限,<300 内存受

LLM工程师所需的直觉,无需硬件手册。
读完本文,像量化、投机解码和连续批处理这类技术,应该不再是一串需要死记硬背的技巧。
有一件事几乎让所有刚开始部署语言模型的人都感到困惑。
你租了一块顶级数据中心GPU。规格表上说它每秒能执行接近千万亿次算术运算。你加载了一个700亿参数的模型,开始生成文本。你盯着利用率监控器,读数很高。一切看起来都很正常。
然后你数了数token。你从一块标称千万亿次运算的芯片上,每秒只得到几十个token。几乎所有的算术能力都没派上用场。
没有出故障。你的代码没问题,驱动没问题,买更大的GPU也几乎帮不上忙。你看到的正是关于这类硬件最重要的事实,一旦你清楚地看到它,你读过的绝大多数性能建议就不再是一串技巧,而是变得显而易见。
这篇文章从头构建这种直觉。我们会探讨为什么GPU是现在这种形态,为什么内存和计算始终在竞争,以及到底是什么让这些机器跑得更快。
不需要CUDA知识。我们会提到几款真实芯片,因为具体数字有助于理解,但这里没有任何内容依赖死记硬背,而且在每种情况下,比例关系远比数字本身重要。
前置知识。你应该熟悉神经网络做矩阵乘法的概念,并且知道模型权重是存储在内存中的数字。仅此而已。
开始吧!
一个不对称性几乎解释了一切
从整个设计所依赖的单一事实说起。
做算术是廉价的。而获取用于算术的数字则是昂贵的。
这句话初读时让人感觉不对劲。乘法听起来像是难点,而移动一个数字则显得微不足道。但在现代硬件上,情况恰恰相反,而且差距并不小。
一个有用的比喻是车间。算术单元是工作台,成千上万张,挤在房间中央。数据存放在仓库里,仓库与车间之间仅由一条走廊相连。
工作台消耗材料的速度远快于走廊输送材料的速度。增加更多工作台也无济于事,因为走廊早已成为瓶颈。人们所说的内存带宽,指的就是这条走廊。
你之后遇到的每一项性能优化技巧,都是为了让每次走廊往返能完成更多工作。

为何差距持续扩大
这并非暂时的工程短板,而是一个长期趋势。
在近几代加速器中,算术能力的增长速度是内存带宽的数倍。每一款新芯片都能执行多得多的数学运算,却只能获取略多一些的数据来进行运算。
因此,这种不平衡每一代都在加剧,而非改善。减少数据移动的技术随时间推移愈发宝贵,而原始算术能力对你实际性能表现的预示作用则越来越小。
为何GPU拥有数千个简单单元,而非少数几个聪明单元
现在,让我们看看硬件如何应对这种不对称性。
CPU的设计围绕让一串指令快速完成而展开。它的大部分硅片面积都用于此目的:大型缓存以便数据近在咫尺,预测硬件以猜测下一步操作,重排序逻辑以便在等待时继续工作。
所有这些都服务于单一的工作流,且在芯片面积上代价高昂。
GPU则押注相反的方向,因为它专为一种具有特殊属性的工作负载而设计。在图形处理中,每个像素都在不同数据上运行相同的短程序。在神经网络中,张量的每个元素都接受同样的处理。
当程序在数百万个数据元素间完全一致时,就不再需要为每个元素配备一个控制器。一个控制器就能指挥数千个算术单元,让它们在不同数值上执行同一条指令。
这就是全部诀窍所在。GPU删除了CPU所携带的大部分控制机制,将节省下来的面积用于算术单元。

从原始数量对比来看,差异显著。一台高端服务器CPU能同时推进数百个线程。而数据中心GPU在相似的功耗预算下,每个时钟周期能推进数万个线程。
关键在于,GPU线程是远为弱小的存在。它无法自行其是,单独运行也不快。它只是这台极宽、极简机器中的一个通道。
有一个术语值得学习,因为你会在各处见到它。线程并非逐一调度。硬件以固定32个为一组处理它们,这些线程步调一致地移动,共享一条指令,这32个一组被称为一个warp。这就是全部定义,也是芯片实际工作的单位。
泄漏发生之处
由于一个 warp 共享同一条指令,其中的 32 个线程无法对执行内容产生分歧。
如果你的代码根据数据分支,而 warp 中部分线程走一条路径、其余线程走另一条,硬件会依次执行两条路径。它会关闭不在当前路径上的线程,让它们闲置,不产生任何输出。
该 warp 的耗时等于两条分支耗时之和。
只有当分歧发生在 warp 内部时,你才会付出代价。不同 warp 走不同路径则是免费的。实际上,这意味着在紧密内层循环中依赖数据的分支值得避免,而当你调用库操作而非自行编写时,很少会遇到这种情况。
GPU 并不会让等待变短,而是让等待变得不可见。
我们说过,获取数据很慢。CPU 和 GPU 都面临相同的物理延迟,但处理方式截然相反。
CPU 试图让等待不发生,利用缓存和预测,在数据被请求之前就准备好。
GPU 则接受等待,并安排好在等待期间总有其他事情可做。
机制如下:芯片加载的工作量远超其同时能执行的数量。芯片的单个计算单元可能驻留六十四个 warp,而在某一时钟周期内只有一个能执行。
当正在运行的 warp 向主存请求数据时,它会停顿。调度器不会陪它等待,而是挑选另一个就绪的 warp 来运行。当那个 warp 停顿后,再选第三个。
算术单元始终保持忙碌。每个单独的 warp 大部分时间都在等待,而整个机器从不空闲。

在线程束之间切换几乎不耗费任何成本。在CPU上,切换正在运行的线程意味着保存一个线程的状态并恢复另一个线程的状态,这需要数百个时钟周期。而在GPU上,每个驻留线程束的状态已经存放在芯片上的专用存储中。切换只是指向另一组存储位置,仅需一个时钟周期。
这就是为什么GPU携带了大量快速的片上存储。它并不是为了让单个线程运行得更快,而是为了让成千上万个线程能同时处于半完成状态,随时准备在其数据到达时立刻被拾取。
这就是为什么监控中的利用率数字具有误导性。它通常报告的是芯片上是否有任何工作被调度,而非算术单元是否在执行有用操作。一个因数据饥饿而100%利用的GPU,与一个全速运行的GPU看起来并无二致。
实际应用
如果你从本节中只提取一个操作理念,那就是这个。
GPU需要大量独立工作才能高效运行。给它一个小任务,它会以大致内存系统的速度完成,而芯片大部分处于空闲状态。给它一大批相似的工作,同样的硬件会突然显得飞快。
这不是一个怪癖。这是设计本意如此,也是为什么批量大小会出现在你将来遇到的每一次服务讨论中。
记忆是阶梯,每一级都比上一级慢得多
现在,我们来谈谈决定你几乎全部性能的部分。
数据并非简单地存在于内存中。它位于距离算术单元不同远近的多个层级之一,而最近与最远之间的差异是巨大的。
有四个层级值得了解。
- 每线程存储。 最近且最快的。每个线程在处理数据时,会在这里持有少量值。访问几乎是即时的。
- 片上暂存器。 一小块非常快的内存,附着在每个计算单元上,大小约几百千字节。关键在于,如果你编写底层代码,这一层由你掌控。你决定什么进入其中以及它停留多久。
- 共享缓存。 更大的池子,几十兆字节,由整个芯片共享。由硬件自动管理。你无法选择什么驻留在这里,尽管访问模式会影响它。
- 主内存。 巨大的池子,几十吉字节,你的模型权重和激活值实际存放于此。这就是人们所说的显存。它物理上位于处理芯片外部,访问它是芯片所做的最慢操作。
随着你沿列表向下移动,有两件事会变化。池子变得更大,到达它们的代价也变得更高。
层级 大致大小 谁能看到 到达成本 每线程存储 少量值 单个线程 基本免费 片上暂存器 每块几百KB 一批线程 非常便宜 共享缓存 整个芯片几十MB 每个块 明显更慢 主内存 几十GB 每个块 慢得多

两端之间的差距并非微不足道。读取线程已持有的值,成本几乎可以忽略不计。而访问主内存的成本则高出数百倍。
你的权重位于最底层,这是最大的层级,也是唯一足够大以容纳它们的层级。
两种成本,而硬件只处理其中一种
访问内存的成本体现在两个不同方面,区分这两者,是理解本文其余内容的关键。
第一种成本是等待。你请求数据,需要经过一段时间数据才会到达。
硬件通过上一节所述的机制为你处理这一成本。它同时维持着数千个请求的流动,因此当一个线程束在等待时,其他许多线程束正在被处理。等待是真实存在的,但几乎完全被隐藏了。
第二种成本是路径的宽度。每秒只能移动这么多字节,无论多少请求重叠都无法改变这一点。
这一成本无法被隐藏。它是一个硬性上限,也是芯片将其内存带宽作为头号指标宣传的原因。
这就是为什么本文其余部分的计算都围绕带宽而非等待展开。
这些层级实际所在的位置
阶梯告诉你每一级有多远。了解它们物理上位于何处也很有帮助,因为布局解释了为什么有些层级是共享的,而有些不是。
GPU 并非一个巨大的算术单元池。它被划分为大约一百个独立的单元,每个单元都是一台小型自包含机器。这些单元被称为流式多处理器,通常缩写为 SM,使用其正式名称是值得的,因为你遇到的每个工具和文档都会使用这个名称。
每个 SM 都拥有自己顶层两级的副本。它有自己的寄存器文件供其中的线程使用,有自己的共享内存和 L1 缓存,还有自己的线程束调度器来决定接下来运行哪个线程束。这些都不与相邻的 SM 共享。
工作以线程块的形式到达,线程块是一批线程,被分配给一个 SM,并在那里一直运行到完成。在块内部,线程按照我们之前遇到的线程束来处理。
一个例子能让这种划分变得具体。假设你启动一个包含256个线程的线程块,这是常见选择。硬件将其分为8个由32个线程组成的warp,此后调度器就在这8个warp之间进行切换。
你从不决定这种划分方式。你选择的是批次大小,而按32个线程分组的操作在你之下自动完成。
这就是为什么批次大小几乎总是32的倍数。如果你请求250个线程,你仍然会得到8个warp,但最后一个warp运行时,其32个通道中只有26个在执行任务,其余6个虽然不产生任何结果,却仍然占用调度时间。
允许的最大线程块是1024个线程,这相当于32个warp驻留在一个SM上。
这正是共享内存有用的原因,也是其名称的由来。块内的每个线程都在同一个SM上,因此它们都能读取相同的共享内存,彼此协作几乎不花费任何代价。
在所有SM之下是L2缓存,每个SM都能访问它。再往下,完全离开芯片,穿过内存总线,是承载着你的权重、KV缓存和激活值的HBM。人们所说的全局内存或VRAM,指的就是HBM。

这张图解释了你不断遇到的权衡问题。任何能保留在单个SM内的东西都是快速且私有的。任何需要被整个芯片看到的数据都必须下行到L2或更远,而这段下行旅程才是代价高昂的部分。
这也赋予了四个层级它们的真实名称,这些名称在其他地方随处可见。每线程的存储是寄存器文件。暂存器是共享内存,与L1并列。共享缓存是L2。主内存是HBM,也就是人们常说的全局内存或VRAM。
本文其余部分将使用这些名称。
反直觉之处
在CPU上,存储容量随速度减慢而增大,形成平滑的金字塔结构。寄存器极小,缓存较大,主内存则庞大无比。
而在GPU上,这一形态被扭曲了。整个芯片上所有寄存器文件的总和,大致与L2缓存相当。这非同寻常,却直接源自前文所述。成千上万个未完成的线程需要存放其状态的空间,因此芯片将异常多的面积用于存储这些状态。
简而言之,内存系统并非单一实体,而是一架阶梯,性能优化的关键几乎全在于尽可能长久地将数据保留在上层。
每字节工作量是决定一切的数字
现在,我们已足以精确阐述核心观点。
以你在GPU上运行的任何操作为例,计算两件事:它执行了多少次算术运算,以及为执行这些运算需从主内存中读取多少字节。
将前者除以后者,所得比值即为该操作的每字节工作量,它能在你进行任何测量之前预测出性能表现。
其正式名称为算术强度,你常会见到此用法。核心思想无非是:每次沿通道下行,你能从中提取多少价值。
两端相反的操作
关键问题不在于一次操作读取了多少字节,而在于它读取的每个值被使用了多少次。
将数组中的每个元素乘以二,每个值恰好使用一次。取出,加倍,写回,不再过问。每次取值对应一次操作。
而矩阵乘法恰恰相反。在1024乘1024的乘积中,从第一个矩阵取出的每个值,都要与第二个矩阵中的1024个不同值相乘。一次取值,千次运算。
单次取值带来的效益相差千倍,而这完全源于数据的复用。矩阵乘法是加速器真正擅长的唯一操作,原因就在于此。
盈亏平衡点
每块芯片都有一个阈值,它源自一次除法。
将芯片的峰值运算速率除以其峰值内存带宽,得出的就是两者恰好平衡时每字节所需的工作量。
以当前数据中心GPU运行16位精度为例,这个数值大约是每字节300次运算。
以H100 SXM5为例,这是大多数人租用的主力型号。其规格表显示,密集BF16吞吐量为989 TFLOPS,内存带宽为3.35 TB/s。
两者相除。每秒989万亿次运算除以每秒3.35万亿字节,得出每字节295次运算,这就是300这个数字的由来。
这个单一数字在性能剖析之前就能告诉你很多信息。如果某项操作每获取一字节所做的运算少于295次,那么无论代码写得多么精妙,都无法充分利用整块芯片。
这个阈值也会随硬件变化,而且方向可能出乎你的意料。H200与H100采用相同的计算核心,因此其运算上限不变,仍为989 TFLOPS,但带宽提升至4.8 TB/s,使得阈值降至每字节206次运算。
较低的阈值是件好事。这意味着更多的工作负载能够跨过门槛,变为计算密集型,这也是为什么即使不增加额外运算,仅提升带宽也能加速推理的原因。
低于300时,你受限于内存。增加算术能力不会改变任何情况,因为算术单元早已在等待。
高于300时,你受限于算术。增加带宽不会改变任何情况,因为数据到达的速度已经超过了你的处理能力。
阈值属于硬件。你的工作负载相对于它的位置,则属于你。

这张图的名字叫屋顶线模型。了解这个术语是值得的,因为它是性能工程师讨论这类问题的标准方式,也正是上述提到的双天花板概念。
为什么生成一个token是能想象到的最坏情况
将这个框架应用于语言模型推理,开头的谜团便迎刃而解。
文本生成是逐token进行的。每个新token都需要对模型进行一次完整的前向传播。这次传播会读取模型中的每一个权重,一次。
先从单个权重实际做什么开始。
一个权重位于两个数字之间。它被乘以一个输入值,结果被累加到一个运行总和,该总和成为输出的一部分。
这就是一个权重的全部工作。一次乘法,一次加法,因此每次使用它时进行两次操作。
现在将其放大。一个700亿参数的模型生成一个token大约需要1400亿次操作,因为每个权重都执行它的两次操作。
接下来数一下字节数。在16位精度下,每个权重占用2字节,因此读取所有权重意味着移动140GB数据。

将两者结合。1400亿次操作由140GB的数据流量支撑,即每字节1次操作。
盈亏平衡点大约在300左右。服务单个请求使你大约低于该阈值三百倍。
这就是为什么算术单元会闲置。你的配置没有问题,而是操作本身包含的工作量不足以让它们保持忙碌。
这个数字实际预测的结果
一旦你接受生成速度受限于内存,你的令牌速率就不再神秘,而是变成你几秒钟内就能自己算出来的计算。
一个700亿参数的模型,采用16位精度,大约占用140GB空间。一块高端GPU从主内存读取数据的速度大约为每秒3.3TB。
这样你就有了两个数字和一个除法。140GB的权重,每秒到达3300GB的数据,算下来读取一次权重需要0.042秒。
可以称之为42毫秒。这就是生成一个令牌所需的时间,因为在批大小为1时,读取一次完整的权重恰好对应一个令牌的成本。
现在把它转换成速率。如果每个令牌需要42毫秒,那么一秒钟内可以生成1000除以42个令牌,大约是24个。
所以大约每秒24个令牌,任何软件上的巧妙手段都无法突破这个下限。
这个下限由每个令牌需要读取的字节数除以字节到达的速度决定。其他一切都只是细节。

Prefill 的行为恰恰相反
还有第二个阶段,它落在分界线的另一侧。
当你发送提示时,模型在生成任何内容之前会一次性处理其所有令牌。这个阶段称为 prefill,因为它同时处理许多令牌,每次获取的权重都会在所有令牌上使用。
每字节的工作量会立即攀升。Prefill 通常受计算限制,这就是为什么长提示在感觉上比长输出更昂贵,且方式不同。
同一个模型有两个瓶颈相反的阶段。性能讨论中的许多困惑源于将它们视为一个整体。
你听过的每一项优化都在移动同一个比率
这就是框架的价值所在。一旦你将性能视为一个单一的比率,这些技术就不再像是一堆无关技巧的列表。
你只能改变两件事:增加每次获取所完成的工作,或减少获取的字节数。下面每项技术都在做其中一件事。
批处理增加工作量
同时处理多个请求,你只需读取每个权重一次,然后将其用于所有请求。
十个并发请求意味着每字节的工作量增加十倍,且无需额外内存成本。在服务吞吐量方面,没有其他方法能与之相比,而它唯一的代价是每个用户稍微增加一点延迟。
盈亏平衡的计算值得记住。在 16 位精度下,你需要大约 300 个并发序列才能让生成阶段变为计算受限。低于这个数量,你就有未使用的算术能力,这正是服务系统如此努力保持批次满载的原因。

融合减少字节传输
链式逐元素操作是最清晰的例子。
分别执行三个操作时,每个操作都从主内存读取输入并将输出写回。对于几乎不耗时的算术运算来说,这相当于在走廊里来回六趟。
将它们融合为一个操作后,中间值永远不会离开芯片。一次读取、一次写入,算术运算完全相同。内存流量削减了三分之二。
这就是为什么融合操作的编译器能在充满小型逐元素工作的模型上产生巨大加速,而同样的编译器对纯矩阵乘法却毫无作用的原因。
保持数据就近减少字节传输
共享内存的存在正是为此。
将一块数据加载到共享内存中,执行所有需要它的计算,然后继续前进。你为这次传输付了一次费,却多次利用了它。
FlashAttention 是著名的例子。普通注意力机制会构建一个大型中间矩阵,将其写入主内存,然后再读回。FlashAttention 以分块方式计算注意力,这些块驻留在片上内存中,因此该中间矩阵根本不会触及主内存。
算术运算本质上没有变化。内存流量骤减,运行时间也随之缩短。
量化直接削减字节数
将权重以8位而非16位存储,每个权重的大小减半。
这使每个token需读取的字节数减半,从而每字节的工作量翻倍,生成上限大致翻倍。同一个700亿参数模型从140GB降至70GB,理论下限从每秒约24个token提升至约48个。
然而,代价是精度。较低精度会丢失信息,其损失程度很大程度上取决于模型和量化方法。这是一个真正的工程决策,并非免费所得。
顺序读取的重要性超乎想象
即便你从不编写内核,也值得了解一个访问模式细节。
内存并非一次移动一个值。硬件按固定大小的块获取数据。当线程束中的线程读取相邻地址时,它们的请求落在相同的几个块内,每个获取的字节都被利用,你便获得所支付的带宽。
当它们读取分散地址时,每个线程拉入整个块却只使用其中几个字节。最终可能获取的数据量是实际所需的八倍。
实际后果是,内存布局成为性能决策。沿错误轴读取矩阵,或使用分散相关值的数据结构,可能在不改变算术的情况下损失大部分带宽。

小任务有其自身的瓶颈
有一个瓶颈是比率无法捕捉到的。
向GPU发送工作有固定成本。CPU必须准备并分派每个任务,无论任务大小,这都需要时间。
如果你的模型运行许多小操作,你最终可能会在分派上花费比计算更多的时间。此时,你既不是内存受限也不是计算受限,而是开销受限。
症状是GPU看似空闲,而CPU却在辛勤工作。解决方法是减少操作数量、增大操作规模,或者捕获整个操作序列以便作为一个单元重放。
如何判断你处于哪种情况
这个框架只有在你能定位自己所在位置时才有用,而有一种简单的方法可以做到这一点。
在运行过程中测量两个指标:实际从主内存移动的字节数每秒,以及实际执行的算术操作数每秒。将每个指标与硬件能力进行比较。
三种结果几乎涵盖了所有情况。
- 带宽接近上限,算术远低于上限。 你是内存受限。减少移动的字节数。更积极地批处理、量化、融合、修正布局。增加算术能力将无济于事。
- 算术接近上限,带宽远低于上限。 你是计算受限。这是一个好位置。改进现在来自更低精度、更好的算法或更强大的硬件。
- 两者都远低于上限。 你是开销受限,或者你的工作太小无法填满芯片。查看你启动了多少个独立操作以及每个操作的大小。
关于优先查看何处的说明。对于语言模型服务,在几乎所有实际配置中,生成阶段都是内存受限的。如果你正在优化令牌吞吐量,并且尚未检查批处理大小、精度和KV缓存大小,这三个因素几乎总是会主导你尝试的其他任何优化。
硬件变化时,哪些保持不变
规格参数瞬息万变。了解其中哪些部分需要重新学习,哪些不会变,是值得的。
变化的是数字。内存容量、带宽和算术吞吐量随每一代产品增长,其中算术性能增长最快。新的精度格式不断涌现,一个比一个小,因为缩减字节数是直击瓶颈的最有效手段。
不变的是形态。算术操作的成本仍远低于数据移动。内存层级结构保持其层级。每字节工作量继续决定你触及的是哪个天花板。
如果说有什么变化,那就是这个框架随时间推移愈发有用。由于算术能力增长快于带宽,盈亏平衡比率持续上升。在旧硬件上受计算限制的工作负载,在新硬件上无需改动一行代码就变成受内存限制。
这是值得押注的方向。减少数据移动的技术将持续增值,而规格表上的峰值算术指标将继续成为文档中最不具预测性的数字。
结论
本文中,我们从单一的不对称性出发,构建了对GPU工作原理的直觉认识,即算术运算成本低廉,而数据移动成本高昂。
我们探讨了为何这一不对称性催生了一颗包含数千个简单算术单元、共享一个控制器的芯片,而非少数几个复杂单元。我们了解到,GPU并未缩短数据等待时间,而是通过保持远超其执行能力的工作负载,并在各片段间即时切换,来隐藏等待。
我们沿着内存阶梯下行,从寄存器文件,经共享内存与L2,直至HBM,每一级都比上一级容量更大、访问更难。我们也看到了这些层级的位置,其中最高两层由每个SM私有,其下所有层级或在整个芯片上共享,或完全位于芯片之外。
这引出了核心数字,即每获取一字节所执行的工作量。每颗芯片都有一个盈亏平衡比率,在当前硬件上,以16位精度计算,约为每字节300次操作。低于此比率,你受限于内存;高于此比率,则受限于算术运算。
将此应用于语言模型,解答了开篇的谜题。生成一个令牌需读取每个权重,并对每个权重执行约两次操作,比率接近1,约为盈亏平衡点的三百分之一。预填充阶段则位于该线的另一端,这解释了两个阶段为何表现迥异。
最后,我们看到所有广为人知的优化无非两种策略。批处理与分块增加了每次获取所完成的工作量;融合、量化及更优的内存布局则减少了获取的字节数。
我们有意略去了编写GPU代码的底层细节,因为直觉必须先行。在此框架之前编写内核,往往会产生作者无法解释为何快速、亦无法定位为何缓慢的代码。
未来的文章将在此基础上进一步深入,变得更加具体,包括如何对照盈亏平衡线解读性能分析器输出、KV缓存如何随着上下文增长改变内存格局,以及服务引擎在决定批处理时内部实际发生的过程。
一如既往,目标是培养扎实的系统级视角,并为你提供一套适应性强的工程框架,用于构建稳健且可维护的AI系统。
一如既往,感谢阅读!