第 4 章 · 自注意力The roofline

roofline 屋顶线

深入阅读 · 第 4 章 自注意力——给「memory-bound」配上一个真正的数字。

显存带宽墙一直靠一个词——memory-bound——来解释为什么注意力、以及一般意义上的逐 token decode,时间都花在等内存而不是算数上。那是个故事。roofline(屋顶线)把它变成一个可以画出来的数字,而这个数字会精确告诉你:故事从什么时候起不再成立。

两道天花板,你先撞上哪一道

GPU 会因为两个彼此独立的原因走到尽头,你先够到哪一个,哪一个就是你的天花板。一个是 compute——Tensor Core 每秒能做多少次数学运算(H100 的 fp16 上限约 989 TFLOPS)。另一个是 memory bandwidth——你每秒能从 HBM 里搬出多少字节(同一块 H100:3.35 TB/s)。一个封住数学,另一个封住搬运的字节。引擎再快,供油跟不上也没用。

到底哪道天花板困住你,取决于工作负载,而不是芯片:它的 arithmetic intensity(算术强度)——每搬一个字节所做的 FLOPs 工作量,单位是 ops:byte

那道位于约 295 ops:byte 的 ridge(脊线)就是收支平衡点。每字节做的运算少于295,你会远在下一批字节到来之前就把数学算完——你是 memory-bound,在 ridge 左侧干等。做得更多,字节到达的速度反而比你能嚼完的还快——你就是 compute-bound,在右侧。roofline 把可达性能对算术强度画出来:一条对角的 bandwidth 上限不断爬升,恰好在 ridge 处与水平的 compute 上限相交。

prefill 和 decode 各自落在哪

推理的两个阶段分坐两侧。prefill 一口气消化整条提示词,所以它读进来的每个权重都被数百个 token 位置并行复用——每字节一座算术大山。它稳稳地落在 ridge 右侧,compute-bounddecode 在 batch 1 时是另一个极端:为了产出一个 token,它把整个模型的权重恰好流过芯片一遍,几乎不做什么数学。它跌下最左侧的悬崖,深度 memory-bound——正是本课程一再撞上的那种「闲着等内存」。

拖动 batch 滑块,看 decode 点行进。把 N 个请求批在一起,能让这 N个都复用同一次权重读取,于是 intensity 大约按 ×N 攀升,点沿对角线向右滑向 ridge——只有当每字节终于有了足够的数学量去喂饱 FLOPs 时,它才从 memory-bound 翻成 compute-bound:

roofline——给「memory-bound」一个数字
1101001000101001000TFLOPSridge ≈ 295 ops:bytecompute 上限 · 989 TFLOPSbandwidth 上限 · 3.35 TB/smemory-boundcompute-boundprefilldecode · batch 1arithmetic intensity——ops:byte(FLOPs ÷ 搬运的字节数),对数刻度
当前 intensity: 1.2 ops:byte
判定: memory-bound

拖动滑块:batch 里的每个 token 都复用同一次权重读取,所以 intensity 大约按 ×batch 攀升,decode 点向右朝 ridge 前进。

计算示例——一个 batch-1 decode step 每个参数只做约 2 FLOPs 的数学(一次乘加),却必须把每一个参数都从内存里读出来(bf16 下 2 字节),只为吐出一个 token:~2 FLOP ÷ 2 字节 1.2 op:byte ≪ 295,被钉在 memory 悬崖上。这个比值几乎与模型大小无关——这正是为什么每个batch-1 的 LLM decode 都是 memory-bound。

这个浏览器内的 demo 被钉在 batch 1,停在最左侧的 memory 悬崖上——单个用户、普通自回归 decode。在这里,更快 GPU 的额外 FLOPs 帮不上忙,因为你受 bandwidth 限制,而非 compute 限制。生产环境的服务会把 数百个用户 一起批处理,沿对角线爬升到 ridge——GPU 的算力正是这样才被真正用起来。

所以「memory-bound」从来不是模型的一个固定属性——它是你如何运行它的属性。一个用户等一个 token,就坐在悬崖上;一台把数百个用户批在一起的服务器,则沿对角线爬升,直到 compute 终于变成那个值得优化的东西。这段攀爬正是后面batching 子章节的全部主题——roofline 就是它读的那张地图。