Roofline 模型:用算术强度定位 GPU 性能瓶颈(算力墙与带宽墙)
更新时间:2026-09-25。上一篇 GPU 架构深入:SIMT、warp 调度与内存合并 解释了 GPU 为什么快、又为什么会停顿,但留下一个更直接的问题:一个 kernel 跑出来只有峰值算力的三成,剩下七成到底丢给了谁?是乘法器不够多,还是 HBM 喂不过来?2009 年 Berkeley 团队提出的 Roofline 模型(屋顶线模型)用一张图回答了这个问题。它是 NVIDIA Nsight Compute、Intel Advisor 等商业性能分析工具的核心视图,也是 AI 加速器脉动阵列 一文反复引用的判据。本篇把这张图从公式到标点到优化方向完整走一遍,GPU 内存层级的硬件背景可对照 GPU 显存层级与带宽。
本文要回答的问题
- 一块 GPU 上,任意一个 kernel 的理论性能上限由什么决定?为什么说这条上限长成「屋顶」的折线形状?
- 算术强度(arithmetic intensity)是什么?FLOP/Byte 该按什么口径数,矩阵乘和逐元素加法各是多少?
- 拐点(ridge point)= 峰值算力 / 峰值带宽,这个除法的物理含义是什么?A100 的拐点为什么在 10 FLOP/Byte 附近?
- 怎样把一个具体算子标到 Roofline 图上,判断它是 compute-bound 还是 memory-bound?两类瓶颈的优化方向为何完全不同?
- FP32、FP16、Tensor Core 的算力差十几倍,为什么多级 Roofline 不是简单平移屋顶,而连拐点都会移动?
一、性能上限只有两条:算力墙与带宽墙
一个 kernel 在 GPU 上执行,时间无非花在两类动作上:做运算(乘、加、访存地址计算等)和搬数据(从 HBM 经过 L2、L1 到寄存器)。对应地,硬件给出两个不可逾越的峰值。
第一个是峰值算力 π(pi):芯片上所有计算单元满负荷时每秒能完成的浮点操作数,单位 FLOP/s。它由 SM 数量、每个 SM 的核心数、时钟频率和数据精度共同决定,是手册封面上最显眼的数字。第二个是峰值带宽 β(beta):显存子系统每秒能搬运的字节数,单位 Byte/s,主要由 HBM 的代际、位宽和频率决定,HBM 的堆叠与接口结构见 HBM 内存接口物理。
关键在于这两个峰值是「与」的关系:kernel 既不能算得比 π 快,也不能在数据供给上超过 β。于是可达性能(attainable performance)P 写成:
P = min(π,β × I)
其中 I 是这个 kernel 的算术强度,单位 FLOP/Byte,下一节展开。这个 min 就是整个 Roofline 模型的全部数学:β×I 是一条过原点、斜率为 β 的直线(算术强度越高,带宽能支撑的性能越高),π 是一条水平直线;两者取小,图像上就是一条先斜后平的折线,像一座斜屋顶接一堵平顶墙,「Roofline」由此得名。在双对数坐标下,斜线的斜率恰好为 1(I 每提高十倍,β×I 也提高十倍),平顶则为零,两轴取对数是为了让从 0.01 到 1000、横跨五个数量级的算术强度都能在同一张图上排开。注意 π 和 β 都是硬件常量,同一块卡上所有 kernel 共用同一条屋顶线;算子之间的区别只在于自己的 I 落在横轴什么位置,以及实际性能点离屋顶还有多远。
二、算术强度:每搬一字节,做多少浮点运算
算术强度 I 的定义朴素:
I = 总浮点运算数 F / 总访存字节数 Q (FLOP/Byte)
分子 F 数这个 kernel 一共执行了多少次浮点操作。一次乘法或一次加法各算 1 FLOP,一次 fused multiply-add(FMA,y = a×b+c)算 2 FLOP。分母 Q 数它在最慢存储层级(画基础 Roofline 时通常指 HBM)上实际搬运的字节数,读和写都算。这里有个必须先说清的口径:F 是算法固有量,GEMM 就是 2N³,写什么程序都不变;Q 却是实现相关的——同一个矩阵乘,每个元素从 HBM 只读一次和读十次,Q 差十倍,I 就差十倍。所以 Roofline 分析的分母要用 profiler 实测的 memory traffic,而不是拿矩阵体积想当然。
用几个小算例建立量级感,以下数字按 FP32(每个数 4 字节)、且数据都在 HBM、没有复用的最朴素情形估算。
- 逐元素加法 C = A + B:每个输出 1 FLOP,读 A、B 共 8 字节、写 C 4 字节,Q = 12,I = 1/12 ≈ 0.08 FLOP/Byte。
- 标量向量乘加 y = a·x + y(BLAS 中的 axpy):每元素 2 FLOP(一次乘、一次加),读写 12 字节,I ≈ 0.17 FLOP/Byte。
- 向量求和 reduction:N 个元素约 N−1 次加法,读 4N 字节,I ≈ 0.25 FLOP/Byte。
- 矩阵乘向量 y = Ax(GEMV):约 2N² FLOP,矩阵本身占 4N² 字节,向量的 8N 字节在 N 大时可忽略,I ≈ 0.5 FLOP/Byte。
- 矩阵乘矩阵 C = A×B(GEMM):约 2N³ FLOP,若三个矩阵各从 HBM 完整读一次,Q ≈ 12N²,I ≈ N/6 FLOP/Byte。N = 4096 时理论 I 接近 680 FLOP/Byte——注意这是「完美复用」的天花板,真实程序靠分块逼近它。
这组数字已经透出规律:逐元素类算子的 I 不到 1,GEMV 约 0.5,而 GEMM 随 N 线性增长。前者搬一堆字节只做一两次运算,后者数据进来以后在片上被反复乘几百次。理解 Roofline 的直觉全在这张对比里。
关于数法还有两个常见争论,提前钉死。其一,分母算不算写流量?标准 Roofline 算:写回的数据同样占用 HBM 带宽和写缓存资源,profiler 的 DRAM traffic 指标也是读写合计。读多写少的 kernel 差别不大,但全写型的初始化 kernel 若漏算写流量,I 会虚高一倍。其二,要不要数整数运算和地址计算?画「FLOP roofline」时分子只数浮点操作,但实际执行的整数指令(地址加加、循环计数)会占用发射槽和执行单元,它们不进公式却压低实际点——分析计算密集的规整算子时这部分可忽略,分析控制逻辑重的 kernel 时要意识到公式悄悄乐观了。也正因如此,严肃分析里 Roofline 的分子分母一律取 profiler 实测值,手算只用于建立量级感。
三、画出屋顶线:斜线、平顶与拐点
以 NVIDIA A100(SXM4 版本)为具体标本:FP32 峰值算力 π = 19.5 TFLOP/s,HBM2e 峰值带宽 β ≈ 2.0 TB/s(精确值 2039 GB/s,取 2 TB/s 便于心算)。横轴是算术强度 I(对数坐标),纵轴是可达性能 P(对数坐标),屋顶线由两段组成。
左半段是斜线 P = β×I:I 很小时,数据搬进来只做寥寥几次运算,计算单元大量时间在等数据,性能被带宽线性锁住。I = 0.1 FLOP/Byte 时,带宽撑死只能托起 2e12 × 0.1 = 0.2 TFLOP/s,不到峰值算力的百分之一——这就是为什么再朴素的逐元素 kernel 也几乎不可能「跑满」GPU。
右半段是平顶 P = π:当 I 大到 β×I 超过 π 之后,带宽已经不再是约束,计算单元成为瓶颈,性能沿 19.5 TFLOP/s 的平顶封顶。
两段的交点称为拐点(ridge point),其横坐标 I* 由 π/β 给出:
I = π / β = 19.5×10¹² / 2.0×10¹² ≈ 9.75 ≈ 10 FLOP/Byte*
拐点的读法是:算术强度超过 10,这个 A100 上的 kernel 才有可能摸到 FP32 算力天花板;低于 10,优化算力(加指令、提频率)毫无收益,唯一的出路是减少搬运或增加复用。拐点是一颗芯片的「性格」:算力越强、带宽越弱,拐点越靠右,大多数真实算子就越够不着屋顶;带宽越慷慨,拐点越靠左。这个除法也解释了为什么不同架构之间不能只比 TFLOP/s。
读图时还要看纵向上的距离。把一个 kernel 的实测点(实测 I,实测 P)标到图上,如果点在斜线段附近,说明它已经接近这个强度下的带宽极限,写得很好;如果点远低于屋顶,说明除了算力/带宽墙之外还有「内耗」——非合并访存、bank conflict、warp divergence、occupancy 过低,这些损耗不会移动屋顶,却会让实际点趴在屋顶下方。Roofline 因此有两层用途:先看落在斜线还是平顶,定瓶颈大类;再看点离屋顶多远,定实现质量。
四、把典型算子标到图上
把前面算过的 I 值对照拐点 10 FLOP/Byte 逐一归位,结论非常整齐。
| 算子(FP32) | 算术强度 I(量级) | 在 A100 屋顶线的位置 | 瓶颈类型 |
|---|---|---|---|
| 逐元素加 / ReLU | 0.08 ~ 0.13 | 斜线最左端,可达性能 <0.3 TFLOP/s | memory-bound |
| reduction 求和 | ~0.25 | 斜线左段 | memory-bound |
| axpy(y=a·x+y) | ~0.17 | 斜线左段 | memory-bound |
| GEMV 矩阵乘向量 | ~0.5 | 斜线段,远在拐点左侧 | memory-bound |
| 浅通道 stencil / 3 点滤波 | 0.3 ~ 1(有邻点复用) | 斜线段 | memory-bound |
| FFT | 1 ~ 5 | 斜线段,接近拐点但通常仍在左侧 | 多为 memory-bound |
| 3×3 卷积(通道充足、有分块) | 数十 ~ 数百 | 跨过拐点,可近平顶 | compute-bound |
| 大 GEMM(N=4096,tile 良好) | 数百(实测有效 I 常达 100+) | 平顶区 | compute-bound |
| 大 GEMM(无分块朴素实现) | 可跌到个位数 | 从平顶滑回斜线段 | 被实现拖成 memory-bound |
最后两行是整张表的精华:算子落在哪个区不是算法的固有属性,而是算法与实现共同决定的。GEMM 的理论 I 高达数百,但如果不分块、B 矩阵的每一列都从 HBM 重读一次,Q 暴涨、实测 I 跌回个位数,本应 compute-bound 的算子被拖回斜线区。反过来,一个逐元素算子几乎不可能靠重写变成 compute-bound——I 的天花板就那么高,这是算法形态锁死的。因此标点之前先问两个问题:分子 F 有没有被无效工作灌水,分母 Q 的复用做到了算法允许的几成。
实践中标点的数据全部来自 profiler:Nsight Compute 直接给出每个 kernel 的实测 FLOP、实测 DRAM 字节数、实测计算与访存吞吐,并内建 Roofline 视图把点和屋顶画在一起。手工分析时用 SM 吞吐百分比和内存吞吐百分比对照即可,两个百分比中明显偏低的那个指向瓶颈。
再给两个工程上高频出现的真实点位。大模型训练中的 GEMM 经过库级优化,实测有效算术强度通常在 100~300 FLOP/Byte 区间,足以越过 FP16 Tensor 的 156 拐点,实测性能可达峰值的七到九成;而大模型推理在 batch=1 的自回归解码阶段,权重矩阵每步都要完整读一遍、激活却退化成向量,F 从 2N³ 缩成 2N²,算术强度骤降到约 0.5,和 GEMV 一样趴在斜线最左端——这就是解码阶段算力利用率常年低于百分之十、吞吐量几乎与显存带宽成正比的根因,也是连续批处理(continuous batching)把多个请求拼批、把 GEMV 重新做「胖」成 GEMM 的价值所在。标点因此不只是诊断动作,它直接决定系统层该上什么技术。
五、两类瓶颈,两套完全相反的优化语言
知道点落在哪一段之后,优化方向才不会南辕北辙。两类瓶颈的手段几乎不通用——给 memory-bound 的 kernel 做指令级优化,和给 compute-bound 的 kernel 做缓存预取,都是白费力气。
memory-bound(点在斜线段)的目标是「少搬数据、搬得更有效」。第一类手段增加片上复用,直接抬高算术强度:用 tile 分块把工作集放进共享内存,让一块数据在被写回前服务多次计算;卷积用滑窗行缓存复用相邻输出共享的输入像素。分块是把算子在图上「向右推」的标准动作。第二类手段减少无效搬运:保证 warp 内线程访问连续地址以触发访存合并(coalescing),非合并访问会让一笔 128 字节的缓存行只用上 4 字节,有效带宽缩水数十倍;消除共享内存 bank conflict;用压缩存储、跳过 padding 区域;精度允许时换 FP16/INT8,数据体积直接减半或缩为四分之一,等效于 I 成倍提高。第三类手段掩盖延迟:提高 occupancy、用足够多的 warp 切换填满带宽流水线,配合双缓冲让计算与 DMA 重叠。这一类手段不增加任何算力,却能让性能沿斜线向右上爬升。
compute-bound(点在平顶区)的目标是「每字节指令做更多运算」。首要手段是走向量和矩阵专用通路:标量 FMA 换向量化的宽 SIMT 指令,再换 Tensor Core 指令(wmma/mma),单次指令完成的乘加数可提升一个数量级;选择与硬件匹配的精度(FP16/BF16/FP8),精度越低 Tensor Core 吞吐越高。其次是消除计算浪费:用快速算法或近似算法降低 F 本身(如 Winograd 卷积、FFT 卷积),让线程映射对齐矩阵形状、避免边缘块的空置 SM,减少地址计算和循环开销等非主体指令。这些手段的本质是把实际点向上抬向屋顶,乃至换到一个更高的屋顶。
把两类手段各压成一个可算账的例子。朴素 GEMM 不分块时,B 矩阵随 A 的每一行被重读 N 次,Q 从约 12N² 膨胀到 4N³,N=4096 时有效 I 从理论的 680 跌到约 0.5,性能点从平顶区一头栽到斜线最左端;按共享内存容量做二维分块后,每块 B 在片上被整块 A 复用,Q 回落,I 回到数百,性能可提升几十上百倍——这是「向右推」最极端也最经典的一次跳跃。而对已经坐在 FP16 平顶上的 GEMM,继续抠访存收益趋零,改用 FP8 Tensor Core 却把屋顶再抬高一倍,此时如果配套分块没加深、I 越不过新拐点,提速照样不发生。两类语言在这两个例子里互不替代。
一个实用的判别口诀:memory-bound 时优化的动词是「搬」——少搬、合并着搬、搬一次用多次;compute-bound 时优化的动词是「算」——换专用单元算、换低精度算、少算无效的部分。如果实测点远低于屋顶,则两类都不是,先修 divergence、bank conflict、occupancy 这些「漏水」问题,把点抬回屋顶轮廓再谈瓶颈。
六、多级 Roofline:换精度后,屋顶和拐点一起变
基础 Roofline 只画一个屋顶,但现代 GPU 的算力是分精度的,每种数据类型对应一块独立的计算单元阵列。A100 上三组关键数字:FP32 标量 19.5 TFLOP/s;TF32 Tensor Core 156 TFLOP/s(稠密);FP16/BF16 Tensor Core 312 TFLOP/s(稠密,开启 2:4 结构化稀疏再翻倍到 624)。而 HBM 带宽 β 不随精度变化,始终约 2 TB/s。于是每个精度有自己的屋顶和自己的拐点:
- FP32:I* = 19.5/2 ≈ 10 FLOP/Byte
- TF32 Tensor:I* = 156/2 ≈ 78 FLOP/Byte
- FP16 Tensor:I* = 312/2 ≈ 156 FLOP/Byte
这个结果反直觉但极其重要:屋顶升高 16 倍,拐点也向右移 16 倍。同一个 GEMM,I = 50 FLOP/Byte,在 FP32 屋顶下稳居平顶、已经 compute-bound;切到 FP16 Tensor Core 后,新拐点在 156,它反而落回了斜线段——Tensor 单元算得太快,HBM 供不上,瓶颈从算力变回了带宽。也就是说,换精度不是「免费午餐」:要兑现 FP16 Tensor 的 312 TFLOP/s,分块必须好到让有效算术强度超过 156,而不只是超过 10。这就是高性能 GEMM 库在 Tensor Core 时代把 tile 越做越大、层级复用做得越来越深的根本原因。
多级 Roofline 还可以沿存储层级继续画:HBM、L2、L1/共享内存各有自己的带宽(数 TB/s、十几 TB/s、数十 TB/s),每层对应一条更陡的斜线和更靠左的拐点。逐层 roofline 能回答「数据在哪个层级被复用才够用」,这正是分块大小选择的定量依据,与 GPU 显存层级与带宽 中给出的各层带宽数字配套使用。需要清醒的是,Roofline 给的是上限而非预言:它告诉你「最好能到哪」,不保证到达;真实点与屋顶之间的落差,仍要靠对 warp 调度、合并访存和占用率的逐项理解去填平。
这一篇在知识体系里的位置
- 硬件前篇:GPU 架构深入:SIMT、warp 调度与内存合并——Roofline 中的「漏水」因素(divergence、occupancy、合并访存)在该篇有完整硬件解释;
- 专用路线:AI 加速器脉动阵列——同样的算力/带宽判据,看 NPU 如何用数据流把算术强度焊进电路;
- 带宽侧:GPU 显存层级与带宽——多级 Roofline 各层斜线的带宽数字来源;
- 存储器件:HBM 内存接口物理——决定 β 的堆叠显存与接口实现。
总结
Roofline 模型用 P=min(峰值算力 π,峰值带宽 β×算术强度 I)一条折线概括 GPU 的全部性能上限:算术强度 I=F/Q 是每搬一字节所做的 FLOP,分子由算法决定、分母由复用决定;A100 的 FP32 屋顶 19.5 TFLOP/s、HBM 带宽 2 TB/s,拐点 I=π/β≈10 FLOP/Byte——左侧斜线段是 memory-bound(逐元素、reduction、GEMV),优化动词是「搬」:分块复用、访存合并、低精度压缩、双缓冲把点向右推;右侧平顶是 compute-bound(大 GEMM、深通道卷积),优化动词是「算」:Tensor Core、向量化、降低精度把点向上抬;换 FP16 Tensor 后屋顶升到 312 TFLOP/s、拐点右移到约 156,同一算子可能重新跌回带宽区,所以高算力必须配更深的分块与更高的有效算术强度——点离屋顶的落差,则交给 warp 调度与访存合并的实现功夫。*