
GPU 实际是如何工作的
GPU算术极廉价而数据搬运极昂贵,这一不对称性决定了LLM推理的瓶颈:生成token受限于内存带宽,所有优化只在移动同一比值。
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 一生中大部分时间都在等待,而整台机器从不空闲。

在线程束之间切换几乎不花任何代价。在 CPU 上,切换正在运行的线程意味着保存一个线程的状态并恢复另一个线程的状态,这需要数百个时钟周期。在 GPU 上,每个驻留线程束的状态早已停放在芯片上的专用存储中。切换只是指向另一组存储。只需一个时钟节拍。
这就是 GPU 携带大量快速片上存储的原因。它不是为了加快任何单个线程的速度。它的存在是为了让数千个线程能够同时处于半完成状态,随时准备在数据到达的那一刻被拾起。
👉 这就是你的监控中利用率数字具有误导性的原因。它通常报告的是芯片上是否调度了任何工作,而不是算术单元是否在做有用的事。一个“利用率”为 100% 却在等待数据的 GPU,看起来和一个全速运行的 GPU 一模一样。
实用版本
如果你要从这一节中带走一个可操作的理念,那就是这个。
GPU 需要大量独立的工作才能运行良好。给它一个小任务,它会以大致相当于内存系统的速度完成该任务,而芯片的大部分处于空闲状态。给它一大批类似的工作,同样的硬件突然就显得很快。
这不是什么怪癖。这是设计按预期运作的结果,也正是为什么批量大小会出现在你今后每一次关于服务部署的讨论中。
内存是一架梯子,每一级都比上一级慢得多
接下来是决定你几乎全部性能的部分。
数据并非简单地“存在于内存中”。它存在于与算术单元相距若干距离的某一层级上,而最近与最远之间的差距极其巨大。
有四层值得了解。
- 每线程存储。 最近也最快。每个线程在处理若干数值时把它们放在这里。访问实际上瞬间完成。
- 片上暂存区。 附着在每个计算单元上的一小池极快内存,大小几百 KB。关键在于,如果你写底层代码,这一层由你掌控。你决定放什么进去、放多久。
- 共享缓存。 更大的一池,几十 MB,由整块芯片共享。由硬件自动管理。你无法选择什么留在这里,尽管访问模式会影响它。
- 主存。 最大的一池,几十 GB,你的模型权重和激活值实际所在之处。这就是人们所说的 VRAM。它物理上位于处理裸片之外,访问它是芯片所做的最慢的事,且差距悬殊。
沿着这份列表往下走,有两件事在变。池子变得更大,而抵达它们变得更难。
层级 大致多大 谁能看到 访问代价 每线程存储 若干数值 单个线程 基本免费 片上暂存区 每块几百 KB 一批线程 非常便宜 共享缓存 几十 MB,整块芯片 每个块 明显更慢 主存 几十 GB 每个块 慢得多,差距悬殊

两端之间的差距并不小。读取线程已持有的一个数值,代价小到不值得测量。访问主存的代价则高出数百倍。
你的权重位于最底层,那是最大的一层,也是唯一大到足以容纳它们的一层。
两种代价,而硬件只处理其中一种
访问内存会让你付出两种彼此独立的代价,而把这两者区分开来,正是让本文余下部分说得通的关键。
第一种代价是等待。你请求数据,然后过一段时间它才到达。
硬件会替你处理这一种,用的正是上一节讲过的机制。它让成千上万个请求同时推进,所以当一个 warp 在等待时,还有许多其他 warp 正在被服务。等待是真实存在的,但它几乎被完全隐藏了。
第二种代价是通路的宽度。每秒能移动的字节数是有限的,再多的请求重叠也改变不了这一点。
没有任何东西能隐藏这一种。它是一道硬性上限,也正是芯片会把内存带宽当作头条数字来宣传的原因。
这就是为什么本文余下部分的每一次计算,讨论的都是带宽,而不是等待。
这些层级实际位于何处
这个阶梯告诉你每一层有多远。同时知道它们在物理上位于何处也很有帮助,因为这种布局解释了为什么其中一些是共享的,而另一些不是。
GPU 并不是一大池算术单元。它被划分为大约一百个彼此独立的单元,每一个都是一台小型自成一体的机器。这些单元被称为流式多处理器,几乎总是写作 SM,而使用这个正式名称是值得的,因为你遇到的每一个工具和文档都会用它。
每个 SM 都带有自己的一份最上面两层的副本。它有自己的寄存器文件,供住在其中的线程使用;有自己的共享内存和 L1 缓存;还有自己的 warp 调度器来决定下一个运行哪个 warp。这些都不与隔壁的 SM 共享。
工作以线程块的形式到达,线程块就是交给某一个 SM 的一批线程,它们会一直待在那里直到完成。在线程块内部,这些线程按我们之前见过的 warp 来处理。
举个例子可以把这种划分讲得更具体。假设你启动一个包含 256 个线程的线程块,这是一个常见的选择。硬件会把它划分成 8 个 warp,每个 warp 32 个线程,从那时起,调度器就在这 8 个 warp 之间进行选择。
你从来不会去选择这种划分方式。你选择的是批次的大小,而按 32 分组这件事是在你之下发生的。
这就是为什么批次大小几乎总是 32 的倍数。申请 250 个线程,你仍然会得到 8 个 warp,但最后一个 warp 只有 32 条通道中的 26 条在做事情,其余 6 条什么也不产出,却仍然要轮到它们。
允许的最大线程块是 1024 个线程,算下来就是 32 个 warp 驻留在一个 SM 上。
这就是共享内存有用的原因,也是它名字的由来。块中的每个线程都在同一个 SM 上,所以它们都能读取同一块共享内存,彼此协作的成本几乎为零。
在所有 SM 之下是 L2 缓存,每个 SM 都能访问它。再往下,完全离开芯片、跨过内存总线,是 HBM,存放着你的权重、KV 缓存和激活值。人们说全局内存或 VRAM 时,指的就是 HBM。

这张图解释了你不断遇到的权衡。凡是能留在一个 SM 内部的东西,都快且私有。凡是必须被整个芯片看到的东西,都必须向下走到 L2 或更远,而这段下行路程才是昂贵的部分。
这也给出了这四个层级真正的名字,也就是你在其他地方到处都会看到的叫法。每线程存储是寄存器文件。暂存区是共享内存,与 L1 并列。共享缓存是 L2。主内存是 HBM,它和人们所说的全局内存或 VRAM 是同一个东西。
本文的剩余部分就使用这些名字。
反直觉之处
在 CPU 上,存储层级越大就越慢,构成一个平滑的金字塔。寄存器很小,缓存更大,主存则极其庞大。
而在 GPU 上,这个形状被扭曲了。整个芯片上所有寄存器文件加起来,大小大致与 L2 相当。这很不寻常,而它直接源自上一节的结论。成千上万个半途而废的线程需要有个地方安放它们的状态,于是芯片把异常大的面积用于存放这些状态。
总而言之,内存系统并不是单一的东西。它是一架梯子,而性能优化几乎完全就是让数据尽可能久地待在上面几层。
每字节工作量才是决定一切的数字
现在我们有足够的铺垫来精确陈述核心思想了。
拿你在 GPU 上运行的任何操作来说。数两样东西:它执行了多少次算术运算,以及为了完成这些运算它必须从主存中拉取多少字节。
用第一个数除以第二个数。这个比值就是该操作的每字节工作量,它能在你进行任何测量之前就预测出你的性能。
它的正式名称是算术强度,你会看到人们就是这么用的。这个想法说白了就是:你每跑一趟走廊能榨取出多少价值。
两个方向相反的操作
有用的问题不是一次操作读取了多少字节,而是它每读取一个值用了多少次。
将数组的每个元素乘以二,每个值恰好只用一次。取出来,翻倍,写回去,再也不看它一眼。每取一个值做一次操作。
矩阵乘法恰恰相反。在一个 1024 乘 1024 的乘积中,你从第一个矩阵取出的每个值都要与第二个矩阵中的 1024 个不同值相乘。一次取用,一千次操作。
这就是一次取用能换来的东西相差一千倍,而它仅仅来自复用。矩阵乘法正是加速器真正擅长的操作,原因就在于此。
盈亏平衡比
每块芯片都有一个阈值,而它来自一次除法。
取芯片的峰值算术速率,除以它的峰值内存带宽。这就得到了两者恰好平衡时的工作量每字节比。
对于当前运行 16 位精度的数据中心 GPU,这个数字大约是每字节 300 次操作。
以 H100 SXM5 为例,它是大多数人租用的主力机型。其数据手册给出 989 TFLOPS 的密集 BF16 吞吐量和 3.35 TB/s 的内存带宽。
两者相除。每秒 989 万亿次操作除以每秒 3.35 万亿字节,得到每字节 295 次操作,这就是 300 这个数字的由来。
在你做任何性能分析之前,这一个数字就能告诉你很多。一个每取用字节做少于 295 次操作的操作,无论写得多么好,都无法用满整块芯片。
这个阈值也会随硬件变化,而且方向可能出乎你的意料。H200 使用与 H100 相同的计算裸片,所以它的算术上限不变,仍为 989 TFLOPS,但带宽升至 4.8 TB/s。这使阈值降至每字节 206 次操作。
更低的阈值是好事。这意味着更多工作负载能跨过门槛,进入算力受限区间,这正是为什么只升级带宽、不增加算力,推理依然会变快。
低于 300 时,你受限于内存。增加算力毫无用处,因为算力单元本来就在空等。
高于 300 时,你受限于算力。增加带宽毫无用处,因为数据到达的速度已经超过你消费它的速度。
阈值属于硬件。你的工作负载落在阈值的哪一侧,则取决于你。

👉 这张图的名字叫 roofline 模型。这个术语值得记住,因为它是性能工程师讨论这类问题的标准框架,而且它正是上文描述的那套“两重天花板”思路。
为什么生成一个 token 是最糟糕的情况
把这套框架套用到语言模型推理上,开头的谜题就迎刃而解了。
生成文本是一个 token 一个 token 进行的。每生成一个新 token,都需要对模型做一次完整的前向传播。这一次传播会把模型里的每一个权重都读取一遍。
先从单个权重实际在做什么说起。
一个权重夹在两个数之间。它被一个传入的值相乘,结果再累加进一个总和里,最终成为输出的一部分。
这就是一个权重的全部工作。一次乘法,一次加法,所以它每被用到一次就是两次运算。
现在把这个规模放大。一个 700 亿参数的模型生成一个 token 要做大约 1400 亿次运算,因为每个权重都要完成它那两次。
接下来数字节。在 16 位精度下,每个权重占 2 字节,所以把所有权重读一遍意味着搬运 140 GB。

把两者放在一起。1400 亿次运算,靠 140 GB 的数据流量来支撑,也就是每字节 1 次运算。
盈亏平衡阈值大约在 300。服务单个请求时,你大约比它低三百倍。
这就是算术单元闲置的原因。你的配置没有任何问题。只是这个运算本身包含的工作量不足以让它们吃饱。
这个数字实际预测了什么
一旦你接受生成受限于内存,你的 token 速率就不再神秘,而变成一个你几秒钟就能自己算出来的计算。
一个 700 亿参数的模型以 16 位精度存储,占用约 140 GB。一块高端 GPU 从主内存读取数据的带宽约为每秒 3.3 TB。
这样你就有了两个数字和一次除法。140 GB 的权重,每秒到达 3,300 GB,算出来就是读取一遍权重需要 0.042 秒。
也就是 42 毫秒。这就是生成一个 token 的时间,因为在批大小为 1 时,一个 token 的开销恰好就是完整读取一遍权重。
现在把它换算成速率。如果每个 token 需要 42 毫秒,那么一秒内就有 1,000 除以 42 个 token,大约是 24。
所以大约是每秒 24 个 token,再多的软件优化也无法突破这个下限。
这个下限由每个 token 必须读取的字节数,除以字节到达的速度决定。其余都是细节。

预填充的行为恰好相反
还有第二个阶段,它落在分界线的另一侧。
当你发送一个提示词时,模型会在生成任何内容之前一次性处理它的所有 token。这个阶段被称为预填充(prefill),而由于它同时处理许多 token,每一次取回的权重都会在所有这些 token 上被复用。
每字节的工作量立刻攀升。预填充通常是计算受限的,这就是为什么长提示词带来的昂贵感与长输出不同。
同一个模型拥有两个阶段,其瓶颈恰好相反。性能讨论中的许多困惑,都源于把它们当成同一回事。
你听说过的每一种优化,移动的都是同一个比值
这就是这个框架发挥作用的地方。一旦你把性能看作一个单一的比值,这些技术就不再像是一堆互不相关的技巧了。
你只能改变两件事:增加每次取回所完成的工作量,或者减少取回的字节数。下面每一种技术做的都是其中之一。
批处理增加了工作量
同时处理多个请求,你只需读取每个权重一次,然后把它用于所有请求。
十个并发请求意味着每字节的工作量变为十倍,且不增加额外内存开销。对于服务吞吐量而言,你能做的其他任何事情都无法与之相比,而它唯一的代价是每个单独用户会多一点点延迟。
这个盈亏平衡的数学值得记住。在 16 位精度下,你需要大约 300 个并发序列,生成才会变成计算受限。低于这个数,你就有闲置的算术能力未被利用,这正是服务系统如此努力保持批次满载的原因。

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

小任务有自己的下限
有一个瓶颈是比率无法捕捉的。
把工作发送到 GPU 有固定成本。CPU 必须为每个任务做准备和调度,无论任务是大是小,这都要花时间。
如果你的模型运行许多小操作,你最终花在调度上的时间可能比计算还多。此时你既不是内存受限,也不是计算受限。你是开销受限。
症状是 GPU 看起来空闲,而 CPU 却在拼命工作。解决办法是减少操作数量并增大每个操作,或者捕获一整串操作,使其能作为一个单元重放。
如何判断你处于哪种情况
只有当你能够在这套框架中定位自己时,它才有用,而有一个简单的方法可以做到这一点。
在一次运行中测量两件事。你实际从主内存中每秒移动了多少字节,以及你实际每秒执行了多少算术运算。把每一项与硬件的能力进行比较。
三种结果几乎涵盖了一切。
- 带宽接近上限,算术远低于上限。 你是内存受限。减少移动的字节数。加大批处理、量化、融合、修正布局。增加算术能力不会有任何作用。
- 算术接近上限,带宽远低于上限。 你是计算受限。这是一个好位置。现在的改进来自更低的精度、更好的算法或更强的硬件。
- 两者都远低于各自的上限。 你是开销受限,或者你的工作太小,填不满芯片。看看你启动了多少个独立操作,以及每个操作有多大。
👉 关于先看哪里的说明。对于语言模型服务,在几乎所有现实配置中,生成都是内存受限的。如果你正在优化 token 吞吐量,却还没有检查批大小、精度和 KV 缓存大小,那么这三者几乎总会压过你尝试的其他任何东西。
硬件更迭中什么始终不变
规格更新得很快。值得弄清楚的是,这其中哪些部分你需要重新学习,哪些不需要。
变化的是数字。内存容量、带宽和算术吞吐量每一代都在增长,其中算术吞吐量增长最快。新的精度格式不断涌现,一个比一个小,因为缩减字节是对瓶颈最直接的攻击。
不变的是形态。算术依然远比数据搬运便宜。内存阶梯依然保持它的层级。每字节工作量依然决定着你会撞上哪一堵天花板。
如果说有什么变化,那就是这个框架随着时间推移会变得更有用。因为算术能力的增长快于带宽,盈亏平衡比持续上升。在旧硬件上受计算约束的工作负载,在新硬件上会变成受内存约束,而一行代码都不用改。
这就是值得押注的方向。减少数据搬运的技术会不断增值,而规格表上的峰值算术数字,将始终是整份文档中最缺乏预测力的那个数字。
结论
在本文中,我们从单一的不对称性出发,建立了对 GPU 工作原理的直觉理解,即算术运算成本低廉,而数据搬运成本高昂。
我们看到了为什么这种不对称性催生出一块芯片上集成数千个共享一个控制器的简单算术单元,而不是少数几个复杂的单元。我们看到了 GPU 并不缩短等待数据的时间,而是通过加载远超其执行能力的工作量,并在不同任务片段之间瞬时切换,来隐藏等待。
我们沿着内存阶梯一路走来,从寄存器文件经过共享内存和 L2 一直到 HBM,每一层都比上一层更大,也更难触及。我们还看到了这些层级所处的位置:最上面两层由每个 SM 私有,而下面的所有层级要么在整个芯片上共享,要么完全在芯片之外。
这给了我们那个核心数字,即每获取一字节所完成的工作量。每块芯片都有一个盈亏平衡比值,在当前硬件上以 16 位精度计算约为每字节 300 次运算。低于这个值,你受限于内存;高于这个值,你受限于算术。
将其应用于语言模型,就解释了开篇的谜题。生成一个 token 需要读取每一个权重,并对每个权重执行大约两次运算,得到的比值接近 1,比盈亏平衡点低了大约三百倍。Prefill 则位于这条线的另一侧,这就是为什么两个阶段的行为如此不同。
最后,我们看到了每一个众所周知的优化都属于两种手段之一。批处理和分块增加了每次获取所完成的工作量。融合、量化和更好的内存布局减少了获取的字节数。
我们有意省略了编写 GPU 代码的底层细节,因为直觉必须先行。在这个框架之前编写 kernel,往往会产生这样的代码:快,但作者说不清为什么快;慢,也找不到原因。
后续的文章将以此为基础,进一步深入具体内容,包括如何对照盈亏平衡线来解读 profiler 输出、随着上下文增长 KV cache 如何改变内存格局,以及服务引擎在决定如何组批时内部究竟发生了什么。
一如既往,目标是建立扎实的系统级视角,并为你提供一个可灵活调整的工程框架,用来构建稳健且可维护的 AI 系统。
照例,感谢阅读!
干杯!:)