GPU 实际是如何工作的

你是不是也有过这种困惑:GPU 利用率 99%,账单吓人,token/s 却少得可怜?规格表写着“每秒近一千万亿次运算”,结果真实体验像是买了跑车却堵在一条单行道上。很多人以为是代码没写好、驱动没调对,甚至怀疑要不要换更大的卡,但问题根本不在这里。

你看到的现象,其实是 GPU 设计里最关键、也最容易被忽略的那条物理规律在发声。一旦把这条规律吃透,量化、推测解码、连续批处理这些“黑魔法”会突然变成顺理成章的常识,而不是需要死记硬背的技巧清单。

👉 前置知识:只要知道神经网络是大量矩阵乘法堆起来的,模型权重是存储在内存里的数字,就够用。


一个不对称解释了几乎一切

算术很便宜,搬运数据才是真正的贵

大多数人直觉里,乘法听起来“很难”,把数字从内存搬到芯片上“应该很简单”。在现代 GPU 上,现实完全反过来:算术几乎不要钱,数据移动才是大头,而且贵得离谱。

可以想象一个巨大的车间:中间密密麻麻摆着成千上万张工作台,对应算术单元;一侧是仓库,里面堆满了权重和激活;两者之间只有一条不算宽的走廊,这条走廊就是内存带宽。工作台干活的速度远远超过走廊能送来材料的速度。

你再往车间里塞更多工作台,产能也上不去,因为走廊已经堵死了。GPU 的“算力”数字就是工作台总数,而真正限制你的,是那条走廊每秒能通过多少字节。你之后见到的几乎所有性能优化,本质都是同一件事:想办法让每次走廊来回,顺便多搬一点有用的东西。

GPU 工作台与内存带宽比喻

为什么算力和带宽的差距只会越拉越大

这并不是哪一代芯片“还没优化好”的问题,而是长期趋势。过去几年里,主流数据中心 GPU 的算术峰值几乎每代都翻倍,有的型号在 16 位精度下已经接近 1 PFLOPS;但内存带宽的增长却慢得多,常常只是从 2 TB/s 提到 3 TB/s、再到 4 TB/s。

结果就是:每一代新卡都能做更多运算,却不见得能更快地把数据喂饱这些算术单元。算术和带宽的失衡在代际间被不断放大。于是,减少数据移动的技术(比如量化、压缩 KV 缓存、FlashAttention)越来越值钱,而“算力越大越好”这种朴素想法,预测实际性能的能力越来越差。

GPU 为什么长成现在这个样子

CPU:为单条指令流服务的“豪华座驾”

CPU 的目标,是让一条指令序列尽可能快地跑完。它把大量硅片面积砸在各种“聪明的控制逻辑”上:多级大缓存把数据尽量留在身边,分支预测猜下一条指令,乱序执行在等待内存时先干别的活。

这些机制都很复杂、很占面积,但它们只服务于少数几个线程。典型服务器 CPU 也就同时推进几百个硬件线程,代价是每个线程都很“豪华”,可以独立分支、乱序执行、深度优化单条指令流。

GPU:砍掉控制逻辑,换成一墙的算术单元

GPU 面对的负载完全不同:图形渲染里,每个像素都跑同一个小程序;神经网络里,每个张量元素都做类似的运算。既然大家都在跑同一段代码,就没必要给每个线程配一套昂贵的控制系统。

GPU 做的事很直接:把 CPU 上那一大坨复杂控制逻辑砍掉,只保留一个相对简单的“指挥中心”,然后用省下来的面积,堆成千上万的算术单元。一个控制器驱动一大群线程,这些线程执行同一条指令,只是操作的数据不同。

GPU 与 CPU 线程对比

高端服务器 CPU 也许能同时推进几百个线程,而一块数据中心 GPU 在同样功耗下,每个时钟周期可以推进数万个线程。代价是:单个 GPU 线程非常“弱”,它不能随心所欲地分支、乱序,只是这台超宽、超简单机器上的一条“车道”。

👉 一个关键术语:GPU 上线程不是一个个单独调度的,而是 32 个线程打包成一组 warp,一起执行同一条指令。warp 才是硬件真正调度的基本单位。

唯一的例外:warp 内部分歧

warp 里的 32 个线程共用一条指令流,这带来一个有点反直觉的限制:它们不能在控制流上各走各的路。如果代码里有 if 分支,而 warp 内有一半线程走 if,另一半走 else,硬件会先执行 if 分支,关闭走 else 的线程;再执行 else 分支,关闭走 if 的线程。

结果就是:warp 的总执行时间变成“所有分支时间之和”。不同 warp 走不同路径没问题,只有 warp 内部分歧才会拖慢速度。对 LLM 工程师来说,这意味着:紧凑内循环里尽量避免基于数据的复杂分支,而调用 cuBLAS、FlashAttention 这类库时,通常不用太担心这个问题。

GPU 不让等待变短,而是让等待“消失”

CPU 想尽办法避免等待,GPU 干脆拥抱等待

数据从 HBM(显存)到算术单元,中间有固定的物理延迟,CPU 和 GPU 都躲不过。CPU 的策略是:用缓存、预取、预测,把未来可能用到的数据提前搬过来,尽量别等。

GPU 的策略完全不同:它接受“每个线程大部分时间都在等数据”这个现实,然后安排好,当某个线程在等的时候,总有别的线程可以上来干活。说白了,就是用海量并发,把等待时间摊平。

在一个计算单元(SM)里,可能常驻着 64 个 warp,但每个时钟周期只执行其中一个。某个 warp 发起内存请求后被卡住,调度器不会干等,而是立刻切到下一个准备好的 warp;那个 warp 也被卡住,就再切到第三个。

GPU warp 切换示意

warp 之间的切换几乎没有成本。CPU 切线程要保存寄存器、恢复状态,动辄几百个周期;GPU 直接在片上给每个 warp 预留了一整套寄存器和状态,切换只需要“指针一转”,一个时钟周期就搞定。这也是为什么 GPU 上要放一大坨寄存器和共享内存——不是为了让单个线程更快,而是为了让成千上万条半完成的线程都能常驻在芯片上,随时接力。

👉 这也是监控里“GPU 利用率 100%”经常误导人的原因:它只说明调度器一直在发工作,并不代表算术单元真的在做有用的计算。数据没喂饱时,空转和满负荷在监控上看起来几乎一样。

实际含义:GPU 需要“大块头”的工作

如果只记住一个操作层面的结论,那就是:GPU 需要大量、彼此独立但结构相似的工作,才能跑得高效。给它一堆小任务,它的速度会被内存系统锁死,大部分算术单元闲着没事干;给它一大批“长得差不多”的任务,同一块硬件立刻就像换了台机器。

这也是为什么所有部署讨论里,批大小这个词总会反复出现。不是大家迷信“batch 越大越好”,而是 GPU 的设计从一开始就假定:你会给它一大批可以一起算的东西。

内存不是一块,而是一座“楼梯”

四级内存:离算术单元越近越快

数据并不是简单地“在内存里”,而是分布在离算术单元不同远近的几个层级上。离得越近,访问越快,但容量越小;离得越远,容量越大,但访问代价成倍增加。

常见可以粗分成四级:

  1. 线程私有存储(寄存器):离算术单元最近,每个线程有少量寄存器,访问几乎是“零成本”。
  2. 片上 scratchpad(共享内存):每个 SM 自带几百 KB 的超快内存,程序可以精确控制存什么、存多久。
  3. 共享缓存(L2):几十 MB 级别,由硬件自动管理,所有 SM 共享,你只能通过访问模式“间接影响”它。
  4. 主内存(HBM/VRAM):几十 GB,用来放模型权重、KV 缓存和激活,物理上在芯片外,访问最慢。

GPU 内存层级示意

两端的差距非常夸张:寄存器里的一个值,访问几乎不花时间;而从 HBM 里拿同一个值,代价可能是前者的几百倍。你的模型权重只能放在最底层,因为只有那一层容量够大。

两种成本:延迟可以藏,带宽藏不住

访问内存有两种成本:

  • 延迟:从发出请求到拿到数据的时间。
  • 带宽:单位时间内最多能传输多少字节。

GPU 用前面说的“海量 warp 轮流上场”把延迟藏起来:一个 warp 在等,就切到另一个。只要有足够多的 warp 在排队,单个 warp 的等待时间就不重要。

但带宽没法靠调度魔法解决:HBM 每秒只能吐出固定数量的字节,不管你排多少 warp,所有请求叠在一起,最终都要挤过这条“总线”。这就是规格表上写的“3.35 TB/s 内存带宽”这种数字的含义。

后面所有关于性能的估算,几乎都只看带宽,而不是延迟。

SM:GPU 的“基本作战单元”

SM 里到底装了什么

GPU 不是一大坨算术单元的池子,而是被切成几十到上百个小型自包含单元,每个叫一个 SM(Streaming Multiprocessor)。所有 CUDA 文档、NVIDIA 工具里都会提到这个名字。

每个 SM 自带:

  • 自己的一大块寄存器文件(线程私有存储);
  • 自己的共享内存和 L1 缓存;
  • 自己的 warp 调度器。

这些东西在 SM 之间不共享。工作以“线程块”的形式提交,每个线程块被分配到一个 SM 上,从头跑到尾都不会被拆开。线程块里的线程再被自动切成若干 warp。

举个例子:你启动一个 256 线程的线程块,硬件会把它拆成 8 个 warp(每个 32 线程),调度器在这 8 个 warp 之间轮流调度。你只需要选线程块大小,warp 的划分由硬件自动完成。

为什么大家都喜欢 32 的倍数

这也解释了一个常见“玄学”:批大小、线程数经常被建议设成 32 的倍数。原因很简单:warp 就是 32 条“车道”。你要 250 个线程,硬件还是要开 8 个 warp,但最后一个 warp 只有 26 条车道有活干,剩下 6 条白白浪费。

单个线程块最大是 1024 线程,也就是一个 SM 上最多同时挂 32 个 warp。因为线程块里的线程都在同一个 SM 上,它们可以用共享内存互相协作,成本极低,这也是“共享内存”这个名字的由来。

所有 SM 下面,是全芯片共享的 L2 缓存;再往下,就是芯片外的 HBM,放权重、KV 缓存和激活。HBM 也就是你在 API 里看到的“全局内存”或 VRAM。

GPU 内存层级与 SM 结构

有个有点反直觉的事实:整个 GPU 上所有寄存器加起来的总容量,往往和 L2 差不多大。这在 CPU 世界很罕见,但在 GPU 上很合理——因为要同时挂着成千上万条半完成的线程,每条都要有一份状态。

每字节做多少活,决定一切

算术强度:一个数字就能预判性能

现在可以把前面的直觉收束成一个可计算的量。对 GPU 上的任意操作,你都可以算出两个数字:

  • 需要执行多少次算术运算;
  • 为了做完这些运算,需要从 HBM 读多少字节。

用“运算次数 ÷ 字节数”,就得到这个操作的“每字节工作量”,也叫算术强度(arithmetic intensity)。它回答的是:每次走廊来回一趟,你到底榨出了多少算力价值。

据公开资料统计,很多经典 HPC 算法的算术强度差异可以达到 1000 倍以上:同样读一遍数据,有的算法只做几次简单运算,有的算法能做上千次矩阵乘法。

两个极端:元素级操作 vs 矩阵乘法

关键不在于你读了多少字节,而在于每个读进来的值被用多少次。比如:

  • 把数组每个元素乘以 2:每个值读一次,用一次,再写回去。每字节大概只做 1 次运算。
  • 做一个 1024×1024 的矩阵乘法:第一矩阵里的每个值要和第二矩阵的 1024 个值相乘,一次读取,参与上千次运算。

单次读取带来的运算量可以差三个数量级,这就是为什么 GPU 对矩阵乘法这种“高算术强度”的操作特别友好,而对一堆零散的小元素级操作就显得很“嫌弃”。

GPU 的“平衡点”:每字节 300 次运算

用 H100 算一笔账

每块 GPU 都有一个“平衡点”,可以用“峰值算术吞吐 ÷ 峰值内存带宽”算出来。以 H100 SXM5 为例(目前云上很常见):

官方规格:BF16 密集算术峰值约 989 TFLOPS,HBM 带宽约 3.35 TB/s。

把这两个数字相除:

989×10¹² 次运算/秒 ÷ 3.35×10¹² 字节/秒 ≈ 295 次运算/字节。

也就是说:如果你的操作每读 1 字节,只能做 100 次运算,那这块卡的算术单元注定要有三分之二时间闲着;只有当每字节能做 300 次以上运算时,才有可能把算力吃满。

H200 用的还是同一套算术核心,峰值算力还是 989 TFLOPS,但带宽拉到了 4.8 TB/s,平衡点就降到了大约 206 次运算/字节。平衡点越低越好,意味着更多工作负载会变成“算术受限”,带宽升级本身就能加速推理。

Roofline 模型示意

👉 这张图就是著名的 roofline 模型:左边斜线是“带宽天花板”,右边横线是“算力天花板”,你的工作负载会落在某个位置,要么被带宽卡住,要么被算力卡住。

为什么生成一个 token 是最糟糕的情况

700 亿参数模型:算术强度只有 1

把这个框架套到语言模型推理上,开头那个“为什么这么慢”的谜题就好解了。生成文本是逐 token 进行的:每生成一个新 token,都要跑一遍完整的前向传播,把所有层都算一遍。

对每个权重来说,它的作用就是:拿一个输入值,乘上这个权重,再加到输出上。也就是 1 次乘法 + 1 次加法,合计 2 次运算。700 亿参数模型生成一个 token,大约要做 700 亿 × 2 = 1400 亿次运算。

再看内存:16 位精度下,每个权重 2 字节,700 亿参数就是 140 GB。生成一个 token,需要把这 140 GB 权重从 HBM 里扫一遍。

生成 token 的算术与内存需求

把两者一除:1400 亿次运算 ÷ 140 GB ≈ 1 次运算/字节。还记得 H100 的平衡点吗?大约是 300 次运算/字节。也就是说,单请求生成阶段的算术强度比平衡点低了 300 倍。

这就是为什么算术单元大量闲置:不是你代码写得差,也不是驱动没调好,而是“生成一个 token”这件事,从物理上就不可能把 GPU 的算力吃满。

用带宽直接算 token/s 底线

既然已经确认生成阶段是“纯带宽受限”,那 token 速率就可以直接用带宽算出来。700 亿参数、16 位精度模型大约 140 GB,H100 级别 GPU 带宽约 3.3 TB/s:

140 GB ÷ 3300 GB/s ≈ 0.042 秒。

也就是说,光是把权重从 HBM 扫一遍,就要 42 ms 左右。批大小为 1 时,每生成一个 token 都要扫一遍权重,所以理论上每秒最多也就 1000/42 ≈ 24 token。

生成速率与内存带宽关系

很多人在线上服务里看到 20 多 token/s 的数字,会本能地去怪框架、怪内核、怪调度。其实吧,只要模型大小和带宽摆在那儿,这个数量级就是物理底线,软件再怎么优化也不可能把 24 变成 240。

预填充:同一块卡,完全相反的瓶颈

为什么长 prompt 感觉“没那么慢”

生成阶段只是推理流程的一半。你把 prompt 发给模型时,它会先一次性处理所有输入 token,这一段叫 prefill(预填充)。prefill 的特点是:同一批权重会被在不同 token 上反复使用,算术强度一下子就上去了。

在长 prompt 场景下,每个权重可能被用几十次甚至上百次,这样每字节的运算次数就远远超过前面说的 300 的平衡点。prefill 阶段往往是“算术受限”的——算力吃满了,带宽还很富余。

这也是为什么很多人主观感受是:长 prompt 的“首 token 延迟”确实会变大,但远没有“输出 1000 个 token”那么夸张。两段流程的瓶颈完全不同,混在一起讨论,很容易得出奇怪的结论。

所有优化,其实都在动同一个比例

只剩两条路:多干活,少搬砖

一旦把性能问题抽象成“每字节工作量”这个比例,很多看似杂乱的优化技巧就会自动归类。你能做的事情,其实只有两种:

  • 增加每次读取能做的工作量(提高算术强度);
  • 减少必须读取的字节数(降低带宽需求)。

你听过的几乎所有优化——批处理、融合算子、FlashAttention、量化、KV 缓存压缩、连续批处理——都只是这两种思路的不同变体。

批处理:同一份权重,多次复用

批处理是最直接、也是收益最大的办法:一次把多个请求塞进同一批里,权重只读一遍,算术量却按请求数成倍增加。10 个并发请求,理论上就能把每字节工作量放大 10 倍。

在 16 位精度下,要让 700 亿参数模型的生成阶段从“带宽受限”变成“算术受限”,大致需要几百条并发序列。有用户反馈,在 H100 上把 batch 从 16 提到 256,生成吞吐可以提升 8 倍以上,但单个请求的延迟会被拉长,这就是典型的吞吐 vs 延迟权衡。

批处理提升示意

融合算子:中间结果不落地

另一条路是减少必须经过 HBM 的字节数。最典型的例子是链式元素级操作:比如先做一次 x = x * scale,再做一次 x = x + bias,然后再过一个激活函数。如果这三步分别调用三个 kernel,每一步都要从 HBM 读一遍、写一遍。

把它们融合成一个 kernel,中间结果可以一直待在寄存器或共享内存里,整个链条只需要一次读、一次写。算术量没变,HBM 流量却直接砍掉三分之二。这也是为什么对“元素级操作很多”的模型,融合编译器(如 TensorRT、XLA)能带来巨大收益,而对“纯 GEMM 堆起来”的模型,收益就有限。

把数据“留在身边”:FlashAttention 的思路

共享内存的存在,就是为了让你把一块数据搬上来,多用几次再放回去。FlashAttention 就是一个经典案例:朴素实现会构建一个巨大的注意力矩阵,把它写到 HBM,再读回来做 softmax 和加权求和。

FlashAttention 把注意力矩阵按块切开,在共享内存里一块块算完就地消费,从不把完整矩阵写回 HBM。算术量几乎没变,但 HBM 流量和运行时间都大幅下降。有公开基准显示,在长上下文(比如 8K token)下,FlashAttention 可以让注意力部分提速 2~4 倍。

量化:直接把字节数砍半

量化是最粗暴、也最有效的“减字节”手段。把权重从 16 位压到 8 位,模型大小直接减半。对生成阶段来说,每个 token 必须读取的权重字节数也减半,于是每字节工作量翻倍。

对 700 亿参数模型来说,从 16 位(约 140 GB)压到 8 位(约 70 GB),理论带宽底线从 24 token/s 提升到接近 50 token/s。现实中会有量化误差、反量化开销等因素,实际提升可能略低,但数量级是对的。

当然,代价是精度损失。不同模型、不同量化方案的效果差异很大,有的几乎无感,有的会明显影响长上下文推理质量,这一点需要通过评测和线上反馈反复验证。

顺序访问:带宽利用率的隐形杀手

还有一个经常被忽略的细节:访问模式。HBM 不是按“单个字节”传输的,而是按固定大小的块(cache line)搬运。warp 内 32 个线程如果访问的是连续地址,硬件可以把它们的请求合并成少数几个块,一次搬完。

如果访问的是稀疏、分散的地址,每个线程都可能触发一次块传输,但只用到其中几字节,剩下的全浪费。极端情况下,你以为自己只读了 1 GB 数据,实际上 HBM 传输了 8 GB 甚至更多。

内存访问模式示意

这就是为什么“内存布局”会变成性能工程里的核心决策:矩阵是按行存还是按列存,KV 缓存是按 batch 还是按 head 排列,都会直接决定你能不能吃满带宽。有一次我帮朋友排查一个“莫名其妙慢”的注意力实现,最后发现只是把一个维度顺序写反了,改完之后速度直接翻倍。

小任务的隐藏成本:开销受限

GPU 看着很闲,CPU 却快烧起来了

前面的算术强度框架有个盲区:它默认你的任务足够大,足以淹没调度开销。但现实里,很多模型会触发成百上千个小 kernel,每个 kernel 本身只跑几微秒。

每次向 GPU 提交工作,CPU 端都要做一堆准备:构造参数、发起调用、等待完成。这些开销和任务大小几乎无关,小任务和大任务的“起步价”差不多。如果你的模型被拆成了很多小操作,CPU 可能忙着发 kernel,GPU 反而大部分时间在等活。

表现出来就是:GPU 利用率不高,HBM 带宽也没打满,但 CPU 占用却很高。遇到这种情况,解决思路通常是:

  • 减少 kernel 数量(算子融合、图编译);
  • 把多个小操作合成一个大操作;
  • 或者用 CUDA Graph 之类的机制,把一串操作录成“宏”,一次性提交。

我也不太确定这个说法对不对,但从最近一些开源推理引擎的演进来看,“减少 kernel 数量”已经成了和“提高算术强度”同等重要的优化方向。

如何判断自己卡在哪儿

三种典型状态,一眼就能分辨

要让上面的框架真正有用,你得先知道自己处在哪种瓶颈下。比较靠谱的做法是:在运行时测量两个指标——实际 HBM 带宽利用率、实际算术吞吐——然后和硬件峰值对比。

常见会落在这三种情况之一:

  • 带宽接近峰值,算术远低:典型的内存受限。优先考虑批处理、量化、算子融合、FlashAttention、优化 KV 布局等,单纯换更大算力的卡意义不大。
  • 算术接近峰值,带宽远低:算术受限,属于“理想状态”。可以考虑更高效的算法、更低精度(FP8、INT8)、或者换算力更强的卡。
  • 两者都远低于峰值:多半是开销受限或任务太小。检查 kernel 数量、单次任务规模,以及是否可以用图编译或 CUDA Graph 合并调用。

👉 在语言模型推理里,生成阶段在绝大多数现实配置下都是内存受限。想提升 token 吞吐率,批大小、精度(量化与否)、KV 缓存大小和布局,几乎总是最关键的几个旋钮。

硬件在变,底层规律没变

数字会变,形状不会变

最近两年,GPU 规格表上的数字变化非常快:显存从 40 GB 到 80 GB,再到 141 GB;带宽从 2 TB/s 到 3 TB/s,再到 4.8 TB/s;算术峰值更是一路飙升。各种新精度(BF16、FP8、甚至更激进的格式)层出不穷,本质都是在“缩小每个权重的字节数”。

但形状没变:

  • 算术依然比数据移动便宜几个数量级;
  • 内存依然是一座楼梯,而不是一块平地;
  • 每字节工作量依然决定你是被带宽卡住,还是被算力卡住。

甚至可以说,这个框架会越来越重要。因为算力增长比带宽快得多,很多在旧卡上“算术受限”的工作负载,搬到新卡上会自动变成“内存受限”,而你的代码一行没改。最近围绕“长上下文 LLM”的讨论,就是一个典型例子:上下文一拉长,KV 缓存暴涨,带宽瞬间变成第一瓶颈。

小结与行动建议

如果把这篇内容压缩成一个判断方法,那就是:

先算清楚:你的任务每读 1 字节,能做多少次运算?再对照硬件的“平衡点”,就能知道自己是在和带宽较劲,还是在和算力较劲。

一旦这个直觉建立起来,批处理、量化、FlashAttention、连续批处理、KV 压缩这些看似杂乱的技巧,会自动归位成两类:要么是“多干活”,要么是“少搬砖”。你不再需要死记硬背每个优化的细节,只需要问一句:它是怎么改变“每字节工作量”这个比例的?

这个判断方法在不同模型、不同硬件上反复验证都很靠谱,值得你收藏下来,遇到性能问题先拿它过一遍,再决定要不要去翻 profiler 截图。


常见问题

Q:为什么我用同一块 GPU,prefill 阶段比生成阶段快很多?

A:prefill 阶段对同一批权重会在多个输入 token 上反复使用,算术强度很高,通常是算术受限;生成阶段每个新 token 都要完整扫一遍权重,算术强度接近 1,几乎纯带宽受限。判断依据是:prefill 时 HBM 带宽往往没打满,但算术单元接近峰值;生成时带宽接近峰值,而算术单元大量闲置。建议在生成阶段优先调大批大小、使用量化或 KV 压缩,而在 prefill 阶段更多关注算子融合和更高效的注意力实现。

Q:如何快速估算某个模型在某块 GPU 上的理论最大 token/s?

A:可以用“模型权重大小 ÷ GPU 带宽”估算生成阶段的物理下限。步骤是:1)算出模型参数总数,乘以每个权重的字节数(例如 BF16 是 2 字节),得到权重总大小;2)用权重大小除以 GPU 的 HBM 带宽,得到“每次完整扫一遍权重”的时间;3)对 batch=1 的生成阶段,这个时间就是单 token 的理论最短时间。比如 140 GB 权重、3.3 TB/s 带宽,大约是 42 ms/ token,即约 24 token/s。实际表现会略低,建议把这个数字当作“天花板”,用来判断优化是否已经接近极限。

Q:批大小增大后吞吐提升不明显,是不是已经算术受限了?

A:不一定,有可能是其他瓶颈先出现。直接判断的方法是看 profiler:如果 HBM 带宽已经接近峰值,而算术吞吐还远低于峰值,那就是典型的内存受限,继续加大 batch 也难有提升;如果两者都不高,说明可能是 kernel 太碎、CPU 调度开销太大,或者存在严重的内存访问不连续问题。建议先检查 kernel 数量和平均执行时间,尝试启用图编译或算子融合,再通过调整 batch 观察带宽和算术利用率的变化曲线。

Q:量化到 8 位后,为什么实际 token/s 提升没有理论上的 2 倍?

A:理论上权重从 16 位压到 8 位,HBM 流量减半,生成阶段的带宽瓶颈应该放宽一倍。但现实中还存在量化/反量化开销、额外的查表或缩放操作,以及部分算子仍然在高精度下运行等因素,这些都会吃掉一部分收益。判断依据是:如果带宽利用率明显下降,而算术利用率上升不多,说明算术开销被放大了。建议使用更成熟的量化方案(如 GPTQ、AWQ)、尽量让主干算子在低精度下执行,并通过端到端基准测试验证收益,而不是只看模型文件大小。

Q:怎么判断自己是否需要上 FlashAttention 这类“高级优化”?

A:关键看你的上下文长度和注意力在整体算时中的占比。对于短上下文(比如 512 token 以下),注意力本身占用的时间有限,FlashAttention 带来的收益可能不明显;但在 4K、8K 甚至更长上下文下,朴素注意力会产生巨大的中间矩阵,HBM 流量暴涨,注意力部分可能占到总时间的 30%~50%。如果 profiler 显示注意力 kernel 的带宽利用率很高、算术利用率一般,而且上下文较长,那就是 FlashAttention 发挥作用的典型场景。建议在长上下文服务或批量离线推理中优先尝试,并对比端到端延迟和吞吐的变化。


如果你正打算上线一个大模型服务,或者在为“为什么这块卡跑不满”抓狂,这套从算术强度出发的思路,往往比问十个朋友更有用。很多看似玄学的调参,其实都能被这一个比例解释清楚。剩下的,就交给你在自己的模型和硬件上慢慢验证了。