加速器系统课程 / 主线 / 专题二 / L200 · 精讲版 📝 讲义(授课稿)

TPU 与 GPU

Two Accelerators, Two Origins
不逐个介绍硬件参数。
拿同一个 FlashAttention,在两块硬件上各跑一遍,看它是从哪一步开始不同的。

两个问题,一条路线。① 这两块硬件在硬件层面到底哪里不一样; ② 把同一个算子从头到尾跑一遍,看那些不一样分别在哪一步冒出来。 —— 全课的结论是:两边处处都不同,但这些不同全是同一个决定的后果。

TPU 侧 v7 (Ironwood) GPU 侧 拆 B200 实测 GB300 NVL72 规格来源 公开源码 + 自有实测 109 分钟 · 43 张图 完整版 L300 →

课程作者 Chris Yang·Google Cloud AI Infra 架构师

⛔ 课前勿点 —— 开讲前的两道热身题现场会一起做;提前看答案=自己剧透,这两道题就废了

开讲之前,先回一道上一课的题

上一课我们拆了 DeepSeek V3:总参数 671B, 但每个 token 只激活 37B —— 这就是 MoE
于是很自然会想:既然只激活 37B,那它是不是就跟一个 37B 的稠密模型一样好养?
问的就是这个:MoE 到底省下了什么?

省算力,不省显存。
算的时候只走 37B 那条路,算力是真省了; 可 671B 的权重一个字节都不能少放 ——  你不知道下一个 token 会挑中哪几个专家,所以全都得在显存里待着。 「激活 37B」跟「像 37B 一样好养」是两回事。

⭐ 而这道题真正想让大家听见的,是它背后那件事: 显存和算力是两样东西,会各走各的。 上一课整整一课都在分开算这两笔账。
这一课要加的是第三样 —— 带宽。 而且下面马上会看到:这三样里,真正决定一个算子快不快的, 常常不是前两样。第 1 节只做一件事,就是把「算」和「搬」的兑换比算出来。

再猜两个数 —— 答案马上给,「为什么」要等到第 3 节

这门课后面会把两颗芯片拆开,一层一层往里看。 在开讲之前先猜一下 —— 它们各自把算力切成了多少块?
B200 上那个单元叫 Tensor Core,v7 上那个叫 MXU, 干的是同一件事:矩阵乘加。
问的是整颗芯片上各有几个

① B200 整颗上有几个 Tensor Core
② TPU v7 整颗上有几个 MXU

两边各选各的 —— 都选完才出答案。

B200:592 个。v7:4 个。
592 = 148 个 SM × 每个 SM 4 个 Tensor Core; 4 = 2 个 TensorCore × 每个核 2 个 MXU。 份数差 148 倍。

⭐ 但真正该惊讶的是下一句: 这两颗芯片一拍能做的乘加总数只差 15% —— B200 是 606,208,v7 是 524,288。
同样一份算力,一边切成 592 小块,一边切成 4 大块。

⭐ 数字给你了,「为什么」先欠着 —— 揣着这个问号往下听。 为什么份数能差 148 倍、总量却几乎一样?切得碎和切得整, 各自要付什么代价?第 3 节整整一节在回答这两个问题。 —— 到那儿的时候,你会自己认出这道题的答案长在哪一张图上。

第 0 节

先立一个前提:两款芯片,出身不一样

正文从下面这张图开始,全课只有这一个前提 —— 看了它,后面每一节你都能自己预判「这里两边应该会不一样」。 看完就往下走,第 1 节只做一道除法,第 2 节就开始拆硬件。

先想三十秒 假如让你设计一块芯片,而你不知道客户会拿它跑什么 —— 你会怎么做? —— 再想一遍反过来的:如果你早就知道它只跑一种东西呢? 这两个答案的差,就是下面这张图。
整门课只有一个前提:两款芯片出身不同,于是每一层都不同 GPU 出身:图形处理器 要伺候大量互不相干的小任务 形状、访问模式、控制流全都无法预知 → 面积大半花在「应付各种情况」上 cache 层次 · warp 调度 · 海量并发线程 不知道你要跑什么 → 处处留一手 TPU 出身:为矩阵乘定做的 ASIC 从第一天就只服务神经网络 形状规则、访问可预测、控制流静态 → 省下的是复杂度,不是换来更多算力 无 cache · 无调度器 · 同样的算力只切成 4 大块 早就知道你要跑什么 → 一手都不留 vs
两种出身不是优劣,是两条路线 —— 一条靠运行时现判,一条靠编译期算准。说到底只差一件事:你要跑的东西能不能提前知道。⭐ 右边那句「早就知道你要跑什么」是全课的前提:后面每一节两边只要不一样,追到底都会回到这一句上。—— 先带着它往下走,别急着记两边各有什么部件。
开课前先把话说死:这门课不比谁强 两种出身各自都是当年的正确决定,代价也各自都还在。 所以全课不会出现「谁更好」这种句子,只会出现 「这件事在这一边由谁决定」
—— 如果只带走一句话,就是这一句:两边处处都不同,但这些不同全是同一个决定的后果。 那「同一个决定」是什么,第 2 节给答案。
⚠️ 先说一句:这一版的节号会跳(没有第 8 节),图上的号是完整版的号(不是你漏读了)

这一版的节号跟完整版 L300 对齐,不重排。 好处是听完想往下挖的人,可以拿着同一个号直接翻 L300 的同一节; 代价是这一版会跳号0 · 1 · 2 · 3 · 4 · 5 · 6–7 · 9 —— 第 8 节整节只在 L300 里,6 和 7 在这一版合成了一节。

⚠️ 还有一处只在图上:有一批图是整块从 L300 搬来的,图内文字用的是 L300 的号。 大节的对照关系是这三条:

  • 图上写 §7(两边的实测) —— 在这一版是 6.3
  • 图上写 §6(怎么比才有意义) —— 在这一版是 6.1 / 6.2
  • 图上写 §8(这一课的数我是怎么核的) ——  这一版没有这一节,它整节只在 L300

小节号同理,而且更容易撞上。下面这些号在这一版里不存在, 看到就直接去 L300 翻,不用往回找: §1.4(那个 312 的拐点)、§2.6(约束能传多远)、 §2.8(把讲过的算子全放回一根轴)、§4.1(互联的边界)、 §5.5(推理引擎的移植难点)、§5.6(AOT 不占卡先算 OOM)。

—— 图不改是故意的:图是 L300 的产物,这一版一个字都不改它, 否则同一张图会有两个版本,改了一边另一边就悄悄漂掉。

第 1 节

先定那条分界线:两边的胃口一样大

这一节只做一件事:把一个数算出来。 那个数是这台机器上「算」和「搬」的兑换比 —— 搬一个字节要花一段时间,那段时间里这台机器能做多少次运算。 有了它,后面每遇到一个算子就拿自己的比值对一眼。低于它,带宽说了算;高于它,才轮到算力。 这一节不给任何结论。

先想三十秒 规格表上其实只有两个有用的数:每秒能算多少次每秒能搬多少字节
这两个数怎么组合,才能变成一句对具体算子有用的话?
—— 提示:你要的不是「哪个大」,是一句能拿去判某个算子的话。

1.1 ⭐ 算力除以带宽:两家算出来是同一个数

两边的官方规格表都给了两个数:每秒能算多少次,每秒能搬多少字节。除一下。 这个比值的意思很实在 —— 搬一个字节要花多少时间,那段时间里这块硬件能算几次所以它是一条最低要求:算法每搬一个字节,配套得算够这么多次,才刚好把算力喂饱。

两家公司、两套架构、两种设计哲学 —— 算力除以带宽,落在同一个数上 全部由两张官方规格表现算,推导链写在图里 TPU v7(每 chip) BF16 2,307 TFLOPS 7.37 TB/s 312.9 FLOP / byte FP8 4,614 TFLOPS 7.37 TB/s 625.8 FLOP / byte 来源:Google Cloud 官方 TPU7x 规格表,直接给的就是 per chip GB200(NVL72 里那颗 · 每 GPU · dense) BF16 2,500 TFLOPS 8.0 TB/s 312.5 FLOP / byte FP8 5,000 TFLOPS 8.0 TB/s 625.0 FLOP / byte 来源:NVIDIA 官方 GB200 NVL72 域级数字 ÷ 72,稀疏值取一半得 dense 这个数到底是什么意思 · 它是硬件的「胃口」:从 HBM 搬一个字节要花一段时间,那段时间里这块硬件能做约 312 次 BF16 运算 · 所以算法必须让每搬一个字节配套算够 312 次,才刚好喂得饱;算不够,多出来的算力就在空等 · 算子那头正好是同一个单位:它每搬一个字节实际算了几次 —— 这就是它的算术强度 · 于是每个算子只剩一个问题:它的算术强度比 312 高还是低?高就是算力受限,低就是带宽受限 · 专题一那八步里,① embedding 的算术强度是 0 —— 它根本不在这根轴上(完整版 L300 §2.8 收全)。这就是第 2 节要处理的东西 · 而 ④ Dense MLP 的大矩阵乘能远高于 312 —— 那是第 3 节的战场。同一颗芯片,两种完全不同的困境 ⚠️ 但这条线不止一条 —— 每往计算靠近一层,它就往下掉一截 同一个除法,在每一层各做一次。下面三个数都只对 GB200 那一颗成立,推导链写在每一行上 HBM3e 片外 2,500 TFLOPS ÷ 8.0 TB/s 312 L2 硬件自动管 2,500 TFLOPS ÷ 21 TB/s 119 共享内存 / L1 软件显式管 8,192 FLOP ÷ 128 B (都按每 SM 每周期) 64 FLOP / byte —— 每一层各自的门槛。条越短门槛越低,算子越容易在这一层变成算力受限 ⭐ 从 HBM 爬到共享内存,这条线只降了约 5 倍 —— 片上不是无限快的 算子融合减掉的是 HBM 那一层的搬运;可那批数据总得在片上落脚 —— 账没有消失,它挪到了下面这条线上 所以融合之后要拿新的强度去跟 64 比,不能再跟 312 比 —— 融合做过头,会在新的这条线上重新撞墙 ⛔ 只画了 GB200 一侧:TPU 的 VMEM 带宽官方没公开,那条线存在但给不出数。 64 的出处:分子 8,192 FLOP/周期/SM 见图 G-2;分母 128 B/周期/SM 有三处一致的公开测量(Hopper 微基准实测 127.9 · SemiAnalysis · Chips and Cheese),硬件上是 32 bank × 4 B。 ⚠️ 三条线口径不同:上两条是整颗芯片(2,500 TFLOPS ÷ 全片带宽),第三条是单个 SM 每周期。换算过去比值不变 —— 148 和时钟同时出现在分子分母,约掉了,这是恒等不是近似 反推校验:2,500 ÷ 64 = 39 TB/s 全片,再 ÷(148 SM × 128 B)得时钟 ≈ 2.06 GHz,与 B200 对得上。 ⭐ 真正的差别不在算法,在可达性:HBM 那 8 TB/s 是 148 个 SM 抢的,共享内存这 128 B/周期是每个 SM 自己的。
这一节的全部产出就在这张图上:一个数,和一个算它的方法。方法比数值值钱 —— 数会随硬件换代变,方法不会。
⭐ 下半段那三条线先记着:后面判断「融合是不是做过头了」,靠的就是换个分母再除一次。

顺带答一个每次都会被问到的问题:两边同样是 8 颗 24 GB 的 HBM3E, 带宽为什么不一样?
—— 因为「HBM3E」只说了代次,没说档位。 每颗 stack 都是 1024 根数据线,带宽 = 1024 × 每根引脚的速率; 而这一代的引脚速率从 8.0 到 9.6 Gbps 都算 HBM3E反推一下:v7 跑的是 7.2 Gbps(7.37 TB/s ÷ 8 颗,算术上分毫不差 ——  但要注意 7.2 本身也在上面那个 8.0–9.6 的区间之外,见下面的折叠), GB200 跑的是 7.8 上下
用哪一档是买方挑的 —— 按功耗、散热、可靠性、供应挑, 不是芯片能力的上限。 (为什么不顶格跑、以及这件事怎么把本课自己的数审出一个洞,看下面。)

⭐ 同样是 8 颗 24 GB 的 HBM3E,为什么一边 7.37 一边 8.0?—— 拆到引脚上算,顺便审出本课自己一个洞

两边的 HBM 配置确实一模一样:都是 8 颗 HBM3E、每颗 24 GB、 合计 192 GB。(v7 那个「八颗」出自 Google Cloud 的 Ironwood 技术博客; B200 那个出自 NVIDIA 的 Blackwell 架构页。)

那带宽为什么不一样?—— 因为「HBM3E」不是一个速度,是一个速度区间。 每颗 stack 都是 1024 根数据线,带宽 = 1024 × 每根引脚的速率。 而 HBM3E 的引脚速率从 8.0 一路到 9.6 Gbps 都算 HBM3E (Micron 标「>9.2 Gbps、>1.2 TB/s」,Rambus 标「9.6 Gb/s、1,229 GB/s」)所以「同样的 HBM3E」推不出「同样的带宽」—— 它只说了代次,没说档位。

反推一下两边各跑在哪一档:

  • TPU v7:官方 7.37 TB/s ÷ 8 颗 = 921.6 GB/s /颗 ——  1024 bit × 7.2 Gbps ÷ 8 = 921.6算术上正好是 7.2 Gbps,分毫不差(1024 × 7.2 ÷ 8 = 921.6, × 8 颗 = 7,372.8 GB/s)。
    ⚠️ 但 7.2 落在上面那个 8.0–9.6 的区间之外, 跟 GB200 那个 7.81 是同一类事:两个反推值都不是这一代的标称档, 说明官方那个带宽数背后有未写明的降频或口径。 两边要同等对待,不能只按住一个。
  • GB200:官方 8 TB/s ÷ 8 颗 = 1,000 GB/s /颗 ——  反推引脚速率是 7.81 Gbps⚠️ 这不是任何一档标称值。
⛔ 于是本课那句「相对差万分之三」不能再当卖点了 那个 7.81 说明「8 TB/s」这个数带着一个官方没写出来的口径, 而它有两种读法,两种都说得通:
· 十进制:厂商跑的是降频 bin,报的就是 8,000 GB/s (厂商给 HBM 降频是常规操作,不奇怪)
· 二进制:实际是 8.0 Gbps 那一档, 8 × 1024 GB/s = 8,192 GB/s,对外取整写成「8」
两种读法本身差 2.4%。算出来: 2,500 ÷ 8.000 = 312.5,而 2,500 ÷ 8.192 = 305.2
—— 「312.9 对 312.5」那第三位,整个落在这 2.4% 的不确定里。

结论不变,精度作废:两边的兑换比确实在同一量级 (差百分之几),这一节的产出照旧。但「差万分之三」是个假精度,不要拿它当卖点。
⭐⭐ 顺带说一句这件事最好笑也最有用的地方: 「这个数是二进制还是十进制」正是第 6 节要教的那四句问法之一 ——  它现在打在本课自己的招牌数字上。 那四句问法不是拿来审别人的材料的,是先拿来审自己的。
⭐ 那为什么不顶到最高档跑?—— 四条,从硬到软 总的一句:HBM 这一族的哲学是「宽而不快」 ——  一颗 stack 一千零二十四根线,有了这个宽度,每根线就不需要跑很快。 所以速率对 HBM 是个可调的旋钮,不是必须顶满的指标。

① 功耗。I/O 那部分的动态功耗基本随数据率走, 而且往往还要抬工作电压。在一个七八百瓦以上的封装里, HBM 不是零头 —— 顶格跑买来的带宽,要从计算那边的预算里扣。
② 热,而且这是 HBM 最要命的一条。 3D 堆叠,最底下那片 die 离散热器最远,热出不去。 而 DRAM 本身怕热:温度一高,刷新就得更频繁 ——  刷新既吃电,又要占总线时间。 结果很反直觉:按最高档买回来,跑热了降频,实到带宽可能还不如稳稳跑低一档。
③ ⭐ 可靠性 —— 这一条正好接上第 0 节那个「简单是不是更稳」的问题。 按 hyperscaler 的数据,HBM 故障是 GPU 故障的头号原因, 比数据中心里其它芯片都更频繁。(SemiAnalysis 引,未给具体数字。) 在一个九千多颗芯片的 pod 里,单颗的失败率乘上规模就是训练中断率 ——  降一档速率换的是整个集群的可用时间,这笔账很好算。
④ 供应与系统平衡。顶格 bin 又贵又少,几十万颗的量能不能稳定拿到本身就是约束; 而且计算侧要是本来就喂不满,多买的带宽只是多花电

所以你那句话是对的:用哪一档是买方定的。 JEDEC 定这一代的上限,DRAM 厂按能稳定跑到多少分档, 而 NVIDIA / Google 在各自的功耗、热、可靠性、供应约束下挑一个点, 挑完写进规格表。
—— 所以 v7 跑 7.2、GB200 跑 7.8 上下,不是能力差距,是两次不同的取舍。 这也正是开场那句「参数表是我们第一个要拆穿的东西」最干净的一个例子: 规格表上那个带宽是一次取舍的结果,不是一个物理常数。
⭐ 顺带答第二问:那 8 TB/s 是读写各一份,还是读写共用? 共用。HBM 的数据线(DQ)是双向的 ——  同一批引脚这一拍送读、下一拍送写,不是全双工, 而且换方向还要付总线掉头的开销。 (HBM 一直沿用并行双向总线,这一点在 HBM 的架构综述里是共识; 业界也直接称它为 half-duplex bus。)

这一条对本课有两个直接后果,都值得记:
① 本课算算术强度时把读和写加在一起数,口径是对的。 向量加那 6 个字节 = 读 a 2 + 读 b 2 + 写 y 2 ——  因为它们花的就是同一份带宽。分母和分子的口径必须一致,这里一致。
② 它解释了融合为什么这么值钱。中间结果写回 HBM、再读回来, 这一去一回吃的是同一份带宽,等于收两次费融合把这两笔一起免掉 —— 所以省下的不是「一半」,是「两趟」。

再往下滚一屏,图还没完。这个除法每一层都能各做一次 —— 而这门课的主线干的正是把数据从一层挪到另一层。 现在只要留个印象:越靠近计算,这条线越低。本节最后讲融合时它才落地。

这条线怎么用 —— 同一道除法,当场量两个,再看融合动了哪一半 算法只有一句话: 数它搬几个字节 · 数它算几次 · 除一下 —— 得到的就是这个算子的算术强度;跟 1.1 那张规格表算出来的 312 比大小,就知道它卡在哪一边。 ⭐ 这句比 312 本身值钱 —— 312 会随硬件换代变,这个算法不会。下面三栏就是它用三次的样子。 ① 向量加 y = a + b bf16 读 a 2 字节 读 b 2 字节 写 y 2 字节 6 字节 1 个 FLOP 强度 = 1 ÷ 6 = 0.17 比 312 低了 一千八百多倍 跑满 GB200 那 8 TB/s,也只有 1.3 TFLOPS 峰值 2,500 —— 用掉万分之五 买更贵的卡能让它变快吗? 不能。它压根不在算力那条路上。 ⭐ 这就是「带宽受限」的样子:分母大得离谱,   分子小得可怜。全世界的算力都救不了它。 ② 方阵乘 N × N × N bf16 2N³ 个 FLOP 6N² 字节(三个矩阵进出) 约一下,强度 = N ÷ 3 —— 只跟边长有关 N = 256 85 还在带宽那一侧 N ≈ 1,000 312 这才刚够到线上 「矩阵乘=算力受限」是个错觉。 它得够大才算数。 ⭐ 两栏合起来才是这条线的用法:   不看算子叫什么名字,只看它的除法结果。 ③ 那怎么救 —— 只有一条路:把分母做小 把连着的几个算子合成一个 kernel, 让中间结果不落 HBM —— 这就是算子融合 ⭐ 它一个 FLOP 都没省:分子分毫不动,   变的只有分母。 312 带宽受限 算力受限 融合把它往右推 ⚠️ 但「不落 HBM」不等于「不落地」 它被塞进片上暂存,而片上有自己的线: 312 64 门槛只降 4.9 倍 片上流量却能涨几十倍 ⭐ 所以融合做过头会在新的这条线上撞墙 ——    不是门槛变高,是分母变大。 接下来每一节都在这条线上 §2 融合前后差一千倍 —— 这条线的主例 §3 同一个融合,两块硬件上各是什么样 §5 融合这件事,该由谁来决定 ⛔ 图上每个数都能当场复核:6 = 2+2+2;312 ÷ 0.167 = 1,872;8e12 ÷ 6 = 1.33 TFLOPS,占 2,500 的 0.053%   2N³ ÷ 6N² = N/3,N/3 = 312 → N = 937,N = 256 → 85.3;312 ÷ 64 = 4.9
⭐ 同一道除法用三次:向量加 0.17、方阵乘 N ÷ 3, 然后融合只把分母做小、把点往右推 —— 分子一个 FLOP 都没动。 第 ③ 栏那一小段轴是全图唯一的轴:它要说的不是「谁在哪」, 而是「往哪边动」。右下角那两个数是这一节最容易被跳过的一句 ——  片上换成 64 那条线,门槛只降 4.9 倍,而片上流量能涨几十倍, 撞墙不是因为门槛变高,是因为分母变大
一次向量加,从头到尾 —— 全程只有一下是「算」,其余全是「搬」 ⭐ 这就是 0.17 长的样子。同一条通路上跑矩阵乘是什么样,见「一个数走完全程」那张(图 T-6)—— 通路没变,忙的人换了 a(2 字节) b(2 字节) y = a+b(2 字节) 唯一一次「算」 ① HBM 片外主存 a 和 b 原本待在这儿 算完 y 也要写回这儿 ⭐ 这一层的兑换比是 312 ② VMEM 片上暂存(不是缓存) 编译器插一条 DMA 搬上来 ⛔ 搬多少、什么时候搬,   编译期就写死了(见 T-6) ③ 向量寄存器 VPU 的输入端 一条向量指令吃 8 × 128 个元素 a 一份、b 一份,各占一个 ④ VPU 向量单元 做那一次加法 ⭐ 整张图里,红一下的只有这里 DMA 载入 喂入 灰色那条是回程:结果 → VMEM → HBM,同一套 DMA、同样是编译期排好的—— 回程也要占带宽,写 y 那 2 个字节一样算在 6 里面
这一轮的账 读 a 2 字节 读 b 2 字节 算  1 个 FLOP 写 y 2 字节 合计:搬 6 字节,算 1 次 → 强度 0.17 看这一轮里「算」占了多少 四个站台走一遍,红框只闪一次。搬 6 个字节换来 1 次加法 —— 这台机器每搬一个字节,本来能算 312 次,现在只算了 0.17 次。 所以「带宽受限」不是慢,是闲—— 算力一直在等数据到位。把这样的算子一个个接起来还各写一趟 HBM,就是第 2 节要治的病。 ⚠️ 示意图:时间比例不代表真实耗时,只表示先后。真实比例(搬 6 字节 vs 算 1 次)没法在同一屏里按比例演 —— 能演的是次序,不是快慢。 图上每个数都能当场复核:bf16 每元素 2 字节 → 读 a 2 + 读 b 2 + 写 y 2 = 6;y = a+b 是 1 次加法 = 1 个 FLOP(⛔ 不是乘加);1 ÷ 6 = 0.167 ≈ 0.17;312 ÷ 0.167 = 1,872 倍。 ⭐ 这张图的形式借自 How To Scale Your Model 的 pointwise-product 动图(jax-ml.github.io/scaling-book,MIT)—— 站台词汇与配色沿用本课 T-6,内容换成本节那个 0.17 的例子。
图 1-7 一次向量加,从头到尾。四个站台走一遍, 红框只闪一次 —— 搬 6 个字节,只换来 1 次加法。 这就是 0.17 长的样子。同一条通路上跑矩阵乘是什么样,见 「一个数走完全程」那张(图 T-6)—— 通路没变,忙的人换了
⚠️ 时间比例是示意,不代表真实耗时 —— 能演的是次序,不是快慢

这张图把这条线用了三次。两个例子特意避开注意力 ——  那一笔第 2 节第一张图就要完整推一遍,在这儿先算等于剧透。

1.2方阵只是特例 —— 强度由最小的那个维度说了算

上面那两个例子里的矩阵乘是方阵,三边都是 N,所以约出来是 N/3。 但真实的矩阵乘是 M×K · K×N,三个维度不一样长。 那强度到底跟哪一个有关?

还是同一道除法,只是这次别急着代数:算的次数是 2·M·N·K, 搬的字节是 2·(MK + KN + MN)。约完是一个很干净的形式 ——  1/强度 = 1/M + 1/N + 1/K

倒数相加,意味着谁最小谁说了算。 所以「K 越长强度越高」是对的 —— 但它有天花板: K 拉到无穷,强度也只到 MN/(M+N)K 救不了 M 或 N 太小。
⛔ 最狠的一个特例:batch = 1 的 decode,M = 1, 于是 1/强度 ≥ 1 —— 强度上限就是 1,K 和 N 再大都没用。 这就是 LLM 解码怎么都卡在带宽上的那一行算术。

把注意力代进去:QKT 的两个外维都是序列长 S, 而收缩维就是 head_dim。多数模型 head_dim = 128,于是强度 ≈ 128 —— 低于 v7 那条 312,也低于 v6e 那条 560。

强度到底由哪个维度决定 —— 是最小的那个,而注意力里最小的就是 head_dim §1 只算了方阵(N/3)。真实的矩阵乘是 M×K · K×N —— 三个维度各有各的作用,而它们的作用方式是「倒数相加」。 算 2·M·N·K ÷ 搬 2·(MK+KN+MN) ⟹ 1/强度 = 1/M + 1/N + 1/K (M=N=K 时退化成 N/3,正是 §1 那个例子) ① 三个维度,倒数相加 M×K · K×N,bf16,A/B/C 各过一次 算:2·M·N·K 搬:2·(MK + KN + MN) 字节 ⟹ 1/I = 1/M + 1/N + 1/K 方阵 M=N=K:I = N/3 边长 256 → I = 85.3(§1 念过的 85) 倒数相加 ⟹ 谁最小谁说了算。 强度永远被三个维度里最小的那个卡住。 ② K 越长强度越高 —— 但有天花板 固定 M = N = 512,只拉长 K: K = 64 → I = 51.2 K = 128 → I = 85.3 K = 512 → I = 170.7 K = 4,096 → I = 240.9 K → ∞   → I → 256 上限 = MN/(M+N) = 256,拉不动了 ⛔ 推论:batch=1 的 decode,M = 1, 所以 I ≤ 1 —— K 和 N 再大都救不了。 ③ 注意力:收缩维就是 head_dim 每个头,S = 75,600,d = 128 QKT:M=S, N=S, K=d=128  → I = 127.6 PV:M=S, K=S, N=d=128  → I = 127.6(同样被 128 卡住) 对照:v7 屋脊 312 v6e 屋脊 560 128 < 312 < 560 —— 不做 Flash 的话, 注意力这两个矩阵乘落在带宽那一侧。 ⚠️ 口径:「注意力的强度」有两个都对的答案,分母不一样。 本图 ≈128 是单个矩阵乘孤立看(A/B/C 各过一次);§2 图 2-4 那个 64 是朴素注意力整体看(中间矩阵 S 和 P 各走一个来回,分母多一倍)。 那 Flash 到底改了什么 —— 以及为什么改完了还是喂不满 Flash 换的是分母,不是分子:中间那个 S×S 不再落 HBM,过 HBM 的只剩 Q/K/V/O = 4·S·d·2 B,   于是 I = 4S²d ÷ 8Sd = S/2 = 37,800(S=75,600)。一个 FLOP 都没省,还因为重算多算了一点。→ §2 同一个 128 咬了两口收缩维 128 → 强度上不去(本图);MXU 是 256 见方,只喂进 128 → 一半空着(§3.4 / §3.6)。   前者是「要不要等数据」,后者是「算的时候算得满不满」—— 两件事,同一个数字。
图 1-8 强度由三个维度共同决定,倒数相加 —— 注意力里最小的那个就是 head_dim;同一个 128 在「强度」和「喂不喂得满 MXU」两个层面各咬一口。

—— 所以「注意力是矩阵乘,当然算力受限」是个错觉。 不做融合的话,它落在带宽那一侧,而卡住它的正是 head_dim。
那 Flash 改的是什么?下一节第一张图会给出完整那一笔; 这里先记住方向:它一个 FLOP 都没省,改的是分母。
⭐ 而同一个 128 还会再咬一口:MXU 是 256 见方, 只喂进 128 —— 那是 §3.4 / §3.6 的事。 强度问的是「要不要等数据」,喂满问的是「算的时候算得满不满」。

—— 这一节到此结束,你手上多了一把尺子。 它只回答一个问题:这个算子卡在带宽还是卡在算力。
下一节把它用在一个真算子上 ——  而那个算子会给出全课第一个大数:融合前后,强度差一千倍。

同一道除法,把分母换成卡间带宽 —— 算出来是 5,768 (这条线换个分母就能判「一张卡该喂多少 token」,展开看)

这一节这把尺子不只能判「算子卡在哪」。分子还是算力, 分母换成卡间网络带宽,同一道除法出来的就是另一个问题的答案: 每张卡每步至少要处理多少 token,跨卡通信才追得上计算。

算给 TPU v7 看(两个数都在本课 T-1 那张图上):
2,307 TFLOP/s ÷ 400 GB/s(单轴双向)≈ 5,768
三个轴一起用,带宽三倍,门槛降到 ≈ 1,922。 铺满一个 9,216 颗的域,全局批次得到 约 1,770 万 token 才不被通信拖累 —— 这就是大模型训练用超大批次的原因, 不是为了收敛,是为了让网络别拖后腿。

⚠️ 分母的口径是这里最容易错的一处。上面这个数取的是 单轴双向 400 GB/s(六条链路 1,200 ÷ 3 个轴), 不是整颗芯片的 1,200。换个口径,答案差三倍 ——  而两种写法在纸面上都读得通。

所以这一节真正的产出是那道除法,不是 312。 分母换成 HBM 带宽,判的是算子;换成卡间带宽,判的是并行策略。 后一半是专题五的事,这里只演示一次「同一把尺子,量另一样东西」。

片上那块存储到底是什么,以及「那为什么不干脆留在寄存器里」(两个追问,展开看)

被塞进去的地方有名字:GPU 上是那块最多 227 KiB 的共享内存, TPU 上是 VMEM。HBM 那一层确实少搬了,但那批数据总得在片上落脚 ——  而那儿的兑换比是 64 不是 312(推导链在 1.1 那张图的下半段)。

那为什么不干脆留在寄存器里?一个 SM 的寄存器堆总共 256 KiB, 跟共享内存一边大;而且你多占一份,能待命的 warp 就少一批。 大多数时候真做不到 —— 这笔账在 3.2 那张 SM 拆解图上。

那 312 的分子为什么只取矩阵单元?CUDA Core 和 VPU 就不算了吗

问得对 —— 312 是「矩阵那条 roofline」的屋脊点,分子取的是 Tensor Core/MXU 的峰值,向量单元确实没算进去。

本课这么取,因为要判的算子 FLOP 几乎全在矩阵乘上 (注意力的两次矩阵乘、MLP 的大矩阵乘)。softmax、归一化那些向量运算 占的 FLOP 是零头。

向量单元到底小多少 —— 算一下:
GB200:18,944 个 CUDA Core × 2 FLOP/周期 × ≈2.06 GHz ≈ 78 TFLOPS
对上矩阵那边的 2,500,差 32 倍。 ⚠️ 那个时钟是推出来的:NVIDIA 没公布 GB200 的 boost 频率, 这里用 2,500 TFLOPS ÷ (606,208 次乘加/拍 × 2 FLOP) ≈ 2.06 GHz 606,208 = 148 SM × 4 个 Tensor Core × 1,024 次乘加/拍, 这条账 3.7 整节拆开 —— 这里先用结论。2026-09-05 补: 原来这里只有一个光秃秃的 606,208,而它的出处在两千多行之后。) ——  本课统一用这个数(此前另有一处按 1.83 外推得 2.03,已废; 1.83 是 HGX B200 的第三方数,它自己对 2,250 就差 1.4%)。
TPU 侧结构上同理:一个 TensorCore 的两个 MXU 合起来一拍 262,144 次乘加(每个 MXU 131,072), VPU 一拍处理 1,024 个元素 —— 两个数不是同一个单位, 但量级差在两位数以上

所以严格讲,判 element-wise 算子该用向量那条 roofline: 屋脊点是 77 ÷ 8 ≈ 9.6,不是 312。

⛔ 两条屋顶线是分开的,不能把两个峰值加起来画成一条。 加起来等于假设同一批 FLOP 能同时喂饱两个单元 ——  而一个 element-wise kernel 压根用不上 MXU,一次矩阵乘也不会去占向量单元。 正确做法是两条线都画,看算子实际落在哪个单元上,就用哪条判
存储那一侧同理:HBM、L2、片上 SRAM 各有各的带宽, 严谨的 roofline 是一族线,不是一条。本课只用 HBM 那条 ——  因为我们要判的问题只有一个:这个中间结果要不要落地
⚠️ 还有一条口径别搞错:矩阵乘一次乘加(MAC)算 2 个 FLOP(一乘一加), 厂商标的峰值就是按这个口径来的。所以 N×N×N 是 2N³ 不是 N³ ——  上面「强度 = N ÷ 3」正是这么约出来的。

但结论不翻。向量加的强度 0.17,比 9.6 还低 58 倍,照样卡在带宽上。 换个说法:那 1.3 TFLOPS 是矩阵峰值的 0.05%、向量峰值的 1.7% ——  换哪个分母都惨,只是后者才是公平的比法。

真正该带走的是这件事本身:向量单元弱不是疏忽,是两边共同的设计选择。 一旦你的算子落到向量单元上,你输了两次 ——  一次是带宽受限,一次是就算不受带宽限制,可用算力也只剩三十分之一。
—— 这就是「必须做算子融合」的根本原因: 融合不是为了少算,是为了别让数据在这条又窄又弱的通路上来回跑。

⚠️ 片上 SRAM 看着属于「只差个百分之几」那一类,其实不是 —— 一个很容易犯的误判

按总量是 GPU 231 MiB 对 TPU 134 MiB,多 73% ——  数字在一个量级,看着就该归进「不值得记」那一堆。但不能这么归。 GPU 领先的那部分几乎全是 L2,而 L2 正是 TPU 整层没有的东西: 差的不是容量,是这块地由谁说了算完整的三个口径在 3.2b。

完整十行对照表 —— 回查用的手册(含表上 * § 四条脚注的出处说明)
TPU v7GB200 / B200差别的本质
矩阵单元MXU 256 × 256,每核 2 个 Tensor Core(第 5 代),每 SM 4 个粒度:一块大的 vs 很多小的
标量 / 地址每核只有一个标量单元 每 SM 128 个 CUDA core,148 个 SM*不规则访问谁来发地址(第 3 节)
片上 · 显式VMEM 64 MiB / 核 · 编译器管 shared memory 最多 228 KB / SM · 人管谁决定块开多大(第 2 节)
片上 · 自动CMEM = 0(没有) L1 + L2 126 MB⭐ 整整一层只有一边有
可编程协处理器SparseCore × 4 / chip
跑自己的程序:前缀和 · 排序 · 计数 · gather / scatter
没有可编程的
TMA 是固定功能搬运,不是这一类
⭐ 另一层只有另一边有
零碎活的小单元 XLU 跨 lane 单元 × 2
转置 · 跨 lane 归约 · shuffle。超越函数(exp / tanh是单独一类资源§ —— Pallas 的成本模型里 transcendentalsflops 平级单开一栏;但部件叫什么、几个,公开资料没有
SFU × 4 / 处理块
exp · rcp · rsqrt 等超越函数。跨 lane 靠 warp shuffle 指令,官方没单列成部件
⚠️ 两边各点名了一个,又各自不说另一个
HBM96 GiB / device(= 192 GiB ÷ 2);整 chip 7,372.8 GB/s(= 7.37 TB/s) 186 GB / GPU(软件可见;物理 192 GB);8,000 GB/s同一代 HBM,量级相同
算力(每 chip / 每 GPU)BF16 2,307 | FP8 4,614 TFLOPS BF16 2,500 | FP8 5,000 TFLOPS(dense,NVL72 里那颗)同一量级
算力 ÷ 带宽312.9 FLOP/byte 312.5 FLOP/byte⭐⭐ 几乎完全相同
卡间ICI 1,200 GB/s 双向 · 3D torus · 最大 9,216 chip NVLink 1.8 TB/s / GPU · NVL72 域 · 72 卡(本课编排口径按 64) 出了域换不换协议(第 4 节)

* 148 个 SM 来自第三方拆解,NVIDIA 官方规格表 没有公布这一项。表里其余数字都出自两家的官方文档。
XLU 的个数出自 Google 公开的 How to Scale Your Modeljax-ml.github.io/scaling-book)—— 原文是「一个标量核管着一个 VPU、 4 个 MXU、2 个 XLU 和多个 DMA 引擎」。⚠️ 同一句里的「4 个 MXU」对不上 v7 (v7 是 2 个 256×256),说明那句没有标代次,所以这个 2 只当「有不止一个」用。
SFU 存在是 NVIDIA 官方明写的(Blackwell SM 构成里列了 Special Function Units),但个数官方框图没标,这里的 ×4 是沿用 Hopper 的画法。

§ 这一格值得单独讲,因为它是全课少见的「证据分三级」的例子。 「一颗专算神经网络的芯片,总得有块硬件负责 exp / tanh / sin」—— 这个直觉是对的,但能公开证明到哪一步,跟直觉走得多远,是两回事

① 曾经有过,白纸黑字。初代 TPU 的框图里就有一个独立的 Activation 单元,2017 年那篇论文的正文也提到芯片上有一块 「nonlinear function logic」。所以这类硬件在 TPU 上不是新鲜事。
② 到今天它仍然是「单独一类资源」,这条有正面证据。开源 JAX 里 Pallas 给 TPU kernel 报成本的接口是 CostEstimate(flops, transcendentals, bytes_accessed, …) ——  transcendentalsflops 平级,单开一栏。 这一点很说明问题:如果超越函数只是「用普通 ALU 多跑几条指令」, 它就该被算进 flops 里,没有理由单开一栏计数。
③ 但部件叫什么、有几个、怎么实现,公开资料确实没有。 TPU v4 的 ISCA 论文对一个 TensorCore 的完整描述只有「四个 128×128 MXU, 一个带 128 条 lane、每 lane 16 个 ALU 的 VPU,以及 16 MiB VMEM」——  那 16 个 ALU 里有没有专算超越函数的,论文没拆。Google Cloud 的架构文档、 JAX 官方的 Pallas 硬件参考页、JAX 里那张机器可读的芯片规格表, 三处都只列 MXU / 向量单元 / 标量单元 / SparseCore。 至于 NVIDIA SFU 那套「查表 + 插值」的实现方式,TPU 侧也找不到公开说明。
⚠️ 最后一句最重要:规格表「没有列出」,不等于芯片上「没有」。 现成的反例就在同一批文档里 —— XLU 也没被任何官方规格表列过, 而它确实存在(它是从另一份公开材料里查到的)。规格表只列它想让你据以选型的东西。 所以本课这一格写的是「是单独一类资源,但部件未公开」既不写「没有」,也不写我拿不出公开出处的细节

第 2 节

⭐ 就是从这儿开始不同:同一次取数,一边路上 4 个决定,一边 0 个

上一节说两边胃口一样大。这一节说吃法完全不同
而「不同」不是指少了哪个零件 ——  是同一条取数的路上,一边有四个「跑起来才知道」的决定,另一边一个都没有。
cache 只是那四个里的两个;第一个决定是 warp 那三十二个地址能不能合并, 最后一个是没命中该换出谁。这门课后面所有的差别,都是从这一处长出来的。

先想三十秒 从显存取一块数进片上,这条路上会发生一串事。
哪些是编译的时候就能定死的?哪些非得等到跑起来才知道
—— 先自己数一数「跑起来才知道」的那一类有几个。下面两边各走一遍。

2.1 同一次访存,两条路

不看参数,只跟着一次取数走一遍:从 HBM 里拿一块数进片上, 这一路要做几个决定,每个决定是谁做的、什么时候做的。

同一次访存,两条路 —— 数一数路上有几个「运行时决策点」 这是第 0 节那两种出身第一次变成具体机制 GPU:运行时适应 硬件替你决定,你写得随意也能跑 TPU:编译期安排 运行时零决策,全部提前算好 一个 warp 的 32 个线程各给出一个地址 这些地址能合并成几条 cache line? ◆ 决策点 · coalescing ⇢ 落在同一个 bank 上吗? 支路 · bank conflict ⚠️ bank 是 shared memory 的分区机制,不在 global load 这条路上 L1 命中吗?(与 shared 合计 256 KB/SM) ◆ 决策点 · 命中 / 未命中 L2 命中吗?(126 MB,全 GPU 共享) ◆ 决策点 · 命中 / 未命中 换出谁? ◆ 决策点 · 替换策略 HBM 编译期:XLA 决定每个数组切成 8 × 128 的块 ☑ 已定死 编译期:决定什么时候搬、搬多大一块 ☑ 已定死 编译期:算准 VMEM 装不装得下 ☑ 已定死 运行时:DMA 整块 HBM → VMEM(64 MiB) 运行时:向量单元直接取,形状必须已经对 (没有这一层) 硬件缓存 = 0 HBM 主路 4 个运行时决策点(+走 shared 再加 1 个) 你能做的是「让硬件更容易猜对」:访问连续、避开 bank、手工 tiling 路上 0 个运行时决策点 没有 cache,就没有命中、未命中、替换这些概念 —— 也没有补救机会 ⭐ 这不是「谁更聪明」—— 是两条路线,各自靠一样东西 GPU 靠运行时适应:形状对不对都能跑起来;代价是命中率要跑起来才知道,不好的时候只能反复试。 TPU 靠编译期算准:形状提前定死,一个决策周期都不浪费;代价是运行时没有补救手段,形状不对只能回去改代码。
同一次访存,两条路。红的是运行时才知道结果的,绿的是编译期已经定死的 —— 主路 4 个对 0 个。虚线那个 bank conflict 是支路,它属于 shared memory,不在 global load 这条路上。

先把第一个决策点说清楚,它是四个里唯一跟 cache 无关的那个
coalescing = 合并访存。一条 load 指令发出去, warp 里那 32 个线程各自算出一个地址 ——  硬件要把这 32 个地址,合并成尽可能少的内存事务。
合得好不好,差 8 倍带宽,而且只有跑起来才知道 ——  因为编译器不知道你的地址是怎么算出来的。 TPU 那边没有这件事:数组按 8 × 128 分块、编译期定死, 搬的是一整块,根本不存在「32 个地址要不要合并」这个问题。

那 8 倍是怎么算出来的(sector 那道算术,两行)

关键不是 cache line,是 sector。NVIDIA 的 cache line 是 128 字节, 但它切成 4 个 32 字节的 sector,而真正的搬运单位是 sector

最好的情况:32 个线程读连续的 4 字节 = 128 字节,正好一条 cache line —— 4 个 sector,一个字节都不浪费。

最坏的情况:32 个线程的地址跨度很大(比如按列去读一个行主序的矩阵), 每个线程落在不同的 sector —— 32 个 sector, 每个 sector 搬了 32 字节回来却只用了 4 字节
—— 32 ÷ 4 = 8 倍。带宽照付,八分之七扔掉。

所以「访问连续」是 GPU 优化的第一课,而它优化的不是延迟, 是你为这次访存实际付了多少带宽 出处:cache line 128 B / sector 32 B 是 NVIDIA 的 cache 组织方式,Nsight Compute 直接按 sectors per request 报这个数 —— 完美访问是 4,多出来的每一个都是浪费。

⭐ 全课那个决定,就是这张图上的「4 对 0」 红框的意思不是「慢」,是「跑起来才知道」。 命中没命中、要不要换页、排在谁后面 —— 这些答案在编译时都不存在。
绿的意思也不是「快」,是「写代码的时候就已经定死了」

两边的分工从这里定死:
GPU 把这几个决定交给硬件在运行时处理 —— 也就是交给了人,交给写 kernel 的那只手。
TPU 交给编译器在编译期算准 —— 也就是接受它的脾气。

到这里,§0 那个「处处留一手」才第一次不是形容词。 它就是这四个决策点 —— 每一个都是真实存在的电路这一次的地址合不合并、命不命中,它都在那儿等着被问。 「一手不留」同样不是形容词,它是编译期已经定死的那三件事。
—— 后面第 3、5 节看到的每一样东西,都是这一句的后果。

⚠️ 那 TPU 凭什么敢说「形状已经对」? 因为它做了一件更狠的事:把向量寄存器的形状和数组在内存里的布局,做成了同一个形状—— 所以搬进来就能直接算,一次重排都不用。 代价不是「一行不再连续」(那恰恰是好处:一段正好铺满 128 条 lane), 而是这个形状被钉死 —— 换最内维要真搬数据、不整除要补零、 跟外部交接要重排。这笔钱 3.3 算给你看。

2.3 主例登场:为什么是 FlashAttention

判据有了,坐标有了。现在挑一个算子把它跑到底。 挑 FlashAttention 的理由很实际:每个大模型都要跑它。 而且它正好卡在 312 那条线的错误一侧 —— 两边都必须动手。

Flash 到底 flash 在哪 —— 那个 seq × seq 的矩阵,一次都不许落到 HBM 这是本节的主例:它同时压中「访存路径」和「片上容量」两件事 先记一个数:一个头、128K 序列、bf16 的注意力矩阵 = (2¹⁷)² × 2 B = 2³⁵ B = 恰好 32 GiB —— 而一个 v7 device 总共只有 94.74 GiB 可分配,装不下 3 个头,模型有 128 个。 ① 朴素写法:中间矩阵在 HBM 里走了一个来回 算 Q · Kᵀ,得到 seq × seq 的分数矩阵 S 把 S 写回 HBM 32 GiB 写 再从 HBM 读回 S 做 softmax 32 GiB 读 把 softmax 结果 P 写回 HBM 32 GiB 写 再读 P 乘 V 32 GiB 读 一个头搬了 4 × 32 GiB,而真正的乘加只有那两次矩阵乘 ② Flash:分块,中间结果在片上生死 把 Q / K / V 按行切成块,一次只取一块 这一块的 S 只在片上出现 不落地 online softmax:只维护 running max 和 running sum 两个标量 累加进输出块,S 当场丢弃 不落地 换下一块,重复 整个 seq × seq 矩阵一次都没在 HBM 里出现过 ③ 那「一块」该多大?—— 两边问的是同一个问题,答案交给不同的人 GPU:kernel 作者自己定。预算 = shared memory 228 KB / SM, 手写 Triton / CUTLASS,块大小是可调超参 —— 调错了也能跑,只是慢。 TPU:编译器定。预算 = VMEM 64 MiB,但块的形状必须对齐 8 × 128, 对不齐就补零 —— 补出来的零照样占 VMEM、照样过 MXU。
①② 两种写法的乘加次数完全一样,差的全在搬运。③ 是同一个问题的两种交法:GPU 交给 kernel 作者,TPU 交给编译器

关键在第 ③ 栏 —— 前两栏图上已经说完了, 而「谁来安排搬运」正是上一张图上那个决定。 现在把它放回 312 那条线上,看看这笔搬运账到底有多离谱:

回到第 1 节那条 312 的线 —— Flash 换的到底是什么 这根轴在完整版 L300 的 2.8 节还会回来一次,那时上面会有五个算子 —— 这里先只放两个 同一个头(n = 128K,d = 128)算两次矩阵乘的 FLOP 一模一样,变的只有搬了多少字节 16 64 256 1024 4096 16384 65536 算术强度(FLOP / byte,对数轴) 312 —— 硬件的分水岭 左边:带宽说了算 右边:算力说了算 朴素注意力 64 Flash 65,536 朴素:每搬 1 字节只算了 64 次,而搬这 1 字节的时间里硬件能算 312 次 —— 差近 5 倍,算力空等,卡在带宽上 Flash:同样的 FLOP,搬的字节少了 1,024 倍 —— 跨到右边,卡在算力上(这才是该卡的地方) 这两个数是算出来的,推导链在这儿 两次矩阵乘的 FLOP(两边相同):2 × (2 · n² · d) = 2 × 2 × (2¹⁷)² × 128 = 8,796,093,022,208 FLOP 朴素搬的字节:S 和 P 各写一次读一次 = 4 × n² × 2 B = 137,438,953,472 B → 强度 = 64 Flash 搬的字节:Q / K / V / O 各过一次 = 4 × n × d × 2 B = 134,217,728 B → 强度 = 65,536 ⚠️ 这是理想上界:真实 Flash 会重复读 K / V,强度到不了 65,536。但结论不变 —— 它是从 312 的左边跨到了右边。
朴素实现每算 1 FLOP 就得搬 1/64 byte,而硬件只供得起 1/312 —— 需求比供给多了近 5 倍。所以 Flash 的价值不是「优化了 kernel」,是把这个算子从分水岭的左边换到了右边

—— 所以必须分块,两边都得改写。剩下的整门课, 就是看这个「改写」在两边分别长什么样。

⭐ 请把这两个数并排看一眼:同一个注意力,FLOP 一模一样, 一次乘加都不多不少,算术强度差一千零二十四倍。
—— 所以算术强度不只由数学决定,也由你怎么实现决定 这句话是这门课后面所有优化动作的许可证: 如果强度是算子的固有属性,那唯一能做的就是换硬件; 正因为它由实现决定,改写才有意义。

⭐ 那它过了片上那道门槛吗 —— 同一道除法,换个分母再做一遍 (答案:取决于块开多大 —— 块行数 Br 得 ≥ 64。展开看这道除法怎么做的)
刚才那句「跨到右边」,跨过的是 HBM 那条线(312)
可片上还有一条:共享内存是 64 —— §1 那张阶梯图的最后一格。
⚠️ 别跟上面那个 64 搞混:轴上那个 64 是朴素注意力的强度, 这里这个 64 是共享内存那一层的门槛一个是算子的,一个是硬件的 —— 数字撞上纯属巧合。
融合做的事,是把字节从 HBM 挪到片上。那就得拿新的分母,再除一次。

设块的行数 Br、列数 Bc,head_dim 是 d
每走一步,从共享内存读进 K 块和 V 块,各 Bc × d; Q 那一块一直待在寄存器里不动。
:两次矩阵乘,4 × Br × Bc × d 次。
4 × Bc × d 字节(bf16)。
约一下 —— 片上强度 ≈ Br,就是块的行数。

Br = 128:128 > 64,过关
Br = 32:32 < 64,在片上重新变成带宽受限
—— 这就是 §1 那句「为什么还会撞墙」的具体答案:撞不撞,取决于块开多大。

外部锚点:真实的 FlashAttention kernel,块行数一律取 64 或 128。 不是巧合 —— 是这道除法逼出来的。

⚠️ 这笔账只数了共享内存那一层。softmax 的向量运算、寄存器那一层的来回,都没算进去。 它给的是量级,不是精确值。

TPU 那一侧算法完全一样:分子取 1,153.5 (per core —— VMEM 是 64 MiB / core,分母那一侧也是 per core, 不能拿 per chip 的 2,307 去配),分母换成 VMEM 带宽 ——  而这个数官方没有公开,本课不推测。 能说的只有方向:片上带宽比 HBM 高一个量级,门槛就低一个量级, 落到几十这一档,和 GPU 那个 64 同一个数量级。 再加上 TPU 的工作台是 64 MiB(GPU 227 KiB 的 289 倍),块能开得大得多 ——  所以卡住 TPU 的不是这条线,是 3.4 那个刀宽。
⭐ 回头看一眼:刚才这就是一次算子融合 上一节那条线的用途,在这里第一次落到实处。
Flash 做的事,就是把「算分数 → softmax → 再乘 V」这一串合成一个 kernel。 那个 32 GiB 的中间矩阵因此根本不用写出去。
⭐ 而它连一次乘加都没有省 —— 图上第 ①② 栏是一样的。省的全在分母。
这就是上一节说的「把算子往右推」。只不过这一推的幅度,是一千倍
—— 所以真正的问题不是「要不要融合」,是「这个融合谁来做」。
Flash 这一次,是人写出来的。第 5 节要问的是:非得靠人吗。
那融合省下来的搬运,是不是要拿多算的 FLOP 去换?多出来的那些算「有用」吗

问得对,而且这是个有标准答案的问题。先把「多算的」分成三种 —— 它们的性质完全不一样

重算。真的把同样的运算再做一遍。Flash 的前向没有这一项 —— 上面那张图第 ①② 栏乘加次数一模一样,这是它少见的地方。 但反向有:既然中间那个矩阵没存下来,反向就得拿 Q、K 重新算一次。 省下来的存储,是用重算换的。

补零。形状对不齐时补出来的那些零 ——  它们照样占片上空间、照样过矩阵单元,但什么都没产出这是三种里最亏的一种,因为连「换来了什么」都说不上。第 3 节会看到它的量。

额外的向量活儿。online softmax 每处理一块,都要把已经累加的 结果重新缩放一次。次数上不多,但它落在向量单元上 ——  而向量单元的算力只有矩阵单元的三十几分之一(第 1 节末尾那个折叠里算过)。 数 FLOP 会低估它,数时间才看得见。

「算不算有用」—— 这个问题有正式答案,第 6 节专门讲 一句话先放着:分子只算模型数学上需要的运算,叫 MFU; 算上硬件真正执行的每一次,叫 HFU。 重算和补零在 MFU 口径里一律不算有用。
为什么这件事重要、两者能差多少、比两块硬件之前该先问什么 ——  都在第 6 节,那里是它该待的地方(那一节讲的正是 「两个数摆上桌能不能比」)。

那这笔买卖到底划不划算?——  第 1 节那条线在这里第二次派上用场,但要先把一件事说准。

算术强度的分子,是硬件实际执行的 FLOP,不是模型需要的那些。 —— 硬件不认识哪些是重算。所以判你落在 312 的哪一侧, 用的是含重算、含补零的那个强度。

但你真正要的产出是有用的 FLOP ÷ 时间。把两件事合起来, 天花板就有两块,谁低听谁的:

有用吞吐 = min( 峰值 × 有用比例 ,  带宽 × 有用强度 前一块管算力那一侧:一半的 FLOP 是重算,这一块就只剩一半。
后一块管带宽那一侧:重算不改变搬了多少字节,所以它一点忙也帮不上。
⭐ 而这两块恰好在含重算的强度 = 312 的地方交叉 —— 这就是为什么屋脊点还是那个 312,一个字都不用改。

拿一个具体的数走一遍:含重算的强度 600,其中一半是重算。
有用比例 = 1/2 → 算力这一块 = 50% 峰值; 有用强度 = 300 → 带宽这一块 = 300 ÷ 312 = 96% 峰值。 取小的 —— 你只跑到屋顶线的一半。

⚠️ 顺便纠一个很容易顺口说错的:这时候带宽并没有被用满。 既然卡在算力上,搬运就得等计算,实际用掉的带宽只有 312 ÷ 600 ≈ 52%矩阵单元 100% 忙着,一半忙在垃圾上, 而带宽在旁边闲着 —— 这就是「换过头了」的典型指纹。

⭐ 所以「用重算换搬运」该换到哪儿为止 在带宽受限区,多算的成本是零 —— 不是便宜,是, 它整个藏在等数据的时间里。一旦越过 312,成本立刻变成按比例扣产出。 中间没有过渡 —— 这是一道悬崖,不是一条汇率曲线。
所以规则只有一句:换到「含重算的强度」刚好顶到 312,就停。
—— 而如果有用强度本身就已经超过 312(比如一个大矩阵乘), 那说明你在算力那一侧就已经满了,一点重算都不该加

⛔ 但不要把它记成「强度不该超过 312 太多」有用的强度越高越好,没有上限 —— 边长 4096 的矩阵乘强度 1,365、 零浪费,那是 100% 峰值,完美。该贴着 312 的,只是你自己制造出来的那部分强度。

最后回到 Flash:它离那个坏情况远得很。 前向两次矩阵乘,反向四次;Flash 因为没存中间矩阵,反向要多算一次 —— 一共 7 次对 6 次。 也就是 HFU ÷ MFU = 1.167有用比例 85.7%

换来的是搬运少了一千零二十四分之一。付 17% 的多算,换掉三个数量级的搬运 —— 所以它不是「优化了 kernel」,是把这个算子挪到了另一个受限区。

第 3 节 · 上

拆开看:同一份复杂度,两边放在了不同的地方

第 2 节数的是决策点,4 个对 0 个。上半节沿着一条路往里拆: 整颗芯片 → 一个核 → 核里那块地 → 并行层级 → 一条指令吃多大一块。
每一层都问同一个问题:这一层两边少了什么、多了什么,是谁的决定造成的。
拆完之后,下半节把零件装回去 —— 两边并排把 FlashAttention 走完一遍。

3.1 先看整颗:一边把缝藏起来,一边把缝交给你

两颗芯片都是双 die 封装 —— 先看这一层,是因为有个差别 只在封装上才看得见,跳进核内部就漏掉了。

一颗 B200 全景 —— 两个 die 对软件是一个 GPU · 148 个 SM · 126 MB L2 · 8 TB/s HBM3e 灰=第三方来源 计算:SM 启用/禁用边界 片上 SRAM 片外内存 互联通路 灰字=第三方来源,非 NVIDIA 官方 一颗 B200(封装级) TSMC 4NP · 2,080 亿晶体管 · 两个 reticle 尺寸的 die 通过 NV-HBI 连成单一 CUDA 设备,L2 全局一致 ↔ 对照 TPU v7:也是双 chiplet,但它反过来暴露成 2 个独立 device,两半各有自己的地址空间。同样的封装形态,软件视图完全相反。 die 0 物理 80 个 SM · 启用 74 个 HBM3e 4 堆栈 全片 8 堆栈 = 物理 192 GB · 8.0 TB/s (对软件暴露 186 / 180 GB,随产品形态变) HBM3e ≈24 GB HBM3e ≈24 GB HBM3e ≈24 GB HBM3e ≈24 GB die 0 的 L2 63 MB 实测本分区 21 TB/s(第三方 Vulkan 压测) 内部再切 2 个分区(全片 4 个,Hopper 的两倍) 跨到对面 die 掉到 16.8 TB/s,延迟也变高 SM 阵列 每格 = 1 个 SM。红框 6 个是出厂禁用的,用来提良率 —— 148 = (80−6) × 2 GPC 分组:Blackwell Ultra 官方是 8 个 GPC / 160 SM;B200 第三方报 10 个 GPC。存疑,图上不画 GPC 边界。 禁用 禁用 禁用 禁用 禁用 禁用 die 1 物理 80 个 SM · 启用 74 个 HBM3e 4 堆栈 全片 8 堆栈 = 物理 192 GB · 8.0 TB/s (对软件暴露 186 / 180 GB,随产品形态变) HBM3e ≈24 GB HBM3e ≈24 GB HBM3e ≈24 GB HBM3e ≈24 GB die 1 的 L2 63 MB 实测本分区 21 TB/s(第三方 Vulkan 压测) 内部再切 2 个分区(全片 4 个,Hopper 的两倍) 跨到对面 die 掉到 16.8 TB/s,延迟也变高 SM 阵列 每格 = 1 个 SM。红框 6 个是出厂禁用的,用来提良率 —— 148 = (80−6) × 2 GPC 分组:Blackwell Ultra 官方是 8 个 GPC / 160 SM;B200 第三方报 10 个 GPC。存疑,图上不画 GPC 边界。 禁用 禁用 禁用 禁用 禁用 禁用 N V - H B I 10 TB/s die 间 一致 全片合计 SM 148 物理 160 Tensor Core 592 148 × 4 CUDA Core 18,944 148 × 128 寄存器堆 37 MiB 256 KiB × 148 L1 + 共享内存 37 MiB 256 KiB × 148 TMEM 37 MiB 256 KiB × 148 L2 126 MB 4 个分区 HBM3e 192 GB 8 堆栈 ↑ 中间三个 37 MiB 不是复制粘贴:寄存器堆、L1+共享内存、TMEM 每个 SM 都正好 256 KiB,三块面积相当的 SRAM 分给了三种完全不同的用途。 往外的三条路 —— 一颗 GPU 不是孤岛 NVLink 5 对外互联 · 18 条链路 × 双向 100 GB/s = 1.8 TB/s / GPU · NVL72 机架内 72 张卡全互联,域内总带宽 130 TB/s · ↔ TPU v7 是 3D torus,每片 1.2 TB/s、只连 6 个邻居 · 拓扑差别比带宽差别重要:全交叉 vs 环面 NVLink-C2C 接 Grace CPU · GB200 超级芯片 = 1 个 Grace + 2 个 B200 · CPU 内存对 GPU 是可寻址的,不必显式拷贝 · ↔ TPU 侧是 4 chip / VM,走主机接口 HIB · C2C 具体带宽本图未查实 PCIe Gen6 接主机与网卡 · 管理面 + 数据装载 · NVLink 连通的两点之间,运行时自动走 NVLink 不走 PCIe · 要点对点直传仍需显式 cudaDeviceEnablePeerAccess · Gen6 ×16 双向合计 256 GB/s —— 约为 NVLink 5 的 1/7
图 G-1 图 G-1 一颗 B200 的全景。两颗 die 用 NV-HBI 缝成「一个 GPU」,对软件完全透明 —— 代价是那道缝还在(L2 跨 die 从约 21 掉到 16.8 TB/s),而这件事不出现在任何 API 里。 —— 出自《GPU 显微镜
一颗 TPU v7 chip 全景 —— 封装里是两个 die,而它们对软件是两个「独立的」加速器 TensorCore(稠密算力) SparseCore(稀疏/通信) MXU 脉动阵列 存储 互连 一个封装 = 一颗 chip 对外名 Ironwood / TPU v7 峰值 bf16 2,307 TFLOP/s · fp8 4,614 TFLOP/s chiplet / die 0 对软件 = 独立的 device 0 TensorCore ×1 @ 2.2 GHz 两个 MXU 合计 131,072 个格子(cell) MXU 256 × 256 MXU 256 × 256 VPU 向量单元 8 × 128 一拍处理 8 sublane × 128 lane VMEM 64 MiB / core 软件自己搬进搬出的暂存,不是缓存 SMEM 1 MiB 标量/描述符(≠ 指令内存 IMEM) SparseCore ×2 每个 = 1 个标量序列器 + 16 个向量 tile SC 0 SC 1 HBM3E 96 GiB 这一半只属于 die 0 —— 两个 die 不共享地址空间 整封装 8 个 HBM3E 堆栈、7.37 TB/s;按 die 拆约 3,433 GiB/s(此拆分口径未定位到官方公开出处) = 封装 192 GiB 的一半 chiplet / die 1 对软件 = 独立的 device 1 TensorCore ×1 @ 2.2 GHz 两个 MXU 合计 131,072 个格子(cell) MXU 256 × 256 MXU 256 × 256 VPU 向量单元 8 × 128 同左 VMEM 64 MiB / core 同左 SMEM 1 MiB 同左 SparseCore ×2 SC 2 SC 3 HBM3E 96 GiB 另一半属于 die 1,两套地址空间互不可见 = 封装 192 GiB 的一半 die-to-die 互连 官方口径:比一条 ICI 链路快 。但请注意它没有把两个 die 缝成一个 device —— 它只是让跨 die 搬运比出封装便宜,地址空间仍然是两套 6 × 数一遍:一颗封装里有几个什么 TensorCore 2 = 2 个 JAX device MXU 4 每 core 2 个,各 256×256 SparseCore 4 每 device 2 个 SparseCore 的向量 tile 64 4 × 16 HBM 容量 192 GiB 官方表头就写 GiB;同页正文的 GB 是笔误 HBM 带宽 7.37 TB/s 官方另一处写 7,380 GB/s,差 0.1% ICI 对外带宽 1,200 GB/s 六条链路双向合计 同样是双 die 封装,软件看到的东西正好相反 NVIDIA B200 两个 die + 一条一致性总线 → 对软件装成一个 GPU。缝还在(跨 die 访问更慢), 但由硬件替你扛。 TPU v7 两个 chiplet 如实暴露成两个 device,各有各的地址空间。缝留在外面,由你的切分 策略去面对 封装之外:一台主机挂几颗 官方规格:每 VM 4 颗 chip(= 8 个 device)、224 vCPU、960 GB 内存、2 个 NUMA 域。主机接口 PCIe 侧约 119.2 GiB/s(未定位到官方公开出处)。记住这个 4 —— GKE 机型名里的数字按 device 算,不是按 chip 算。 对外的六个出口 —— 这才是 TPU 和 GPU 差得最远的地方 每颗 chip 有 6 条 ICI 物理链路,对应三维的正负方向。它们不是「加速卡之间的选配互联」,而是芯片出厂时就长在硅上的第 一性结构:拓扑是 3D 环面(torus),超过 64 颗以后由 4×4×4 的 cube 拼起来,cube 内走铜缆、cube 之间走光纤并经过光 路交换机重新配线。 X+ 200 GB/s X− 200 GB/s Y+ 200 GB/s Y− 200 GB/s Z+ 200 GB/s Z− 200 GB/s 六条合计 1,200 GB/s(双向) 一个必须说清的口径坑:官方正文写的是「每双向 200 GB/s」,可是 3 个轴 × 200 只有 600,对不上同一页表格里的 1,200。 只有把它读成「每条链路 200」(6 × 200 = 1,200)才自洽 —— 本文按后者画。 对照一下:B200 的 NVLink 5 是 1,800 GB/s,数字更大 —— 但它 连的是一个机柜内的 72 颗,再往外要换成 InfiniBand/以太网。 ICI 这 1,200 GB/s 是一路铺到 9,216 颗都不换协议的那种。 读这张图最容易出错的一处 chip 和 device 的比例是 1 : 2,而所有框架日志都按 device 报数。所以看到「每 device 1,153 TFLOP/s」不要以为掉了一半 —— 那正是 2,307 的一半;看到 tpu7x-128 也不要以为是 128 颗芯片,那是 64 颗。算 MFU 时分母要用 2,307(按 chip)或 1, 153.5(按 device),两个口径混用会让结论直接差两倍
图 T-1 图 T-1 一颗 TPU v7 的全景,同样两颗 die。但它如实暴露成两个 device —— 缝在明处。这两张图并排看,是第 2 节那个决定在封装这一层的样子:一边把复杂度藏起来,一边把它交给你。 —— 出自《TPU 显微镜
同样的物理构造,两种呈现方式 代价不会因为藏起来就消失 —— 你付了钱,但不知道自己付了。
—— 这个「藏 / 摊」的分工后面每一层都会再遇到,第 5 节正面算它的对价。
⭐ 为什么「把缝藏起来」那一侧,两块 die 之间那条线必须造得特别宽 (结论:那 9 倍不是工艺差距,是「表现得像一颗芯片」这个承诺的标价。展开看账)
先看这两个数 —— 它们差了约 9 倍:
· B200 片间互连:10 TB/s (NVIDIA 公开口径)
· TPU v7 的 D2D:6 × 单条 ICI link ——  每条 ICI 双向 200 GB/s,换算约 1.2 TB/s (规格以「几倍 ICI」的形式给出,本课按课程一贯的 ICI 口径换算)

⚠️ 但请先别把它读成「谁的工艺更强」 —— 这 9 倍是买了两样不同的东西。

因为它扛的不是数据,是一个承诺对上层来说,这两块硅必须表现得像一颗芯片。
一旦做了这个承诺,任何一次落到对面那块 die 上的访问都得走这条线 —— 而那是缓存未命中那一类事,编译期不知道会不会发生只能按「最坏情况随时可能发生」来造,宽度要逼近片上缓存的带宽。

而「把缝交给你」那一侧不做这个承诺:跨 die 不是意外, 是编译期排好、并且可以刻意避开的动作 ——  只需要供得起计划中的那点跨越要求低一档,线就能窄一档。
v7 那条实际要扛的主要就是一个张量并行维度的来回, 可以提前排,不是随机洪峰。

所以那 9 倍不是工艺差距,是那个承诺的标价。 代价照例是对称的:一边你从不用想它(但那 21 → 16.8 你照付, 而且它不出现在任何 API 里);另一边你必须想它(但你能量到、能绕开)。 ⚠️ 出处口径:10 TB/s 是 NVIDIA 公开数v7 那个「6 × 单条 ICI link」出自内部架构梳理,本课未能定位到官方公开出处 —— 它是一个已发布产品的参数,但「产品已发布」不等于「这一项公开过」。 照本课规矩,这种数要么给出处、要么标明没有;这里标明。
⚠️ 图上那个 131,072 先别急着往峰值上乘 (漏掉「每格一拍 2 次乘加」那一步,算出来正好差一半。展开看完整那条链)

它数的是格子:两个 MXU 一共 2 × 256² = 131,072 个。
每个格子一拍做 2 次乘加 —— 这一步图上没有。
⚠️ 「2 次乘加」不是「一个乘 + 一个加,一共两次」。 一次乘加本身就是一乘一加 —— a × b + c,一个周期里一起做完,记 2 FLOP
所以「一格一拍 2 次乘加」= 4 FLOP,不是 2。这一步错了,最后就差一倍。
常见的错法正是把一格当成一拍只出一次乘加: 131,072 × 2 core × 2 FLOP × 2.2 GHz = 1,153 TFLOP/s, 刚好是图头 2,307 的一半。少的就是「每格 2 次」那一步。
完整那条链在 3.4,到那儿一步步走一遍。

3.2 把一个核拆开:少掉的那五样,是同一个决定的五个后果

往里一层。先看 GPU 这边有什么,再看 TPU 那边没有什么 ——  顺序不能反,不然会以为 TPU 是个「简化版」。

一个 SM 的显微镜展开 —— NVIDIA Blackwell(计算能力 10.0) · 4 个处理块 · 128 CUDA Core · 4 Tensor Core · 256 KiB TMEM 灰字=官方未标 指令 · 调度 通用向量:CUDA Core / 寄存器 张量通路:Tensor Core / TMEM 访存单元 整个 SM 共享 官方未公布个数 一个 SM(Streaming Multiprocessor) B200 全片 148 个(物理 160,每 die 80、启用 74) · Blackwell Ultra 全片 160 个 · 最多 64 个 warp、32 个线程块同时驻留 L1 指令缓存 · 线程块从 GigaThread 引擎派进来,落在这个 SM 上就不再迁走 一个线程块(CTA)整体钉在一个 SM 上;块里的每个 warp 再被分派到下面四个处理块中的某一个,同样不迁走。这两条「钉死」是后面所有容量除以 4 的前提。 处理块 0 sub-core / partition L0 指令缓存 Warp 调度器 ×1 + 分发单元 每周期从驻留 warp 里挑 1 个就绪的,发 1 条指令 驻留 warp 槽 ×16 深=已驻留 白=空槽 寄存器堆 16,384 × 32 bit = 64 KiB = 64K 寄存器/SM ÷ 4 · 单线程上限 255 个 CUDA Core ×32 FP32 / INT32 统一单元 一条指令 → 32 条 lane 同时算 = 一个 warp 一拍做完 Tensor Core ×1 (第 5 代) 1,024 次乘加 / 周期 ← 推导值,见下 操作数不走寄存器堆,走下面的 TMEM 一次只吃很薄的一片矩阵,不是想象中的大方阵 薄到什么程度、怎么攒成大矩阵 —— 见图 G-4 TMEM 本块够得着的 32 lane × 512 列 = 64 KiB ⚠️ 按整体申请,不是各分一块 本块只够得着 lane 0–31 LD / ST ×8 SFU ×4 访存单元 exp · rcp · rsqrt 处理块 1 sub-core / partition L0 指令缓存 Warp 调度器 ×1 + 分发单元 驻留 warp 槽 ×16 寄存器堆 16,384 × 32 bit = 64 KiB CUDA Core ×32 Tensor Core ×1 (第 5 代) 1,024 次乘加 / 周期 TMEM 本块够得着的 32 lane × 512 列 = 64 KiB 只够得着 lane 32–63 LD / ST ×8 SFU ×4 处理块 2 sub-core / partition L0 指令缓存 Warp 调度器 ×1 + 分发单元 驻留 warp 槽 ×16 寄存器堆 16,384 × 32 bit = 64 KiB CUDA Core ×32 Tensor Core ×1 (第 5 代) 1,024 次乘加 / 周期 TMEM 本块够得着的 32 lane × 512 列 = 64 KiB 只够得着 lane 64–95 LD / ST ×8 SFU ×4 处理块 3 sub-core / partition L0 指令缓存 Warp 调度器 ×1 + 分发单元 驻留 warp 槽 ×16 寄存器堆 16,384 × 32 bit = 64 KiB CUDA Core ×32 Tensor Core ×1 (第 5 代) 1,024 次乘加 / 周期 TMEM 本块够得着的 32 lane × 512 列 = 64 KiB 只够得着 lane 96–127 LD / ST ×8 SFU ×4 ↑ 与 0 号处理块完全对称,说明文字不再重复;只有 TMEM 的 lane 编号各不相同 两个 SM 配对 cta_group::2 这块 SM 的 4 个 Tensor Core,可以和隔壁那块 SM 的 4 个一起做同一次 MMA,共用一份操作数。 注意层级:配对发生在 SM 与 SM 之间,不是上面四个处理块之间 —— 处理块之间连 TMEM 都是切开各管各的。 L1 数据缓存 + 共享内存 —— 同一块 SRAM,四个处理块共用 这是 GPU 上「编译器/程序员能显式管」的那一层,对应 TPU 的 VMEM 合计 256 KiB / SM · 其中可划给共享内存最多 228 KiB(单个线程块最多 227 KiB,CUDA 留 1 KiB) 可选切分:0 / 8 / 16 / 32 / 64 / 100 / 132 / 164 / 196 / 228 KiB · 静态声明仍限 48 KiB,超过要显式 opt-in 分 32 个 bank、每 bank 每周期 4 B —— 历代如此,Blackwell 官方文档未重申 同一个 cluster 里别的线程块能直接读写这块内存 —— 分布式共享内存,Hopper 引入 实测 L1 命中约 39 周期 (第三方实测,非官方) TMA 张量内存加速器 一条指令搬一整块多维张量:软件填一个描述符(基址 · 各维长度 · 各维步长 · 分块形状),硬件自己算地址、自己搬。 这就是 GPU 版的 DMA 引擎,和 TPU 的 DMA 引擎是同一类东西 —— 都是「把地址生成从计算单元手里拿走」。Hopper 引入,Blackwell 沿用。 纹理 / 采样单元 共享内存原子单元 实测 32 次/周期/SM ⚠️ 一个 SM 里有三块 256 KiB,归属完全不同 —— 这是全图最容易混的一处 寄存器堆 256 KiB 切成 4 份,每份 64 KiB ⛔ 严格私有:warp 只能碰自己那份, 跨处理块完全不能访问 L1 + 共享内存 256 KiB 一整块,不切,四个块全都能访问 ⭐ 四个处理块之间唯一的数据通道 —— 「线程块」这个抽象就落在它身上 TMEM 256 KiB 整体申请(一列跨全部 128 lane) 介于两者之间:访问权限四分 (一个 warp 32 lane),所有权不分 把上面四列乘回去 —— 整个 SM 的账 CUDA Core 128 32 × 4 Tensor Core 4 1 × 4 寄存器堆 256 KiB 64 KiB × 4 TMEM 256 KiB 64 KiB × 4 L1 + 共享内存 256 KiB 四块共用 最多驻留 warp 64 16 × 4 最多驻留线程 2,048 64 warp × 32 每周期发指令 4 条 1 × 4 个调度器 「1,024 次乘加 / 周期」这个数从哪来 A100 白皮书原话:四个 Tensor Core 合计每周期 1,024 次 FP16 乘加(=每核 256)。H100 官方称「张量吞吐 2× A100」,Blackwell 再 2× → 每核 1,024 代回 B200 对账:1,024 × 2 FLOP × 4 核 × 148 SM × 1.83 GHz = 2.22 PFLOPS,官方 2.25 —— 差 1.4%,缺口就是官方没公布的确切时钟。完整对账表见正文 §2。
图 G-2 图 G-2 一个 SM 拆开。warp 调度器、256 KB 寄存器堆、228 KB L1/共享内存、记分板 ——这些部件加起来只干一件事:在运行时动态地藏住延迟和分支。 —— 出自《GPU 显微镜
把一个 TensorCore 拆开 —— 值得数的不是它有什么,是它「少了」什么 矩阵乘 向量/标量 片上存储 对外搬运 灰色虚线 = 官方未公开 一个 TensorCore (= 一个 JAX device 的全部算力) @ 2.2 GHz 控制:标量单元发射 VLIW 指令包 一拍发出一整包指令,包里各个槽同时喂给下面不同的单元。谁在第几拍动、数据什么时候到位, 全部由编译器在编译期排好 标量 向量 矩阵 杂项 9 个发射槽 槽位构成是 v2/v3 论文的公开数字,v7 官方未公布 MXU 256 × 256 65,536 个 cell × 每 cell 每周期 2 次乘加 = 131,072 乘加 / 周期 官方 MXU 256 × 256 65,536 个 cell × 每 cell 每周期 2 次乘加 = 131,072 乘加 / 周期 官方 VPU 向量单元 8 sublane × 128 lane 一拍处理 1,024 个元素 激活、归一化、逐元素运算、归约 —— 阵乘之外的活都归它。后面 §3 讲的 lane / sublane,物理来源就是这张 8× 128 的网格。 跨 lane 单元 ×2 转置、跨 lane 归约、 shuffle。VPU 的 128 条 lane 各干各的,数据要横着 走就得经过这里。 个数未定位到官方公开出处 片上存储 —— 容量出自 JAX 开源代码,带宽官方未公开 VMEM 64 MiB 软件显式搬进搬出的暂存。不是缓存 ——没有自动填充、没 有替换策略、命不命中这件事不存在 SMEM 1 MiB 标量数据与 DMA 描述符 累加器 128 个 每个形状 (8, 256)、32 bit,挂在每 个 MXU 上 向量寄存器 查不到 v2/v3 论文说每 sublane 32 深; v7 没有公开 对外:DMA 引擎 HBM ↔ VMEM 的搬运由 DMA 完成,而描述符是标量单元发出来的 —— 也就是说「什么时候搬、搬多少」同样写在 指令流里,不是硬件自己决定的。 一个 TensorCore 到底有几个 DMA 引擎:查不 到。所以这里只画一个盒子,不标数量。 一个 GPU SM 里有,而这里「没有」 先数清楚少了什么,再问省下的硅去了哪 4 个 warp 调度器 指令由谁选、什么时候发 —— 这里没有「选」这个动作 64 个 warp 槽(2,048 个线程上下文) 没有线程概念,也就没有「切换到别的线程」这条退路 256 KB 寄存器堆 GPU 那么大的寄存器堆主要是为了同时装下几十份上下文 L1 / 共享内存的自动填充 VMEM 是纯暂存:谁搬谁负责,没有命中率这回事 记分板 / 乱序发射 全部前移到编译期。代价见 §7 —— 算错了没有兜底 省下的硅去了哪:软件能直接指挥的暂存 TPU v7 一颗 chip 上的 VMEM 是 128 MiB(两个 core 各 64 MiB),而且每一个字 节都由编译器显式安排 TPU v7 一颗 chip 的 VMEM 128 MiB B200 一整颗的共享内存合计 约 34 MB 第三方 口径是软件可控的暂存,不含 B200 那 126 MB 的 L2 缓存。 这张图想让你记住的一句话 GPU 的 SM 里,大部分晶体管不是在算数,是在「决定接下来算什么」 —— 调度器、上下文、记分板、缓存控制,全都是为了在运行时动态地藏住延迟和分支。TPU 把这一整套前移到了编译期,于是这些部件可以整个不要。 省下的不是「面积换算力」,是这套机构整个不做了 —— 电路更简单、功耗更低。它没有变成更多算力,也没有变成更多片上存储(两项 B200 都更多)。这不是「TPU 更简单」,是把复杂度换了个地方放 —— 从硅上换到了编译器里。
图 T-2 图 T-2 一个 TensorCore 拆开,重点在右边那一栏。上面那五样在这里一样都没有。而它们不是五个独立的取舍,是同一个决定的五个后果:一旦你决定不在运行时换任务,这五样同时失去存在理由。 —— 出自《TPU 显微镜
这是全课最值得停一下的一张对照 TPU 少掉的那五样不是省出来的,是不需要了
一旦决定「不在运行时换任务」—— warp 调度器、记分板、几十个 warp 槽、 常驻上下文、动态分支处理,同时失去存在理由

省下的面积去哪了?—— 先说它去哪:没变成更多算力,也没变成更多片上存储。 这两条 3.7 和 3.2b 会各用一张表堵死(乘加总量 GPU 反而多 16%, 片上 SRAM GPU 多 73%)。
它变成的是「不做」本身:整套 cache 与调度机构不做了, 同样多的算力只切成 4 大块而不是 592 小块 ——  控制接口的份数少 148 倍(592 对 4)。 ⚠️ 这换来了多低的功耗、多好的良率?这门课一个数都没量过 ——  但官方替它量过一部分,见下面那个折叠。
这条有官方出处:TPU 初代论文(ISCA 2017)就是这么论证的 —— 以及它十年后为什么不再成立(引文 + 三条边界)

省掉了哪些东西,论文里是一句话列完的(Jouppi et al., ISCA 2017, In-Datacenter Performance Analysis of a Tensor Processing Unit, arXiv:1704.04760,第 8 页):

the single-threaded TPU has none of the sophisticated microarchitectural features that consume transistors and energy to improve the average case but not the 99th-percentile case: no caches, branch prediction, out-of-order execution, multiprocessing, speculative prefetching, address coalescing, multithreading, context switching, and so forth. Minimalism is a virtue of domain-specific processors.

省下来变成了什么,摘要里直说

The lack of such features helps explain why, despite having myriad MACs and a big memory, the TPU is relatively small and low power.

最硬的一个数在 Figure 2 那张 floor plan 上 ——  整颗 die 的面积分配:数据缓冲 37% · 计算 30% · I/O 10% · 控制逻辑只有 2%。论文紧接着那句是:

Control is much larger (and much more difficult to design) in a CPU or GPU.

—— 这就是「简单」在版图上的样子:2%

「稳定性」那一条,官方讲的其实是另一个词:不是良率,是 确定性执行(deterministic execution)。论文的论点是 ——  CPU / GPU 那些随时间变化的优化(cache、乱序、多线程) 提高的是平均吞吐,不是尾延迟;而 TPU 的确定性模型 更匹配这类应用 99 分位响应时间的要求。 ⚠️ 良率/可靠性我没有找到出处,所以这门课不说这一条。

⛔⛔ 引这篇论文必须带上的三条边界 ① 它比的是 TPU v1(2015)对 K80(2014)。那一代省下来确实换成了算力: 同样一颗小 die 塞进 25 倍的 MAC、3.5 倍的片上内存,功耗还不到 K80 一半。
但到 v7 对 B200,这个结论反过来了 ——  3.7 那张账表数出来:乘加总量 606,208 对 524,288,GPU 反而多 16%同一个机制(minimalism),十年之间结果不一样了。
所以今天只能说「省下的是复杂度」,不能再说「省下的变成了算力」。 —— 顺带一提,我这门课的开场原来就写错成后者, 因为脑子里装的是 2017 年那个结论。老约束在新一代上已经被解除, 而结论会自己留在原地。

② Ironwood 官方那个 perf/watt「2×」,比的是上一代 TPU(Trillium), 不是 GPU。(Google blog,2025-04-09;口径是每芯片 TDP 每瓦的 峰值 FP8 flops。)拿它去对 GPU 是换了分母。

③ ⛔ 「那面积到底省了多少」—— 这个问题在公开信息层面答不了 NVIDIA 公布 B200 是 2,080 亿晶体管、两颗 reticle 极限 die(各约 800 mm²); 而 Google 从不公布 TPU 的 die 尺寸和晶体管数 一边公布一边不公布,「谁的 die 更小」就没有可比口径。
但「省在哪几项」可以逐项点名,用的还是本课自己的两张表: L2 整整一层 TPU 没有(B200 侧 126 MB)、 线程上下文的寄存器堆 TPU 也没有(B200 侧 37 MiB)——  合计 157 MiB 的片上 SRAM,花在 TPU 压根不存在的两样东西上; 再加上控制接口的份数少 148 倍(592 个 Tensor Core 对 4 个 MXU, 差 588 套 —— 148 是倍数,不是差值)。 SRAM 和控制接口都是要占面积的 ——  省下来的能一项一项点出来,只是「整颗谁大」给不出答案。

④ 良率、可靠性:没有任何可引的出处,不说。 官方讲的是「确定性执行」,那是尾延迟可预测,跟可靠性不是一回事。 —— 论文全文摘录(省掉的完整清单、2% 那张 floor plan、 v1 时代的全部量化结果)在 L300 §3.2 那个折叠里。
两条只当对照用的边注:那五样东西,以及最容易记混的那个数
📖 上面这两张图里有五样东西,本课只当对照用,不展开讲 专题二问的是「两边有什么不同」,不是「GPU 内部怎么工作」。 五条索引折在下面,回查用。
去哪儿看这五样 —— 《GPU 显微镜》对应节号(回查用 · 讲的时候跳过)

① warp 调度器:部件在 GPU 显微镜 第 2 节, 它工作起来什么样在显微镜第 7 节(「延迟怎么被藏起来 —— 一边靠换人,一边靠排班」)。
② 线程 / warp 到底钉在哪块硅上:显微镜第 3 节,一层抽象一层硅地对。
③ SM 里的寄存器怎么用:显微镜第 2 节。一句话预告 ——  256 KiB / SM 切成四份,每个处理块 64 KiB,单线程最多 255 个它这么大,主要不是为了算得快,是为了同时装下几十份上下文。
④ L2 怎么用:容量与分区在显微镜第 1 节,一个数从 HBM 走到乘加单元中间几站在第 6 节
⑤ SM 之间怎么协同:显微镜第 3 节讲 cluster / 分布式共享内存 ——  它之所以存在,正是因为 148 个 SM 之间除了 L2 再没有别的快捷通道; 另外上面那张 SM 图里的 cta_group::2 是更紧的一档: 两个 SM 的 Tensor Core 合起来做同一次矩阵乘。

⚠️ 顺手澄清一个很容易记混的数 —— 下一节的表马上要用 B200 的 L2 是 126 MB(每个 die 63 MB,全片 4 个分区)。不是三十几 MB。
三十几 MB 的那个是另一样东西 —— 全片共享内存合计: 148 个 SM × 228 KiB = 33,744 KiB,约 33 MiB
两个数差着近四倍。而且它们的性质正好就是本课那个决定L2 是硬件自动管的,那 33 MiB 是软件说了算的。

拆完部件,下一个必然要问的是片上那块地 ——  3.6 那个融合的全部收益就来自「中间结果留在片上」。 这个问题只有两半:多大、多快,下面两小节各答一半。

3.2b 那 GPU 片上存储明明更多啊 —— 先说清楚在比什么

这是个必须正面回答的反问。把两颗芯片的片上 SRAM 全列出来, 一格都不漏:

这块 SRAM 干什么用谁说了算 GPU B200(整颗)TPU v7(整颗 chip)
L2 缓存硬件 126 MB ≈ 120 MiB没有这一层
线程上下文(寄存器堆)硬件 / 编译期分配 37 MiB(256 KiB × 148)不驻留上下文,没有
矩阵操作数暂存指令显式搬 TMEM 37 MiB累加器 4 MiB
软件显式管的暂存编译器 / 你 L1+共享 37 MiB
单个线程块最多 227 KiB
VMEM 128 MiB+SMEM(标量内存)2 MiB
每核一整块 64 MiB
合计 ≈ 231 MiB≈ 134 MiB
向量寄存器官方未公开,未计入

GPU 侧四项与 TPU 侧容量均取自两份《显微镜》的来源标注 (L2 与共享内存为第三方实测/规格,VMEM 64 MiB / core、SMEM 1 MiB / core 出自 JAX 开源代码)。
—— ⚠️ TPU 的 SMEM 是 scalar memory,存标量与 DMA 描述符,跟 CUDA 里的 shared memory 不是一回事本表里对得上 CUDA shared memory 的是 VMEM 那一格。 合计为本文相加所得:37+37+37+126 MB→120 MiB = 231 MiB64×2+1×2+1×2×2 = 134 MiB

同一张表,三种口径,三个方向相反的结论 ① 比总量 —— GPU 赢,231 对 134,多出 73%。 这一条是对的,别嘴硬。
② 比「软件当数据暂存用的那一层」(只数表里「软件显式管的暂存」那一行) —— TPU 赢,130 对 37,约 3.5 倍。 本课之前那句「省下的面积给了片上暂存」说的是这个口径 ——  但当时没把口径写出来,所以按 ① 读完全合理。
⚠️ 这一条排除了「矩阵操作数暂存」那一行 ——  按「谁说了算」那一列读,TMEM 37 和累加器 4 也算软件管的,全算进来是 134 对 74、1.8 倍。3.6 引的是前一个数。 报这类比值,必须先说清数了哪几行。
③ 比「一个计算单元能当成连续工作台用的」—— TPU 赢 289 倍。 64 MiB ÷ 227 KiB ≈ 289 GPU 那 37 MiB 不是一整块,是 148 个互相看不见的 256 KiB 小岛 —— 而这 256 KiB 里 L1 还占着一部分,一个线程块能显式管到的最多 227 KiB
⚠️ 那 64 MiB 是这块地的总面积,不是你一次能用的额度 上面这张表比的是容量。但 289 倍这个数还有半句话没说: 一个算子一次能动用多少 VMEM,另有一道上限。

XLA 给每一条指令/每一个融合单独设一个「scoped VMEM 额度」, 由目标芯片加上一个可调的编译选项共同决定 (xla_tpu_scoped_vmem_limit_kib,遇到 VMEM 放不下时调它是常规操作)。 编译器切块的时候,除的是这个额度,不是那 64 MiB。
而且这块地还有别的用户:**决定哪些张量常驻 VMEM 的那个编译阶段**跟它抢同一块, 双缓冲(预取下一块)也要占一份。

所以正确的读法是:64 MiB 决定了「你有可能开多大的块」, scoped 额度决定了「这一次实际开多大」。 ⚠️ 那个额度的默认值本课没能确认,不同代次也未必一样 ——  所以这里只说有这道闸,不报数。要用就自己在目标机型上量。
读这张表的两个坑:那 289 是上限比上限, 以及为什么第 ① 条最容易把人带沟里(要引用这些数再展开)
⚠️ 那 289 是上限比上限 —— 两边的「独占」都有水分 GPU 那 227 KiB,不是「一个 Tensor Core 的工作台」。 它是一个线程块能申请到的上限;而这个 SM 上有 4 个 Tensor Core, 还可能同时驻着别的线程块 —— 它们抢的是同一块。 (每 SM 的共享内存只能取几个固定档: 0/8/16/32/64/100/132/164/196/228 KB。228 是最大档, 线程块能显式管到 227。)
TPU 那 64 MiB,也不是全归你。 官方说得很清楚:Ironwood 每个 TensorCore 64 MB VMEM (一颗 chip 两个核,所以是 128 MB/chip), 而这 64 要在「当前这一步用的」和「预取下一批权重的」之间分 分多少由一个 flag 定(xla_tpu_scoped_vmem_limit_kib)。 ⚠️ 默认值官方没公布 —— 但官方教程里常见的写法是把它设到 65536(=64 MiB),反过来说明默认低不少

所以 289 的准确读法是「一个 kernel 实例能独占的连续暂存」的上限之比它仍然是决定「一次能融多大」的那个数 ——  但别把它读成「TPU 片上存储多两个数量级」那是口径 ① 的问题,而口径 ① 的答案前面已经说了:GPU 更多。
⭐ 为什么第 ① 条虽然对,却最容易把人带沟里 GPU 总量领先的 97 MiB,几乎全来自那一行 L2。 而 L2 是这张表里唯一一格你没法安排的空间 ——  你不能把一块权重「放进 L2 待着」,只能发访存指令然后希望它还在。
—— 把 L2 算进「我有多少片上空间」,正是这门课从第 2 节起就在防的那个错。 第 2 节那 4 个运行时决策点里,有两个(L2 命中、被挤出去)说的就是它。

所以这三种口径不是并列的,得看你在问什么: 问「这颗芯片一共有多少 SRAM」用 ①; 问「我能把中间结果放在哪儿不落 HBM」—— 只有 ③ 算数。 而后面这个问题,正是算子融合每一次都要回答的那个 ——  3.6 并排走 FlashAttention 时,这个 289 倍会直接决定两边把块切多大。

3.2c 那速度呢 —— L2 很大,但它并不快

3.2b 回答的是「有多大、归谁管」。还差一半:跑多快。 补上之后会撞出一个几乎所有人都猜错的结果。

这一站带宽相对 HBM数从哪来
HBM3e(片外)8.0 TB/s官方
L2 本分区21 TB/s2.6×第三方实测
L2 跨到对面 die16.8 TB/s2.1×第三方实测
L1 + 共享内存(全片合计)≈ 35 TB/s≈ 4.3× 每 SM 128 B/周期
有实测,见折叠
TPU VMEM官方未公开 —— 公开资料只说得到「比 HBM 高约一个数量级」 只给量级,原因见下

⚠️ ≈ 35 TB/s 是推导值,128 B/周期是上限不是保证 —— B200 时钟本身也不确定,所以写 ≈ 35 而不是一个精确数。 推导链、第三方实测背书与两个限定,折在下面。

这一行怎么来的,以及为什么现在敢写(溯源 · 讲的时候跳过)

推导链是 32 个 bank × 每 bank 每周期 4 B = 128 B/周期/SM× 148 SM × 1.83 GHz = 34.7 TB/s128 B/周期这个数有第三方实测背书:Hopper 微基准(arXiv 2402.13499 表 V) 在 H800 上量到 共享内存 127.9、L1 125.8 B/周期/SM ——  两者撞同一个上限,说明它们确实共用一条数据通路。 另有两份公开材料独立给出同一个 128 B/周期/SM。
跨代锚点:按同一公式算 H100 是 132 × 128 × 1.755 GHz ≈ 29.7 TB/s, 第三方对 H100 报「约 33 TB/s」(反推时钟约 1.95 GHz)—— 量级对得上,公式没跑偏。
⚠️ 两个必须带上的限定: B200 时钟本身不确定(1.83 是第三方 boost 值, 另一家实测反推约 1.99),所以写 ≈ 35 TB/s 而不是一个精确数; 128 B/周期是上限不是保证 —— 同一张表里 RTX 4090 标量 FP32 只跑到 63.7, 换成 FP32.v4 才上到 121.2;H800 标量就能到 125.8。 访问宽度不够,这条通路吃不满。

为什么 TPU 那一行只给量级,不给数(口径说明)
⚠️ 为什么 TPU 那一行只给量级,不给数 VMEM 的带宽属于未公开规格 —— 官方规格页只给容量,不给带宽。 本课能说的到此为止:它比 HBM 高约一个数量级而 GPU 的共享内存是 4.3 倍
—— 所以能确定的结论只有一句:各跟各的 HBM 比,TPU 这一层甩开得更多。 这一句不依赖具体数值。不要往这里填一个精确数字,也不要写成某个整数倍 —— HBM 带宽是公开的,写出倍数等于把那个未公开的数说出来。
⭐ 反直觉的那一条:L2 大,但只比 HBM 快 2.6 倍 大多数人默认「cache 总该快一个数量级吧」。B200 的 L2 不是。
—— 所以 L2 的价值不在「它本身快」,而在「省掉一趟 HBM 往返」。 命中了,你拿到的是 2.6 倍;没命中,你什么都没省还多绕了一圈。
这就是为什么「让它落在 L2 里就好了」不是一个能依赖的策略 ——  收益倍数本来就不大,而且你还没法保证它在。 第 2 节那 4 个决策点里的两个,押的就是这 2.6 倍 —— 而它只有 2.6 倍。
⚠️ 但「2.6 倍」有个前提,而它常常不成立 —— 块得够大 上面比的是峰值带宽,而峰值是 HBM 最擅长的那种工况下才有的:一次连续搬很大一块
HBM 每次访问都要先付一笔固定的启动开销(几百纳秒量级), 而且它靠很宽的总线并行取数才跑得满 ——  块小的时候这笔开销摊不掉、总线也填不满,实际带宽比峰值低一到两个数量级。
片上那一层没这个问题:离得近、粒度细、启动几乎不要钱
所以块越小,L2 命中的实际收益越大 —— 远不止 2.6 倍。 2.6 倍是这个收益的下界,不是它的典型值。 —— 这也从另一个角度解释了 2.1 那道 sector 算术: 「访问连续」争的不只是别浪费带宽,还有让这一次搬运够大 具体低多少取决于是哪一代 HBM、哪一种访问模式, 各家各代实测曲线都不一样 —— 本课只给方向,不给曲线
⭐ 为什么它只有 2.6 倍,以及换个问法结论完全不同 为什么慢:L2 是全片共享的。 共享内存 ≈ 4.3 倍、VMEM 约一个数量级,都比 L2 的 2.6 倍高 ——  因为它们就贴在计算单元旁边,一个 SM 只读自己那块; L2 的数据得穿过片上互连才到得了 SM。 (这一条是架构推理,不是实测归因 —— 省掉 tag 与替换逻辑也省面积和功耗, 但带宽差距的大头在距离。)

⭐ 而「2.6 倍」只回答一个很窄的问题。 它答的是:这一个字节命中 L2 而不是去 HBM,快多少。
换个问法结论完全不同:每一字节容量能供多少带宽? —— L2 是用 1/1524 的容量做到 2.6 倍的带宽的。 两者一乘,L2 的带宽密度是 HBM 的约 4,000 倍。
换成一个能想象的说法:把 HBM 整个读一遍要 24 毫秒, 把 L2 整个读一遍只要 6 微秒(再往里,全片的 L1 + 共享内存只要 1.1 微秒)。
⭐ 所以这两个数各管各的,别合并 · 「2.6 倍」是优化决策用的数 ——  它告诉你「靠 L2 命中」这条路收益有限,所以不能依赖。这一条不变。
· 「4,000 倍」是理解架构用的数 ——  它告诉你整颗芯片为什么围着片上存储设计。

⭐⭐ 而这两个数的比值恰好就是容量比 ——  也就是说:片上存储的本事,全在「它小」这件事上。 它快不是因为它是 SRAM,是因为它小、离得近、线短。
—— 这一句反过来正是本课的主线: 「能融多大」和「融了有多值」是同一块地的两个面, 3.6 那口灶台会把它们摆在一起。 ⚠️ TPU 侧这个比值算不出来:VMEM 的带宽官方没有公开, 本课只给量级。只能说方向相同、而且更极端 ——  VMEM 的容量比 L2 还大(128 MiB 对 126 MB), 而它对 HBM 的带宽倍数按公开口径是「约一个数量级」,比 L2 那 2.6 倍高得多。

—— 所以真正快的那两块,恰好都是软件说了算的那一类。

那 VMEM 相当于 GPU 的哪一级?—— 哪一级都不是。 按性格它是共享内存(软件显式搬、没有 tag、不会 miss), 按容量它是 L2 那一级(128 MiB vs 126 MB,几乎一样大,而共享内存全片才 37 MiB)。 —— 最准的说法是:TPU 把「共享内存那种性格」的东西做到了「L2 那么大」, 然后把 L2 整层删掉了。

⛔ 别把这两小节读成「TPU 赢了三局」 那三条领先都是真的,但每一条都有对价 —— 把对价一起记住,这两节才算读懂了。
① 暂存又大又快,代价是没有兜底。 没有 cache 就没有「猜错了硬件帮你补」这回事: GPU 猜错只是慢一点,TPU 编译器切错了就是真的停在那儿等。 这一层的可预测性是买来的,不是白送的
② GPU 那 37 MiB 寄存器堆 + 37 MiB TMEM 看着「没用在数据上」,它买的是别的东西形状不规则、长度会变、分支不确定的活,照样能跑得不太难看。 TPU 上同样的活,得先把形状固定下来 —— 这笔账第 5 节会正面算。

⭐ 所以这两小节真正的结论不是「谁的片上存储更好」。
两边都在处理同一个东西 —— 不确定性,只是把它放在了不同的地方。
GPU 花硅去兜住不确定:缓存、替补、调度器。
TPU 把不确定性消灭在编译期,省下的硅**没有**变成更多算力(3.7 会数给你看),变成的是更少的电路 ——  代价是要求你(或者编译器)提前把话说死。
—— 落到选型上就一句话:形状规整、可预测的活,TPU 这套换算得过来; 形状不规则、长度动态变的活,GPU 那笔保险费就花得值。 第 5 节整节都在算这笔保险费到底多少钱。
顺带两个常被问到的:L2 能用对面 die 的吗?SM 之间怎么换数据?

① L2 不是「每 die 切一半」,是切成 4 个分区、每 die 两个(Hopper 的两倍)。 对面 die 的能用,而且不用你做任何事 —— 两个 die 由 NV-HBI(10 TB/s)连成单一 CUDA 设备,L2 全局一致。 代价就在上面那张表里:21 掉到 16.8,延迟也变高这正是 3.1 说的「你付了钱,但不知道自己付了」的具体形态。 TPU 反过来 —— 两半直接暴露成 2 个 device,想用对面得自己写一步。

② SM 之间换数据分三档,档与档之间差别很大: 同一个 cluster 内(≤ 8 个线程块、H100 起可 opt-in 到 16,须在同一个 GPC)——  分布式共享内存,直接读写别人的共享内存,不走 L2; Blackwell 还多一档更紧的 cta_group::2,两个 SM 的 Tensor Core 配对干同一次矩阵乘。 出了 cluster,就只剩 L2 和 HBM —— 片上再没有快捷通道,同步只能靠原子操作、协作组或者拆 kernel。

⭐ 值得记的是因果方向cluster 之所以被发明出来,正是因为 148 个 SM 之间除了 L2 再没有别的路。 它是在「一个 SM」和「整颗 GPU」之间硬插的一层。 —— TPU 完全不需要这一层:一个 chip 只有 2 个 TensorCore, 本来就不存在「一组核怎么协同」的问题。核少反而省掉一整层抽象, 这是规模带来的差别,不是设计水平的差别。
展开讲在 GPU 显微镜 第 3 节。

3.2d 那等多久呢 —— 同一条 load,30 拍到 659 拍

3.2b 问「有多大」,3.2c 问「跑多快」。还差第三个轴:等多久。 而这个轴,是三个里面唯一一个 GPU 自己也答不上来的。

先把提问摆清楚:一个 warp 要一块数据,L1 里没有 → 去 L2 查 →  L2 也没有 → 再去显存搬。这一串下来要多久?

延迟阶梯 —— 同一条 load 指令,30 拍到 659 拍 §3.2b 比的是容量,§3.2c 比的是带宽。这是第三个轴:等多久—— 而三个轴给出的结论方向并不一致。 NVIDIA GPU:一次取数可能落在哪一档 指针追逐微基准实测,单位是时钟周期(拍)。前五行是 H100,最后一行是消费级 Blackwell 做对照。 寄存器 ≈ 1 拍 —— 直接就在手里 共享内存 / L1 命中 30–40 拍 —— H100 与 Blackwell 都在这一档 L2 命中 273 拍 —— 已经比 L1 慢了七八倍 L2 两个分区都打满 508 拍 —— 分区设计在拥挤时优势消失 一路到 HBM 659 拍 —— 全 miss 的端到端耗时 (对照)消费级 GDDR7 877 拍 —— 换一种显存这个数就变了 不要把这几段相加。659 拍是一次全 miss 的端到端耗时—— L1 和 L2 那两次扑空已经含在里面了。该记住的不是「加起来多少」,是「同一条指令在 30 到 659 之间摆」。 ⚠️ B200 没有同类公开数据。量级与形状是这一代通用的,但别把它当成 B200 的实测值。 出处:arXiv 2507.10789 的指针追逐微基准。 TPU:这个轴上只有常数 不是数字保密 —— 是没有「可能命中、可能没命中」这一档。 VMEM 访问 —— 编译器显式管的暂存。数在那儿不是因为运气好, 是因为编译器自己发的那条 DMA 把它放在那儿 HBM → VMEM 的 DMA —— 这一段当然也有延迟,但它是一次被安排好的搬运 延迟已知,等待点也是编译器插的 所以「缓存缺失」在 TPU 上不存在—— 不是缓存做得好,是根本没有硬件缓存。缓存是一台猜测机器;编译期排好的机器不需要猜 那怎么规划?—— GPU 根本不规划 这二十倍的差距你在源码里看不出来—— 同一条 load,长得一模一样。它的三条对策没有一条在试图算准: ① 超量线程 —— 一个 SM 驻留几十个 warp,卡住就切下一个 延迟不是被消除,是被别人的活盖住的 ② 记分板 + 动态发射 —— 运行时盯着操作数到没到,到了才发 ③ 缓存本身 —— 它就是一台猜测机器,赌你还会再用一次 ⚠️ 调优在两边动的旋钮一样都不挨着:GPU 调「怎么让别的活足够多」,TPU 调「怎么让编译器排得开」。 ⭐ 一句话对照 GPU 用「总有别的活可干」来盖住不确定的延迟;TPU 用「把不确定性删掉」来避免它。—— 一个是统计学的答案,一个是确定性的答案。 这也是这门课从第 1 节起一直在说的那件事,第一次落到了「拍」这个单位上。⚠️ 但别读成「两边越走越远」—— 现代追峰值的 CUDA kernel 用异步拷贝+显式 barrier 自己管双缓冲,那就是在 GPU 上手写一个静态调度
同一条 load 指令,在源码里长得一模一样,实际耗时差二十倍。但不要把这几段相加 —— 659 拍是一次全 miss 的端到端耗时, L1 和 L2 那两次查找已经含在里面了。 ⭐ 右边那半是这张图真正的落点:TPU 上这个轴只有常数 ——  不是数字保密,是没有「可能命中、可能没命中」这一档数出自 arXiv 2507.10789 的指针追逐微基准(H100 与消费级 Blackwell)。B200 没有同类公开数据,量级与形状可以借,别当成 B200 实测值。
⛔ 提问里最容易走偏的一步:这几段不能相加 「L1 查一下要时间,L2 查一下又要时间,HBM 搬又要时间」——  这个直觉会把账算重
659 拍是一次全 miss 从发出到拿到的端到端耗时L1 那次扑空、L2 那次扑空,都已经含在这 659 里了。
—— 真正该记住的不是「加起来多少」,是「同一条指令的耗时在 30 到 659 之间摆」。
⭐ 那怎么规划?—— 答案是:GPU 根本不规划 「每种情况耗时不一样长,这就没法事先排」—— 这个判断完全成立。 而 GPU 的回答不是「我能算准」,是「我不需要算准」。

它的三条对策,没有一条在试图预测延迟:
· 超量线程 —— 一个 SM 上驻留几十个 warp,这个卡住就切下一个。 延迟不是被消除的,是被别人的活盖住的
· 记分板 + 动态发射 —— 硬件在运行时盯着每条指令的操作数到没到,到了才发。
· 缓存本身就是一台猜测机器 —— 它赌你还会再用一次。

TPU 走的是反方向:把不确定性从源头删掉。 没有硬件缓存 → 没有缓存缺失 → 没有可变延迟。 VMEM 里的数在那儿,不是因为运气好,是因为编译器自己发的那条 DMA 把它放在那儿的, 而且它也知道该在第几拍等。

一句话对照:GPU 用「总有别的活可干」来盖住不确定的延迟, TPU 用「把不确定性删掉」来避免它。 一个是统计学的答案,一个是确定性的答案。
—— 所以 TPU 没有 L2 不是缺功能。缓存是一台猜测机器, 而一台编译期就把每一拍排好的机器,不需要猜。
⭐ 顺带解释了一件事:两边的「调优」没有一样东西是重合的 GPU 上你调的是「怎么让别的活足够多」 ——  占用率、每个 SM 驻留多少 warp、访存合不合并、要不要牺牲寄存器换 warp 数。
TPU 上你调的是「怎么让编译器排得开」 ——  维度对不对齐、块怎么切、双缓冲够不够、scoped vmem 给多少。
—— 同一个词叫调优,动的东西一样都不挨着。 这也是为什么从一边转到另一边,经验几乎不能直接迁移 —— 不是难度问题,是你手上那些旋钮在对面根本不存在
⚠️ 但别把这条线读成「两边越走越远」—— 恰恰相反 现代 CUDA kernel 正在往 TPU 那一侧靠。 异步拷贝、TMA、显式 barrier —— FlashAttention 那类追峰值的 kernel 现在干的事,本质就是「我自己发异步搬运、自己管双缓冲、自己等」。
—— 那不就是在 GPU 上手写一个静态调度吗。 因为一旦你真的要峰值,「猜」就不够用了。
⭐ 这条线在 §3.5 的课后延伸里还会再出现一次(Blackwell 的矩阵乘 把累加器搬出寄存器、改成单线程发射)—— 同一个方向,两个不同的部件。

3.3 并行层级:哪几层是运行时才定的

先补一对坐标词 —— 下面这一节和 3.4 全靠它们说话, 而这两个词的字面会骗人

lane 和 sublane 是寄存器的形状 —— 不是内存的概念,也不是「大小」的概念 来源:JAX 公开源码中 v7 那一支的 num_lanes = 128 / num_sublanes = 8 一个向量寄存器 共 128 列 1 条 lane 红色这一竖条,自己一套 ALU 1 个 sublane = 横着的一整条 128 格那么长 = 512 B 共 8 行 整个寄存器 = 8 × 128 × 32 bit = 4,096 B = 4 KiB ⚠️ 「sub」不是「更小」—— sublane 不是 lane 的一小段,它是横跨全部 128 条 lane 的一整条 立画面只要两步:先只看一条 —— 128 格那么长;再叠 8 条 —— 就是一个寄存器。 记法:最内维 → lane,次内维 → sublane ⭐ 然后 XLA 故意把内存布局做成同一个形状 数组 […, C, D] 落到内存里:最内维 D → lane(128 一组),次内维 C → sublane(8 一组)。 于是一个 [A, B, C, D] 的数组,在内存里是 A × B × ⌈C/8⌉ × ⌈D/128⌉ 个 4 KiB 的块。 直接后果:⚠️ 「一行」在内存里并不连续 —— 它被切碎在一排块里。下一片讲这件事要付多少钱。
先只看一条 —— 128 格那么长;再叠 8 条 —— 就是一个寄存器。
lane 量的是长度(第几格,0–127),sublane 量的是第几条(0–7)。
⚠️ sublane 不是 lane 的一小段,它是横跨全部 128 条 lane 的一整条 —— 「sub」说的是第二维,不是更小。
XLA 故意让内存布局等于寄存器形状,代价是「一行」在内存里不再连续。

刚才少掉的是硅片上的部件。往上一层,编程模型里也会少掉东西 ——  同一个原因,第二次露面。

这一节先归到一个问题上。

⭐ 这一节所有东西都从这一句长出来 你写 GPU 代码的时候,不知道这堆活会落在哪个 SM 上,也不知道什么时候落。
是硬件在运行的时候才决定的。

既然不知道,你就没法点名写不出「让 3 号 SM 干这个」。

先想三十秒 点不了名,那你还能对硬件说什么? —— 提示:你手上唯一还剩的信息,是这些活彼此之间的关系。

你能做的只剩一件事:描述关系哪些活必须待在一块儿,哪些落哪儿都行。

「必须待在一块儿」的那一批,就是一个 block。

为什么是「必须」?因为这批活之间要互相传中间结果。
而中间结果放在 SM 自己身上那块小内存里,不在外面。
所以一批活一旦落到某个 SM 上,就不许再搬走 —— 搬走了,中间结果就找不着了。

反过来,互相不用说话的活,落哪儿都行。全部加起来,叫 grid

这两层是你说了算的。 你写代码,其实就是在说两句话:这些必须在一起,总共这么多。

硬件那边还有它自己的分组,理由完全不一样 —— 是为了省电路

取一条指令很贵。那就让 32 份数据共用同一条指令,一起算。
这 32 个绑成一捆,就是 warp这个数你改不了,硬件焊死的。

到这儿四层齐了:底下两层硬件定死,上面两层你定。

剩下两层是后来补的,各被一个很具体的麻烦逼出来

warp group —— 新一代的矩阵指令,要四捆一起发才凑得出形状。
cluster —— 相邻几个 SM 想直接够到对方那块小内存,不想绕远路。

六层就齐了。每一层各自被什么逼出来,横着摆一遍

那六层为什么是六层 —— 每一层都是被一个具体的麻烦逼出来的 包含关系与「谁定的」看那张折叠全表,每层钉在哪块硅上看《GPU 显微镜》G-3 —— 这张只答一个问题:为什么要有这一层 有多大 它保证了什么 ⚡ 为什么会有这一层 grid grid 整个 kernel 的全部 block 只剩 L2 和 HBM —— 没有片上快捷通道 —— 它是边界,不是被逼出来的 cluster thread block cluster ≤ 8 块(H100 起可 opt-in 到 16) 同一个 GPC 内,能直接读写别的块的 shared memory 一个 SM 那 227 KiB 不够用了。代价:邻居的暂存要走 GPC 网络,比自己那块慢 线程块 CTA / block ≤ 1,024 线程 = ≤ 32 warp 保证落在同一个 SM:共享 shared memory、能 __syncthreads 「谁跟谁能共用暂存、能对齐脚步」需要一条边界 —— CTA 就是这个作用域 warp 组 warpgroup 4 个 warp = 128 线程 一条 wgmma 由这 128 个线程整体发出 Tensor Core 大到一个 warp 的寄存器喂不饱了,发指令的单位只好往上抬一级 warp warp 32 个线程 共用一条指令流;分支分歧只在这一层内发生 取指译码太贵。取一次、译一次、32 份数据一起算 —— 管理成本摊薄 32 倍 线程 thread 1 个 自己的寄存器。没有自己的命运 —— 跟着所在的 warp 走 —— 它是边界,不是被逼出来的 ⚠️ 三个最容易记错的点:1,024 是「一个 block 的上限」(=32 warp),不是一个 SM 的上限。 一个 SM 同时驻留 64 个 warp = 2,048 线程(B200 / H100 / A100 这几代如此,消费级和 Turing 更少)—— 这才是「有没有别的活可切」的本钱。 warp 内「天然锁步」是 Volta 之前的事—— 现在每个线程有独立的 PC,要对齐得显式写 __syncwarp()。 ⭐ 这六层没有一层是为了「让程序好写」加的 它们合起来,就是「靠运行时适应」这条路线的组织成本—— 要在运行时换人、分工、组队,就得把这些层级做进硬件,再配上调度器、记分板、常驻的巨大寄存器堆。 ✕ TPU 这一整列是空的。它不做「运行时换人」这个决定,于是这六层同时失去存在的理由 —— T-3 那张对照表上的两个空格,就是这一列不存在时的样子。
六层里只有 gridthread 是边界,其余四层每一层都对应一个具体的硬件麻烦:取指译码太贵,于是 32 个线程共用一条指令流;Tensor Core 大到一个 warp 喂不饱,于是发指令的单位抬到 4 个 warp;共享暂存需要一条作用域边界,于是有了 CTA;一个 SM 的 227 KiB 不够用,于是允许跨 SM 组簇。它们合起来就是「靠运行时适应」这条路线的组织成本 —— 而 TPU 不做那个决定,这一整列同时失去存在的理由。
⭐ 这一节的落点 上面那一列硬件麻烦,还能再往回退一步。
这六层,全是从最开头那句「不知道」长出来的。
正因为运行时才决定,你才需要一整套语言来描述「谁跟谁必须在一起、谁能看见谁」。

而 TPU 那边,编译的时候就已经把一切定死了 —— 不存在「不知道」。

所以这一整套语言,它一层都不需要

两边的层根本不在回答同一个问题 ——  GPU 那些层是用来描述「谁必须在一起」的,而 TPU 压根不需要描述这件事。
所以下面这张按问题排,不按名词排。

并行层级逐层对照 —— 按问题对齐,不按名词对齐 NVIDIA B200 TPU v7 TPU 上没有这一层 问的是同一个问题 NVIDIA B200 TPU v7 最小的那个东西是什么 一次能被单独指名道姓的最小执行体 1 个 thread 有自己的程序计数器、自己的寄存器。可以走自己的分支 1 个元素 只是向量里的一格。没有程序计数器,也不能走自己的分支 —— 它不是执行体, 是数据。 硬件天然锁步的一组是多少 这一组必须一起执行同一条指令 1 个 warp = 32 条 lane 32 个 thread 锁步。分支不一致时两边都要走 一遍 1 条向量指令 = 8 × 128 每格 = 8 条 lane 8 个 sublane × 128 条 lane、共 1,024 个 元素一起动。连「分支不一致」这个概念都没有 —— 没有分支可言。 共享一块暂存的是哪一组 谁和谁能通过片上暂存互相看见数据 1 个 thread block 同一个 block 里的 warp 共享一块 shared memory,由程序员在核函数里划 定,运行时才知道有几个 ✕ 没有这一层 VMEM 属于整个 TensorCore,不属于某一组。谁能看见什么,编译期就定死了 —— 没有「一组」这个中间概念。 运行时谁决定接下来跑哪个 延迟出现时,硬件有没有别的活可切 4 个 warp 调度器 / 64 个 warp 槽 某个 warp 卡在访存上,调度器立刻换一个能跑的。这就是 GPU 藏延迟的全 部秘密 ✕ 没有这一层 指令什么时候发、数据什么时候到,编译期就排死了。卡住了就是真的空转 —— 没有别的活能顶上来(见 §7)。 一个物理核里有什么 最小的、自带完整控制通路的硬件块 1 个 SM ×148 128 个 CUDA Core + 4 个 Tensor Core + 228 KB L1/共享内存 (容 量为第三方) 1 个 TensorCore ×2 2 个 MXU + 1 个 VPU + 64 MiB VMEM + 1 MiB SMEM 一颗芯片对软件是几个 框架里 devices() 数出来是几 1 个 两个 die 由一致性总线缝成一个 GPU。跨 die 更慢,但 API 里看不出来 2 个 两个 chiplet 如实暴露成两个 device。缝在明处,由你的分片策略面对 不换协议能连到多大 超出这个规模就得换一套互联 72 颗 NVLink 5 一个 NVLink 域。再往外换 InfiniBand/以太网,编程模型也跟着换 9,216 颗 ICI 4.0 3D 环面一路铺到整个 pod,全程同一套 ICI。这是互联那一节的落点。 看这张表要看的是空格 七行里有两行 TPU 那一栏是红的。而这两行不是随便哪两行 —— 它们恰好是 GPU 用来在运行时藏住延迟的那两层。 空的第一格:没有「一组线程」 GPU 的 block 是个运行时概念:有几个 block、落在哪个 SM 上,启动时才知道。TPU 没有这个中间层,所有归属在编 译期就写死在指令里。 空的第二格:没有「换一个跑」 GPU 一个 warp 卡住就换下一个,这需要同时驻留几十份上下 文 —— 那 256 KB 寄存器堆主要就是为它准备的。TPU 不留这 些上下文,所以也省下了那片面积(见 §2)。 于是 TPU 的层级全在描述「形状」 lane、sublane、tile、slice —— 每一层说的都是数据长什 么样,而不是谁在执行。这就是为什么 TPU 编程里你调的是分 片策略,而 CUDA 编程里你调的是线程组织。顺带解释了一件 常被问到的事:为什么 TPU 上「跑一个不规则的算法」这么别 扭 —— 不是编译器不肯,是硬件层级里根本没有一个能承载「 不规则」的单位 一句话总结这张表 GPU 的层级是「执行体的层级」,TPU 的层级是「数据形状的层级」。thread、warp、block 说的都是谁在跑;lane、sublane、tile、slice 说的都是数据被切成什么样。 这个区别有个非常实际的后果:GPU 的性能问题多半出在「占用率」上(同时驻留的 warp 够不够多,能不能把延迟盖住);TPU 的性能问题多半出在「形状」上(矩阵维度对不对齐、切片切得均不均匀)。两边的调优直觉不能互相搬运 —— 这也是为什么收缩维那一节里的 head_dim=128 打 TPU 却不打 GPU。
图 T-3 图 T-3 TPU 这边刻意按问题对齐,不按名词对齐。名词对名词能排出一张漂亮的翻译表,按问题排才看得见有两行 TPU 那一栏是空的。空的那两行正是 GPU 的 block 和 warp 调度器。 —— 出自《TPU 显微镜

CUDA 那套六层套娃 thread → warp →(warp group)→ block → cluster → grid 的完整包含关系折在下面,回查用 ——  真正要记的是它后面那三处「不对齐」。

六层包含关系全表(回查用 · 讲的时候跳过)
有多大谁定的落在哪块硬件上
thread 线程1 个—— 一个处理块里的 1 条 lane
warp32 个 thread 硬件写死,不是你选的 一个 SM 四个处理块(sub-core)中的一个
warp group4 个 warp = 128 thread 指令集写死 一个 SM 的四个处理块各出一个 warp
block 线程块
(= CTA)
1,024 thread = ≤ 32 warp 你写代码时定 整块钉死在一个 SM 上,落下就不迁走
cluster8 个 block
H100 起可 opt-in 到 16
你定,可选(Hopper 才有) 同一个 GPC 内的若干 SM
grid一次 kernel 的全部 block 你定整颗 GPU(B200 是跨两 die 的 148 个 SM)
⭐ 光背包含关系不会用 —— 真正要记的是三处「不对齐」 ① 一个 SM 上住的不止一个 block。 block 上限是 32 个 warp,而一个 SM 有 64 个 warp 槽(=2,048 线程)——  所以同一个 SM 上会同时住好几个 block,它们互相看不见对方的共享内存。 5.1 说的「占用率」,量的就是这 64 个槽填了几个。
② warp group 不是调度单位,是指令单位。 没有「一个 warp group 被调度上去」这回事 ——  它是 wgmma/TMEM 这类指令要求四个连续 warp 一起发才凑得出的形状。 这就是 3.4 那五代矩阵指令「动员多少线程」越来越多的那条线。
③ cluster 是后加的一层,加它的理由 3.2c 已经说过。 148 个 SM 之间除了 L2 没有别的路;cluster 让同一个 GPC 里的块 直接读写对方的共享内存,绕开 L2。 —— TPU 不需要这一层,因为它一颗 chip 上只有两个 TensorCore。

一句话记法:前三层的边界是硬件定死的,后三层的大小是你写代码时定的。 —— 每一层「能共享什么、怎么同步」的完整版在 GPU 显微镜 图 G-3。
三个名字的来历 —— warp / CTA / warpgroup (是词源不是机制,听不懂上面那张图才需要展开)

warp 出自织布:织布机上纵向绷紧、被一起拉动的那排线叫经线, 英文就是 warp。取的是「一排并排绷着、一起动」的意象。 中文标准译法线程束,「束」这个字抓到了重点 —— 它们是被捆在一起的。

为什么是 32?越宽,取指译码摊得越薄, 但分支分歧的惩罚越重、访存合并的粒度越粗。 AMD 早年用 64(叫 wavefront),新架构也退回了 32。

CTA 全称 Cooperative Thread Array它就是 thread block ——  CUDA 那一层叫 block,到了 PTX 和硬件那一层叫 CTA。看到两个词别当成两层。

warpgroup 除了发 wgmma,还带出一种写法叫 warp 分工(warp specialization):一个 warpgroup 专管从 HBM 往 共享内存搬数据,另几个专管发矩阵指令 —— 搬的和算的分开,流水线自然叠起来FlashAttention-3 就建在这套上 —— 3.6 并排走那一遍,GPU 侧就是这个形状。

再往里一层。「一个 SM」到此为止一直是个黑盒 ——  拆开它,上面那些数才不是背下来的。

一个 SM 拆开看 —— 它不是一整块,是四个格子 warp 为什么是 32、住得下多少人、换 warp 为什么不要钱 —— 这三个问题的答案都在这个画面里 1 个 SM(B200 全片 148 个) 处理块 0 sub-core / quadrant 1 个 warp scheduler 32 个 CUDA Core 1 个 Tensor Core 64 KiB 寄存器 · 16 个 warp 槽 处理块 1 sub-core / quadrant 1 个 warp scheduler 32 个 CUDA Core 1 个 Tensor Core 64 KiB 寄存器 · 16 个 warp 槽 处理块 2 sub-core / quadrant 1 个 warp scheduler 32 个 CUDA Core 1 个 Tensor Core 64 KiB 寄存器 · 16 个 warp 槽 处理块 3 sub-core / quadrant 1 个 warp scheduler 32 个 CUDA Core 1 个 Tensor Core 64 KiB 寄存器 · 16 个 warp 槽 四个格子共用:228 KB 的 L1 + 共享内存(单个线程块最多要走 227 KiB)· TMA 搬运引擎 · 通往 L2 的出口 ⛔ 一个 warp 一旦被分到某个格子,就在那儿待到死 —— 不迁走。这条「钉死」是后面所有「除以 4」的前提。 四格加起来 = 整个 SM 128 个 CUDA Core 4 个 Tensor Core —— 不是 1 个 65,536 个 32-bit 寄存器 = 256 KiB 64 个 warp 槽 = 2,048 个线程 最多 32 个 线程块 ⭐ 寄存器堆 256 KiB,和旁边那块 L1 + 共享内存一样大(也是 256 KiB,其中最多 228 可划给共享)。 一块芯片上最贵的 SRAM,整整一半拿去当了「让人待命」的本钱。 住得下多少人?三个闸门,谁先卡住算谁—— 这就是「占用率」这个词量的东西 ① 寄存器 —— 最常先卡住的那个 65,536 ÷ 2,048 = 32 每线程只能用 32 个,才住得满 每线程 32 个 → 2,048 线程 占用率 100% 每线程 64 个 → 1,024 线程 占用率 50% 每线程 128 个 → 512 线程 占用率 25% 每线程 255 个(上限) → 256 线程 占用率 12.5% ⛔ 循环展开、把中间结果留在寄存器里 —— 都在削减能待命的人数。 ② 共享内存 一个 SM 一共 228 KB,单个线程块最多要走 227 KiB。 你的 block 要 100 KiB —— 这个 SM 就只住得下 2 个 block 寄存器还剩多少都没用了。 ⭐ 你问的「100 个线程把资源占没」—— 真会发生的是这一条。 一个 block 独吞 227 KiB,整个 SM 就只剩它一家,最多 1,024 个线程。 ③ 槽位本身 最多 64 个 warp、最多 32 个 block block 开得太小也吃亏:每块只有 32 个线程时, 32 块 × 1 warp = 32 个 warp, 64 个槽只填得满一半 资源一点没超,人就是招不满。 ⚡ 那为什么不干脆把 warp 做成 128,省得四个凑一堆? 先把问题翻译一下:32 就是一个格子的宽度,128 就是四个格子各出一个 warp。所以「把 warp 做成 128」=「把四个格子合成一个」。 ① 合成一个就只剩一个 warp scheduler。现在四个格子能同时跑四条毫不相干的指令流 —— 合并了这个能力就没了。 ② 绝大多数指令根本不需要 128 宽(访存、整数、分支、超越函数)。只有 wgmma 那一类要。为它加宽,等于让全部代码替矩阵乘付账。 ③ 32 × 4 B = 128 B = 正好一条 cache line;而分支发散的惩罚随宽度线性变重 —— 一个 if 走岔,整组都得两边各走一遍。 ⭐ 换 warp 是零成本的 —— 而这张图就是那张账单 CPU 换线程要把寄存器存下来再恢复;GPU 从来不搬 —— 2,048 个线程的寄存器一直物理占着,谁的数据到了就发谁。 代价就是那 256 KiB。「运行时才知道数据什么时候到」—— 为这一条付的账里,最贵的一项不是调度器,是这堆养着待命者的寄存器。
四个格子这个画面一立起来,三件事同时有了答案:32 是一个格子的宽度, 128 是四格各出一个 warp;能住多少人由三个闸门里最紧的那个说了算, 而最常卡住的是寄存器 —— 65,536 ÷ 2,048 = 每线程只能用 32 个才住得满; 最后,换 warp 之所以零成本,是因为那 2,048 份寄存器根本不搬最后这一条是「运行时适应」账单上最贵的一项 —— 256 KiB,比旁边的 L1 加共享内存还大。
一个线程独占什么、又跟谁共享什么(那笔归属账,展开看)
⭐ 一个线程独占什么,又跟谁共享什么 算的部件,它一样都不占。
CUDA core、Tensor Core 都不是分给谁的 —— 它们是流水线上的一个位置。 哪一捆 warp 这一拍发了指令,就用这一拍,下一拍换别人。
Tensor Core 更极端:一条指令是整捆 warp(Hopper 起是四捆)一起发的 —— 「某个线程的 Tensor Core」这东西不存在。

它真正独占的只有寄存器,加上所在那一批分到的共享内存。
一批活落到 SM 上就把这些划走,跑完才还,中途从不搬动。

这一条同时解释了两件事。
正因为寄存器不搬动,换一捆 warp 上来是零成本的 —— 上一捆的东西原地放着就行。
也正因为独占,先卡住你的一定是寄存器不够,不是算的部件不够。
寄存器到底怎么数 —— 单位、上限、以及为什么是 8 的倍数 (回查用 · 讲的时候跳过)

一个寄存器就是 32 位、4 个字节,编译器报的「每线程多少个寄存器」数的就是它。 这条能自己验:65,536 × 4 B = 256 KiB,正好是图里那个寄存器堆。

每线程有上限:现在的卡都是 255 个(历史上变过,早期只有 63)——  就是上面那张图里「每线程 255 个(上限)」那一行。 ⚠️ 这个数 NVIDIA 自己两处对不上:编程指南写 255, 而 CUDA 自带的 cuda_occupancy.h 从 Volta 起写 256。 我没能确认哪个是硬件真值(大概是「能分配 256、能用 255」,但那只是解释)——  按 255 算
而且不是一个一个分的,是按 warp 打包:粒度是 256 个一包, Kepler 到 Blackwell 一路没变 —— 先算「每线程个数 × 32」,再取整到 256 的倍数。
—— 换算到线程头上就是向上取整到 8 的倍数所以挤掉一两个寄存器占用率常常纹丝不动,要动得跨过 8 这道坎。 出处不是文档是代码:CUDA 自带的 cuda_occupancy.h 里按计算能力分档写着这几个数。

64 位的东西占两格 —— double 和指针。 多留几个指针,占用率掉得比你预期快。

最后回到最底下那一格。

一捆 warp 是 32 个线程。每个线程在这一捆里有个编号,0 到 31 ——  那个位置,就是一条 lane。

所以 GPU 上也有 lane。硬件上它就是一条 SIMD 通道, 跟 TPU 那边的 lane 是同一样东西。

差别只在一件事:GPU 的 lane 有名字,TPU 的没有。

GPU 那条有编号能写出来,你可以对它单独写 if、写循环,看着就像在写一个真线程。
TPU 那条你碰不到 —— 写不出「第 37 号 lane 单独干点别的」
你只能让整条向量一起动,哪个元素落在哪条 lane 上,是编译器的事。

所以 CUDA 说的「线程」,跟你平时写的那个线程不是一回事 ——  它究竟是什么,得把左右两边都摆上才说得清。

三个都叫「线程」—— 中间那个两头都沾:写法像左边,硬件像右边 CUDA 的 thread 借了操作系统线程的名字和写法,骨子里却是一条 SIMD lane —— 把左右两根柱子立起来,中间那根的怪异才看得见 操作系统的线程 CUDA 的 thread TPU 上对应的位置 有没有自己的取指、发射 有,完全独立的控制流 有自己的 PC(Volta 起),但发射单位始终是 warp —— 所以它不能自己决定下一条指令是什么 没有。它是一条向量指令覆盖到的一个位置 有没有「名字」 有 —— pthread_self() 有 —— threadIdx。这是 GPU 最特别的一点 你能写「第 37 号线程去干点别的」,而且它真的会去 没有。你写不出「第 37 号 lane 干点别的」 —— 能写的只有整块整块的操作 换人要多少钱 把寄存器存下来再恢复 —— 微秒级 零。寄存器物理常驻,从来不搬 代价是那 256 KiB 一直占着 —— 见「一个 SM 拆开看」那张 不存在「换人」这回事 —— 顺序编译期就排死了 一次处理多宽 你的代码说了算 一条 32-bit lane。访存可向量化到 128-bit,fp16 一拍两个 但基本盘就是一格 —— 跟你的直觉一致 8 sublane × 128 lane 那张网格里的一格 谁决定它什么时候跑 操作系统调度器,会被抢占 warp scheduler,运行时挑—— 谁的操作数到了就发谁 编译器,编译期排死。没有调度器 有多少个 几十到几百 一个 SM 驻留 2,048,全片 148 个 SM = 约 30 万 一条向量指令一次盖住 1,024 ⭐ 一句话:CUDA 的 thread 是「有名字的 SIMD lane 你按操作系统线程的写法去写它 —— 标量代码、if、循环,全都成立;但它没有自己的取指,三十二个一起听同一句口令。 而这个「有名字」,就是两边的分界线。GPU 把 lane 暴露给你,代价是它得配调度器、记分板、和那堆养着待命者的寄存器; TPU 把 lane 藏在编译器后面,于是省掉那一整套 —— 但你也就再没有「单独指挥某一条 lane」这个动作了。「那六层为什么是六层」那张,讲的是同一件事的另一半。
没有自己的取指,三十二个一起听同一句口令 —— 这一头像 TPU 的 lane; 可它有名字,你能写「第 37 号线程去干点别的」而且真的会去 —— 这一头像操作系统线程。 「有名字」就是两边的分界线:GPU 把 lane 暴露给你,代价是配齐调度器、记分板、 和上一张图里那堆养着待命者的寄存器;TPU 把它藏在编译器后面,省掉这一整套, 也就此没有了「单独指挥某一条 lane」这个动作。

3.3a 那一条 lane,在一次真矩阵乘里到底干什么

「一个线程就是一条 lane」这句话还太抽象。拿一次矩阵乘当场落地 ——  而这里必须分两个时代讲,混在一起一定说不清。

① 没有 Tensor Core 的经典写法。 一个线程 = 输出矩阵里一格(或一小块)的所有者。 它自己跑那个 K 循环:从共享内存把 A 的一段、B 的一段读进自己的寄存器, 乘加,累加在自己手上,最后写回。 —— 搬运和计算,它都干。这就是大多数人脑子里那个画面。

② 用 Tensor Core 的现代写法 —— 那个画面直接作废 矩阵乘指令是整个 warp 一起发的一条指令, 操作数是一个 fragment:同一块矩阵的元素, 按 PTX 规定的固定布局散在 32 个线程的寄存器里。
拿一条典型的 m16n8k16 举例:每个线程交出 A 的 8 个 fp16、 B 的 4 个 fp16、累加器 C 的 4 个 fp32
—— ⭐ 单个线程自己算不出任何一个完整的输出元素。 结果也是按 fragment 散回各线程手里。

所以「一个线程负责哪一格」这个问题, 到 Tensor Core 上就失去意义了
线程退化成两样东西:一份寄存器的持有者,和发那条指令的三十二分之一。 —— 这才是「一个线程其实是一条 lane」的完整含义。

⭐ 那这到底是不是 SIMD?—— 两个层面,答案相反 硬件上:是。一个 warp 的 32 条 lane 共享取指、译码、发射, 同一拍执行同一条指令 —— 这就是一套 SIMD 的硬件。
编程模型上:不是。你写的是标量代码,一个线程一份; 而且 Volta 起每个线程真的有自己的程序计数器,可以各走各的分支。

所以 SIMT 这个词的意思就是:用标量的写法,跑在 SIMD 的硬件上 代价写在分支上 —— 真发散了,各条分支只能轮流走, 看着能各走各的,实际是排队。 ⚠️ 还有第二层容易混:每个线程内部也能用向量指令 (一次取 128 bit)。那是「线程内的 SIMD」,跟 warp 那一层的 SIMD 不是一回事。

—— 而这一整套(让你写标量、硬件替你凑成 SIMD、发散了替你排队) 就是「靠运行时适应」要付的钱。TPU 不做这个决定,所以它那边 连「线程」这个词都没有 —— 只有 lane,而 lane 是不能单独指挥的。

⭐ 顺带回答「它管不管搬运」—— 管,但这一半正在退场Ampere 之前线程自己搬(全局 → 寄存器 → 共享),搬的时候它就被占住; Ampere 起 cp.async 让数据绕过寄存器直通共享内存; Hopper 起 TMA 一个线程发一条描述符就能搬一整块,另外 31 个不参与。
三代下来,线程从「搬运工 + 计算工」被剥成了「寄存器的容器」。 这条通路 3.3b 会拆开画(那张三代对照图), 到 Blackwell 更进一步 —— 累加器搬进 TMEM,连寄存器都不占了。

3.3b 「运行时适应」在硬件上是三套电路 —— 把账单打开

上一节的落点还只是编程模型那一层的说法 ——  它说的是你写代码时看到的形状。

往硬件里再走一步。这笔成本落地成三套电路,每一套只回答一个很窄的问题:

① 谁现在能发指令?
② 谁跟谁怎么对齐?
③ 数据怎么在不占用线程的前提下就位?

第一个问题这门课其实欠了一笔账:「记分板」这个词前面已经点过好几次名 ——  拆 SM 的时候、说 GPU 把 lane 暴露给你要付什么代价的时候, 却从没说过它是什么。先把这笔还上。

一句话还这笔账:记分板就是一张表 ——  表上记着每条指令要的操作数到齐了没有调度器每一拍扫一遍这张表,谁的操作数齐了就让谁发射。
—— 这就是「运行时才知道谁能跑」在硅片上的样子: 它得有个地方把「还没齐」这件事记下来,而 TPU 那边这张表整个不存在。

记分板是怎么知道哪个 warp「就绪」的(机制向,624 字,被问到再展开)
⭐ 记分板:调度器凭什么知道哪个 warp「就绪」 这名字不是 NVIDIA 起的,它来自 1964 年的 CDC 6600
那台机器第一次做到指令不按顺序完成 —— 于是需要一张集中的表, 记着哪个单元忙着、哪个寄存器正等人往里写。
那个部件的正式名字,就叫 Scoreboard。

回到 GPU。NVIDIA 先按一条线把指令劈成两半 —— 编译器知不知道它要跑多久。
定长的那一半根本不用硬件管:浮点乘加几拍出结果是固定的, 编译器直接在指令里写一个「停几拍」的数,硬件照做。 —— 这就是 SASS 每条指令前面挂着一串控制位的原因。
变长的那一半才是记分板:访存、特殊函数、矩阵指令,跑多久编译期不知道。 每个 warp 六个计数器(SB0SB5); 生产者发出时加一、写回时减一;消费者带一个六位掩码,写明要等哪几号归零。

⭐ 所以它不是「哪个寄存器脏了」的一张大表,是 六个很小的计数器加上编译器提前埋好的标记,只回答一个问题: 这个 warp 现在能不能发指令。 而这一个问题正是延迟隐藏的地基 —— 没有它,就没有「换一个 warp 上来」这个动作。

📌 顺手带走一条判据:Nsight 里的 long / short scoreboard, 分界线是等的东西出没出 SM —— 共享内存、特殊函数、矩阵指令算 short, 全局内存加载算 long。访存瓶颈的 kernel,long 一定是最大那一项。
⚠️ 六个计数器与控制位格式出自第三方微基准反推 (Analyzing Modern NVIDIA GPU cores, 2025),不是官方文档。
那为什么叫「记分」—— 以及这块板和 CDC 那块已经不是一回事 (词源与辨析 · 讲的时候可跳)

先说清哪句有据、哪句是推的。有据的是:这个词出自 CDC 6600, 那个部件的正式名字就是 Scoreboard。设计者本人为什么挑这个字,我没找到出处 —— 下面这段是推测,别当史料引。

英文 keep score 的本义不只是记比分,是把当下的状态记着: 球场边那块牌子的用处,是让你抬一次头就知道现在什么局面。这张表干的正是这件事。 所以中文译成「记分板」其实译窄了,叫「状态板」更贴。 —— 不过到了 GPU 这一代反倒名副其实:它真的在数数,发出去加一、写回来减一; 老那块牌子只是挂个「忙」的旗子。

更要紧的是别把两块板当成同一个东西。CDC 那块是一张集中的大表, 管全部寄存器的读写冲突,连「后写的别把先写的盖了」这类反向冲突也归它仲裁, 依赖判断全在运行时做
NVIDIA 这块只有每个 warp 六个计数器,只回答「我等的那件事回来了没有」; 定长指令的依赖压根不经过它,那部分是编译器提前写进控制位排掉的。
—— 名字继承下来了,机制小了一大圈。 缩掉的那一圈正好是本课主线的一个小注脚:能在编译期定的就别留到运行时 —— 连 GPU 自己也在往这个方向挪。

第二个问题的答案是 SM 里一小组硬件 barrier: warp 陆续到达,硬件数人头,数满全放行。
机制折在本节末尾,这里只带走最实用的那一条 ——

在 barrier 上等着的 warp 不腾位子。 所以同步密集的 kernel,占用率不会因为「大家都在等」而变好看。

第三个问题值一张图 —— 带着「哪一列在变、哪几列自始至终没变」去看

一块数据怎么进到共享内存 —— GPU 走了四代,TPU 出厂就在终点 只看两件事:数据在半路要不要经过寄存器,以及地址、跨步、边界是谁算的—— 这两件事一变,线程就从「搬运工」退成了「发号令的」 中转这一格 —— 整张图的差别都在这儿 地址、跨步、边界谁算 发起它的线程接下来干什么 一直都有 普通 ld / st 全局内存 寄存器 数据在这儿过一道 共享内存 Tensor Core 线程自己算,自己跑循环 被占住 —— 搬完才轮到算 Ampere 起 cp.async(SASS:LDGSTS) 全局内存 直通 不落寄存器 共享内存 Tensor Core 还是线程自己算 省了寄存器这一道, 地址和循环仍要自己跑 Hopper 起 TMA + 一张拷贝描述符 全局内存 TMA 引擎 按张量维度+块坐标 共享内存 Tensor Core 硬件算:跨步、偏移、 边界全由它接管 一个线程发完就走 搬完引擎自己去 barrier 报数 Blackwell 起 tcgen05 + 一块专用矩阵暂存 共享内存 TMEM 256 KB/SM 操作数与累加器都住这儿 矩阵暂存 Tensor Core 仍是硬件算;而这一代 连结果都不回寄存器堆 一个线程代表整个 CTA 发 要用结果得再显式取回来 TPU:出厂就是这样 编译器显式发的 DMA HBM DMA 引擎 和上面那行是同一类东西 VMEM MXU 编译期就排好了 这一栏对 TPU 不成立 ——  这边压根没有「线程」这个角色 ⭐ 一条线看下来:搬运从「取数指令的副作用」,变成了「一台按描述符干活的引擎 GPU 用了四代走到这儿,TPU 第一天就在这儿。这不是谁抄谁 —— 是同一个物理约束逼出来的同一个答案: 矩阵单元越快,喂料这件事就越不能占着算的人。⛔ 但别把这条线读成「GPU 在往编译期挪」 —— TMA 那张描述符是运行时建的 收敛的是怎么搬(整块、按描述符、不占线程);谁决定那一层一步没动 —— GPU 是运行时某个线程发,TPU 是编译期就排好的一步。这正是全课那条主线。 —— 「搬运是取数的副作用,还是一条独立的 DMA」这句话,在《GPU 显微镜》图 G-6 里已经出现过;这张图给的是它怎么一步步变成现在这样 ⚠️ 图上没有 L2,这是故意的 —— 它在这条路上是「透明」的 所有 global 访问都路过 L2(全片 126 MB、4 分区、在 SM 外面,所有 SM 共用),但它不是任何一步的目的地 —— 这几行搬运的终点都是共享内存 / VMEM。 别把共享内存当成 L2共享内存在 SM 里,和 L1 是同一块 228 KB;L2 在 SM 外、大三个数量级。TMA 引擎也在 SM 里(Hopper 起每 SM 一个),它的目的地同样是共享内存。 ⚠️ 生产者一换人,对齐的办法也得跟着换 第三行那台引擎不是线程,它不会「到达」,只会「往里放了多少字节」。而 __syncthreads() 数的是人头,对它没用。 所以 Hopper 的 barrier 多了一项本事:连字节数一起数—— 人到齐并且字节够了,才放行。这就是「异步事务 barrier」这个名字的来历。 📌 这一张全部出自官方:cp.async 的直通路径见 CUDA 编程指南;TMA 的单线程发起、硬件接管地址生成异步事务 barrier 数字节,是 Hopper 架构官方博客的原话。 Blackwell 那一行:tcgen05 由单个线程代表整个 CTA 发射TMEM 256 KB/SM 且由软件在 MMA 作用域显式管理 —— 出自 PTX ISA 与第三方微基准,本图不额外推导。
四行里只有中转那一列在变,两头是同一个东西。 第一行数据要在寄存器里过一道,而且地址和循环全靠线程自己跑; cp.async 砍掉了寄存器那一道,地址仍要自己算; 到 TMA 才把地址生成整个接管过去 —— 一个线程填张描述符发出去就走。 ⭐ 走到这儿,它和 TPU 那条编译器发的 DMA 已经是同一类东西; 剩下的分歧只有一个 —— 这条 DMA 是运行时某个线程发的,还是编译期就排好的一步
另外两个问题的答案:block 里怎么同步、cluster 怎么读到别的 SM (回查用 · 讲的时候跳过)

一、block 里怎么同步。 __syncthreads() 背后是 SM 里一小组硬件 barrier 资源: warp 陆续到达,硬件数人头,数满全放行。三个容易忽略的点 ——  ① 按 warp 到达,不按线程,因为一个 warp 本来就一起走; ② 它同时兼一个内存栅栏,barrier 之前写进共享内存的东西之后别人一定读得到, 所以不用另写 fence; ③ 等在 barrier 上的 warp 仍占着它的槽 ——  这一条已经提到主线上去了,不在这儿重复。

Ampere 起多了异步 barrier:把「到达」和「等待」拆开。 干完先喊一声「我到了」,不阻塞,接着去做别的独立的活,真需要数据时才等。 早到的人不用干站着。Hopper 又让等待的线程能真的睡过去 —— 以前是在共享内存上自旋。

还有一处很实际:两种 barrier 住在不同地方。 老的 bar / barrier 是 SM 里固定的一小组资源, 每个 CTA 能用的个数有限;mbarrier 是放在共享内存里的对象, 要多少个建多少个,只受共享内存容量限制—— 这句是 PTX 文档自己写的。

二、cluster 怎么读到别的 SM 的共享内存。三件事凑齐才成立 ——  ① 同一个 cluster 的 block 被硬件保证同时调度上去; ② GPC 内部有一条专用的 SM 到 SM 网络cluster 必须整个落在一个 GPC 里,正是因为这条线只铺到 GPC 边界; ③ 地址靠 cluster.map_shared_rank(指针, 目标块编号) 换来一个指向对方那块内存的地址(编译成 PTX 的 mapa), 有了地址普通读写指令就发得出去,网络负责路由。

⚠️ 它有多近,比「能不能」更值得记 H100 微基准实测:读自己的共享内存 29 拍; 走 DSM 接口读自己的 33 拍(多出的 4 拍是接口本身,一步没跨出去); 跨到另一个 SM 181 拍,比走 L2 低约三成; 绕全局内存一趟(一存一取)要 956 拍

所以它不是「把几个 SM 的共享内存拼成一块大的」 ——  181 拍是本地那 29 拍的六倍多。它是一条比绕 L2 便宜的近路, 用它的理由是省掉那九百多拍,不是因为它跟本地一样快。
⭐ 三套硬件,同一张账单 记分板回答「谁现在能发指令」,barrier 回答「谁跟谁怎么对齐」, TMA 回答「数据怎么不占用线程就位」。 三套加起来,就是「跑起来才决定」这条路的明细账。

TPU 一样都不需要 —— 不是它简陋,是这三个问题它在编译期就全回答完了—— 这也是 3.2 那五样「少掉的部件」的第二次收账:少掉的不是能力,是这笔账。

3.3c ⭐ 矩阵乘在硬件上到底是按什么顺序跑的

到这里,两边的部件都拆完了。但有一件更基本的事,前面一直没说: 一次矩阵乘,在硬件上究竟是按什么顺序算的?

这一节大概是全课唯一一处「几乎所有人都想错了、而且错得毫无察觉」的地方。 因为错的那个版本太自然了 —— 它就是你在纸上算矩阵乘的方法。

先把你脑子里那个说出来:拿左矩阵的第一行、右矩阵的第一列, 对应相乘再加起来,得到结果矩阵左上角那一个数;这个格子就算完了, 换下一个。—— 数学上完全正确。 而真实硬件,无论 GPU 还是 TPU,一个都不这么算。

同一个矩阵乘,三种把它铺在硅上的方式 —— 结果一模一样,数据流完全不同 几乎所有人脑子里的矩阵乘都是「先把一个点积算完」—— 而硬件一个都不这么做。这张图就是为了把那个差别摊开。全程用同一个小例子:A 是 2×3,B 是 3×2,C 是 2×2(M=2,N=2,K=3)。 ① 朴素算法(内积序)—— 你脑子里那个。一个累加器就够,但它几乎没有复用 写成代码就是三层循环,k 在最里面:先把 C 的一个格子彻底算完,再去下一个格子。 A(2×3) a₀₀ a₀₁ a₀₂ a₁₀ a₁₁ a₁₂ × B(3×2) b₀₀ b₀₁ b₁₀ b₁₁ b₂₀ b₂₁ = C(2×2) c₀₀ c₀₁ c₁₀ c₁₁ 第 1 步:拿 A 的整个第 0 行(3 个数)× B 的整个第 0 列(3 个数),乘加完 —— 得到 c₀₀ 一个格子,写出去。 第 2 步:还是 A 第 0 行,换 B 第 1 列,得到 c₀₁。⛔ A 第 0 行那三个数,你又读了一遍。 第 3、4 步同理,A 第 1 行也要读两遍。 数一下这笔账 A 的 6 个数各读 2 遍 = 12 次;B 的 6 个数各读 2 遍 = 12 次。总共读 24 次,只换来 12 次乘加。 复用率 0.5 —— 读两次才干一次活。所以真实硬件一个都不这么算。(累加器确实只要 1 个,代价是把操作数反复搬。) ⭐ 卡住很多人的正是这一步:「一个格子算完就退休」的画面太自然了,自然到没人怀疑硬件不是这么干的。 ② GPU 的算法(外积序)—— 把 k 挪到最外层,M×N 个格子同时往上涨 每走一步 k,只读 A 的第 k 列(M 个数)+ B 的第 k 行(N 个数),然后这些数两两配对,一次更新全部 4 个格子 k = 0 b₀₀ b₀₁ B 第 0 行 a₀₀ a₁₀ A 第 0 列 +1 +1 +1 +1 累加器 4 格全动 4 个数 → 做 4 次乘加 → 4 个格子各涨 1/3 k = 1 b₁₀ b₁₁ B 第 1 行 a₀₁ a₁₁ A 第 1 列 +1 +1 +1 +1 累加器 4 格全动 4 个数 → 做 4 次乘加 → 4 个格子各涨 1/3 k = 2 b₂₀ b₂₁ B 第 2 行 a₀₂ a₁₂ A 第 2 列 +1 +1 +1 +1 累加器 4 格全动 4 个数 → 做 4 次乘加 → 4 个格子各涨 1/3 ⭐ 三步走完,K 到头,四个格子同时算完,一起倒出去—— 总共读 12 次,做 12 次乘加,复用率 1.0,整整好一倍 回答「每个格子被用到几次」:恰好 K 次,一次不多一次不少,左上和右下完全一样。每走一步 k,所有格子同时被更新一次 —— 累加器总更新次数 = M×N×K = 乘加总次数。 ⚠️ 这里画的是算法级的数据流。指令级上,一条 MMA 已经在内部把 K 方向的一小段(16/32/64 个)规约掉了—— 形状完全一样,只是成批做 ③ TPU 的 MXU(脉动阵列)—— K 方向的求和不靠累加器,靠「沿着阵列往下流」 B 先装进阵列,装完就不动(weight-stationary)。A 一行一行地从左边流进去,部分和一路向下累加,从底下出来时已经是算完的一行 C。 A 的行(一次一行,走的是时间) a₀₀ a₀₁ a₀₂ a₁₀ a₁₁ a₁₂ t = 0 送第 0 行,t = 1 送第 1 行 MXU 阵列 —— 真机是 256×256 K(3)= ,N(2)= —— 两个都是空间维 b₀₀ b₀₁ b₁₀ b₁₁ b₂₀ b₂₁ C 的一行,从底下出来 每个 PE 干的事 ① 从左边接一个 a ② 乘上自己怀里那个 b ③ 加上从上面流下来的部分和 ④ 把 a 往右传,把新的部分和往下传。就这四步,没有别的。 所以 K 方向那 3 项求和,是「空间上」完成的—— 三个 PE 各做一次乘加, 把部分和接力往下传。这 3 项不需要任何外部累加器。 外部累加器只在跨 K 分块时才用到—— 真机 K 常常几千, 阵列一次只吃得下 256,剩下的按块累加。 M 走的是时间:A 的行一行一行进去,一行一行出来。 复用的账两边其实一样:TPU 这边,B 装一次被 M 行反复用,A 的一个数进来一次被同一行的 N 个 PE 用—— 跟 GPU 那边靠二维累加器换来的复用,是同一笔账的两种物理实现 ⭐ 落点:同一个数学,区别只在「哪个维度摊在空间上,哪个维度走时间」 ① 朴素(内积序) ② GPU(外积序) ③ TPU(脉动阵列) M(左矩阵的行) 时间 空间:累加器的行 时间:一行一行流进去 N(右矩阵的列) 时间 空间:累加器的列 空间:阵列的列 K(求和那一维) 时间 时间:累加多少步 空间:阵列的行 累加发生在哪 一个寄存器里,反复加 M×N 个格子里,各被更新 K 次 沿着阵列往下传,边流边加 要多大的累加器 1 个 M×N 个(真机是一个 tile,不是整个矩阵) 只需跨 K 分块的那一份 复用率 0.5 M×N ÷(M+N)—— 128×256 的块约 85 B 用 M 遍、A 用 N 遍,账同上 一句话:GPU 把 M 和 N 摊在空间上、让 K 走时间;TPU 把 K 和 N 摊在空间上、让 M 走时间。—— 这一个选择,决定了后面所有事:累加器要多大、tile 为什么是核心旋钮、以及 TPU 为什么非得让权重坐着不动。 而三种铺法算出来的 C,一个数都不差。「结果相同、数据流不同」正是这门课想让你养成的那种看法—— 规格表只写结果,快慢全在数据流里。
三种铺法算出来的 C 一个数都不差,数据流却完全不同。 ① 朴素(内积序):先把一个格子彻底算完 —— 只要 1 个累加器, 但读 24 次才干 12 次活,复用率 0.5。 ② GPU(外积序):k 挪到最外层,每步只读 A 的一列 + B 的一行, M×N 个格子同时各涨一点,每个格子恰好被更新 K 次。 ③ TPU(脉动阵列):B 装进阵列就不动,A 一行一行流过, K 方向的求和是沿着阵列在空间上完成的,压根不经过外部累加器。 ⭐ 落点在最后那张表:GPU 把 M 和 N 摊在空间上、让 K 走时间; TPU 把 K 和 N 摊在空间上、让 M 走时间。
⛔ 错的不是数学,是数据流 —— 而规格表只写数学 内积序和外积序算出来的结果一模一样,一个数都不差。 所以你永远不会因为「算错了」而发现自己想的不是硬件干的那件事。
它们差在要搬多少数据上,而这才是决定快慢的那一半。
—— 内积序:读 24 次,做 12 次乘加。外积序:读 12 次,做 12 次乘加。 这还只是 2×2 的玩具例子;块一大,差距就是几十倍上百倍。
⭐ 这一节要带走的那条算式:累加器买的是复用率 为什么不能只留一行累加器?—— 有一条特别干净的算式能回答。

假设累加器是 M×N 的一小块。K 方向每往前走一步,你要读进来的是: A 的一列(M 个数)+ B 的一行(N 个数),一共 M + N 个数。 而这一步你能做多少次乘加?M × N 次。

所以复用率 = M × N ÷(M + N)。
累加器形状复用率读一个数干几次活
1 × N(只留一行)N ÷(1+N)≈ 1,N 开到一百万也一样
128 × 12816,384 ÷ 25664
128 × 25632,768 ÷ 384≈ 85
这条算式最狠的读法:它被短边卡死。 短边是 1,那长边开到一百万,复用率还是 1 —— 长的那一边一点忙都帮不上。
—— 所以累加器阵列不是用来「装结果」的,它是用来买复用的。 你多开一个累加器,就等于让搬进来的那批操作数多配对一次。 ⭐ 这正是第 1 节那条算术强度的主线,第一次长在了硅上。
⭐ 还有一个从电路上看的角度,同样说明它必须是二维的 MXU 是一个 256×256 的二维阵列,每一拍物理上要吐出 256 个部分和。 你如果只有一行累加器,那就是拿二百五十六分之一的阵列在干活,其余全空转。
—— 二维的阵列,天然要配二维的累加器。累加器的形状就是阵列的形状, 不是谁随手定的。

顺带回答另一个常见的追问:那「倒出去」不是也很费吗? 不亏。不管怎么切,最后倒出去的总量都是 M×N,一个字节不多不少。 真正被多付的是 B 那一侧 —— 一行一行来的话,整个 B 你得重读 M 遍。
⚠️ 两条准确性上的边界,讲的时候别丢 图里 GPU 那一栏画的是算法级的数据流。指令级上,一条 MMA 已经在内部把 K 方向的一小段(16/32/64 个)规约掉了 ——  形状完全一样,只是成批做「累加器跨 k 块持续累加」这件事是明写在 指令里的:第一块把累加器清零,之后每一块都是加上去。
TPU 那一栏,K 方向的求和在阵列内部沿空间完成, 那一段不需要外部累加器外部累加器只在跨 K 分块时才用到 —— 真机 K 常常几千,而阵列一次只吃得下 256。

把这一节收成一句:同一个数学,区别只在「哪个维度摊在空间上,哪个维度走时间」。 这一个选择往下决定了所有事 —— 累加器要多大、tile 为什么是最核心的那个旋钮、 TPU 为什么非得让权重坐着不动。 接下来 3.4 要比的「一条指令吃多大一块」,比的就是这个选择的具体取值。

3.4 一条指令吃多大一块 —— 收缩维差 16 倍

上一节讲的是谁来发这条指令。这一节换一个问题 ——  一条指令发出去,一次能吃下多大一块?

这个问题听起来很内部,但它会直接决定一件很外部的事: 你的 head_dim 选多少不吃亏。

Tensor Core 一次吃多大矩阵 —— 五代演进,以及一张真实比例的叠图 全部出自 PTX ISA warp 级(32 线程) warp 组级(128 线程) 双 SM 级(256 线程) TPU MXU,同一把尺子 五代演进 —— 变的是「一次动员多少线程」,不变的是「K = 16」 Volta 第 1 代 8 个线程 参与一次 MMA mma.sync.m8n8k4 —— 一个 quad-pair 八条 lane 凑一次 MMA PTX 目标注记:m8n8k4 requires sm_70,并注明「为 sm_70 优化,别的架构上会明显更慢」 操作数住在哪 寄存器 8 条 lane Turing 第 2 代 32 个线程 = 1 个 warp 参与一次 MMA mma.sync.m16n8k8 —— warp 级形状从这一代进 PTX 同一页还写着:u8/s8 的 m8n8k16、u4/s4 的 m8n8k32、b1 的 m8n8k128 都要 sm_75 —— 整数与亚字节精度 是从 Turing 开始的 操作数住在哪 寄存器 32 条 lane Ampere 第 3 代 32 个线程 = 1 个 warp 参与一次 MMA mma.sync.aligned.m16n8k16 —— K 从 8 加倍到 16,加 BF16 与 TF32 PTX 目标注记:m16n8k16 requires sm_80。这条形状一路活到今天 操作数住在哪 寄存器 32 条 lane Hopper 第 4 代 128 个线程 = 1 个 warp 组 参与一次 MMA wgmma.mma_async.m64nNk16,N 从 8 到 256 可选 NVIDIA 原话:Warp-group MMA across 128 threads只在 Hopper 有(要 sm_90a) 操作数住在哪 A 在寄存器或共享内存,B 在共享内存 128 条 lane Blackwell 第 5 代 两个 SM 配对 · 单线程发射 参与一次 MMA tcgen05.mma · 单 SM:M = 64/128,N = 8,16,…256(步长 8) · 两 SM 配对:M = 128 /256,N = 16,32,…256(步长 16) · K 仍然只有 16 配对时 M 和 N 的步长都翻倍 —— 这不是「同一条指令做得更大」,是粒度本身变粗了⚠️ 这一代趋势反转:前 四代是「一次动员越来越多线程」,tcgen05 由单个线程发射(cta_group::2 说的是哪两个 CTA 供数,不是 256 线程一起发),操作数改由 TMEM 供给 —— 所以这条轴该读成「操作数的作用域有多大」,不是「动员多少线 程」 操作数住在哪 操作数与累加器都在 TMEM,不占寄存器 256 条 lane 同一把尺子上叠一遍 —— 大方框是 TPU 一个 MXU 的 256×256,里面三块是 GPU 三条指令各自的输出矩阵 每个元素画 1.17 像素,三块和大框用的是同一个比例,没有任何缩放作弊。三块共用左上角,所以是嵌套关系,不是并排。 TPU v7 一个 MXU 256 × 256 65,536 个乘加单元,一条指令喂满 tcgen05 m128 n256 wgmma m64 n256 tcgen05.mma M×N = 128 × 256 输出块占 32,768 个位置,是 TPU 那个 256×256 的 1/2 Blackwell 最大的一条。两个 SM 配对时 M 到 256 —— 那时它的高度正好是这个大方框的一半,宽度已经铺满。 wgmma M×N = 64 × 256 输出块占 16,384 个位置,是 TPU 那个 256×256 的 1/4 Hopper 的 warp 组指令。宽度和上面那条一样满,只有高度是它的一半 —— 两条的差别全在 M 上。只在 Hopper 有,Blackwell 换成了 tcgen05。 mma.sync M×N = 16 × 8 输出块占 128 个位置,是 TPU 那个 256×256 的 1/512 在这张图上它只有 9 × 19 像素 —— 就是左上角那个圈住的小蓝点。从 Turing 一路留到今天的 warp 级指令,Blackwell 上不但没被砍,消费级那颗 die 的 块量化还只挂在它身上(见图 G-5)。 还有更关键的一维:收缩维 K —— 上面画的是输出矩阵,K 才决定「一次能吃多深」 M 和 N 决定输出块有多大,K 决定一次乘加链有多长。GPU 这一维从 Turing 的 8 加倍到 Ampere 的 16 之后 就再没变过:fp16/bf16 是 K=16,fp8 是 K=32,fp4 是 K=64 —— 换算成位宽全都是 256 bit,也就是同样的 8 个 32-bit GPU Tensor Core 的 K = 16 ← 就这么窄 TPU MXU 的收缩边 = 256 —— 同一把尺子,差 16 倍 这 16 倍不是「GPU 只能算 16 深」。它连发多条 K=16,累加器一直待在 TMEM 里不落地,深度照样累得上去。差的是一条指令的粒度 不是能力上限。 粒度粗细各有代价。这就是为什么注意力里 head_dim=128 这件事打 TPU 却不打 GPU:128 撞上 TPU 的 256 收缩边,只喂满一半;而 128 是 16 的整数倍,对 GPU 来说刚好切成 8 条指令,一点不浪费。
图 G-4 图 G-4 Tensor Core 一条指令吃多大一块。GPU 的收缩维是 16 —— 记住这个数,下一张图要用它。 —— 出自《GPU 显微镜
MXU —— 256×256 的阵列里,数是斜着穿过去的 乘加单元(权重驻留其中) 激活:从左边进,每拍前进一列 部分和:向下累加,最后落进累加器 灰色 = 这一拍还没轮到 一次矩阵乘在阵列里的样子 —— 第 16 拍的快照 颜色深浅表示这个单元什么时候开始干活:左上角最早,右下角最晚。同一条反对角线上的单元在同一拍工作 —— 这就是「 脉动」两个字的意思。 ← 第 16 拍的波前 一个 MXU 256 × 256 图上 1 格 = 16 × 16 个真实单元 激活 逐行进入 权重:预先载入,整趟计算期间驻留不动 累加器 128 个 · 形状 (8, 256) · 32 bit 激活一行一行从左边进来,每拍往右挪一列;第 r 行比第 0 行晚 r 拍到齐。部分和则沿着列往下累加,走到底就落进累 加器。 延迟和吞吐是两回事。一个数从进阵列到出结果要走满 2N−1 拍 —— 对 256×256 就是 511 拍。但阵列一旦灌满,每一拍 都有一整列结果落地。所以矩阵越大越划算:填满流水线的那 511 拍被摊薄了。 反过来说,小矩阵在 TPU 上格外亏:算一个 256×256×256 的乘法,光是灌满和排空流水线就占掉三分之二的时间(256 + 511 = 767 拍,其中 511 拍不在稳态)。这是 §7 那条「编译器必须提前排班」的物理根源之一。 峰值这条链是官方的,不是推的 262,144 FLOP / 周期 / MXU × 2.2 GHz × 4 个 MXU 2,307 TFLOP/s Google 工程博客逐字给出这三个数,乘出来精确等于官方峰值 把它摊到单个 cell 上:262,144 ÷ 2 ÷ (256×256) = 2,也就是每个 cell 每周期做 2 次乘加。注意这是新架构才有的 —— 128×128 的那 几代(v3 / v4 / v5e / v5p)全部还原成每 cell 1 次。 至于「2 次」在硅上怎么实现(每个 cell 真放了两个乘法器?还是 256×256 是逻辑视图?)—— 公开资料答不了,本文不猜。 256 是「粒度」,不是「上限」—— 这两件事常被搞反 K 比 256 大,一点不亏。K = 1024 就是走 4 趟、累加器一直不落地,跟一趟算完的效率几乎一样。 K 比 256 小,才是真亏。阵列的收缩边是物理的 256 行,喂进去 128 就只有一半行在动 —— 另一半在空转,而且没有办法把别的活塞进去 回到 §3:TPU 没有「换一个跑」这一层)。 对比 GPU:它的收缩维 K 是 16,128 是 16 的 8 倍,切成 8 条指令一点不浪费。同一个模型配置,在两边的「浪费」完全不在一个位置。 一个真实例子:注意力的 head_dim = 128 多头注意力里,QKᵀ 的收缩维和 PV 的输出维都等于 head_dimhead_dim = 128 时,两处各只喂满 256 的一半 —— 纯几何,不需要任何 内部信息就能推出上限是 50%。 实测佐证:把 Qwen3-30B 的注意力从 32 头 × 128 改成 16 头 × 256(参数量和 FLOP 完全不变),MFU 在 8K / 16K / 32K 上分别提升 21% / 32% / 46% 结论不是「TPU 不适合注意力」,是模型配置和硬件收缩边要一起选。同样的改动搬到 GPU 上,收益接近于零。 喂满率:三种 K,同一个阵列 K = 256 100% 刚好铺满收缩边。理想情况。 K = 128 50% 只有一半行在动,另一半空转。head_dim=128 就是这一格 灰色那一半不能拿去干别的 —— 没有第二个任务可以 填进来。 K = 512 100% 分两趟走,累加器不落地。和第一格一样满 第 1 趟 第 2 趟 这就是「粒度不是上限」的意思:大于 256 的维度 只是多走几趟,不产生浪费。
图 T-4 MXU 是 256×256,收缩维比 GPU 大 16 倍。这个差别落到一个真实的 head_dim 上会怎么样,正文接着走。 —— 出自《TPU 显微镜

两张图里最该看的是同一个位置:那条指令在「求和的那一维」上有多深

GPU 是 16,TPU 是 256。差 16 倍。

这一维有个名字叫收缩维 ——  就是矩阵乘里被加掉的那一维,一条指令一次要沿着它累加多少项。

这三个字母(MKN)后面到处都是, 先记一条就够K 是唯一出现两次、也是唯一消失的那个 —— 它被加掉了,所以叫收缩维。 剩下两个就是答案的形状。

而那个 128 的麻烦,一句话是:两头不靠 —— 当阵列的边嫌它小(只占 256 的一半),当流过去的行数又嫌它短 (摊不动装权重的钱)。

为什么是这样 —— 字母的来历、N 为什么也废一半、 以及转置到底等不等价(被问到再展开)
⭐ 顺带把 MKN 一次说清 —— 后面到处都是它们 矩阵乘写成 A[M, K] × B[K, N] = C[M, N] 三个字母只需要记一条规律:
K 是唯一出现两次的,也是唯一消失的那个。
它出现两次,是因为「A 的列」必须等于「B 的行」—— 它是两个矩阵的接口; 它消失,是因为它被加掉了。所以它叫收缩维。

剩下两个就是答案的形状M = 有多少条结果,N = 每条结果里有多少个数。 K = 每个数要加多少项。

记住这条就能读指令名了m16n8k64 —— 输出 16×8,每个数由 64 项加出来。 (顺序也有规律:BLAS 先报输出形状 MN, 最后才报 K。)
⭐ 那这三个字母是怎么来的?—— 答案在一段三重循环里 矩阵乘最朴素的写法就是三层循环,而三层循环的变量名, 从上到下一直就是 ijk
对每个 i(第几条结果)→ 对每个 j(这条里的第几个数)→ 对每个 k(把第 k 项加进来)

三个大写字母,就是这三个小写循环各自的上界。 Mi 跑多少次,Nj 跑多少次, Kk 跑多少次。
k最内层那一层 —— 最内层干的正是「累加」。 所以「K 是收缩维」不是约定,是它本来就是那个累加循环的名字

⚠️ 至于为什么偏偏是 i/j/k/m/n 这几个字母: 这套记号来自 Fortran 写的 BLAS,而 Fortran 里以 I 到 N 开头的变量默认就是整数 —— 所以数组下标和维度长度自然都从这六个字母里挑。 (这是通行解释,不是标准文本里的原话;但 Fortran 那条隐式规则本身是硬事实。) ⛔ 别去给字母编含义(M≠Matrix、N≠Number),编了更难记。
⛔ 真正拧巴的不是字母,是「行数」这个词本身 「行数」量的其实是一列有多长;「列数」量的是一行有多长 说「M 行」的时候,你脑子里该出现的是一条的东西。这确实反直觉。

三个换法,从好用到最好用:
① 别说行列,说高和宽 A 是 M 高 K 宽,B 是 K 高 N 宽。 能乘的条件变成「A 的宽 = B 的高」,结果是「A 的高 × B 的宽」。 「行数是列的长度」这个坎,直接绕过去了。
② 把形状写成一串,看它接龙 M K · K N → M N ——  挨在一起的那两个必须相等,然后它俩一起消失 剩下的就是答案。
③ ⭐ 最值钱的一个:M 是时间,KN 是空间。 在脉动阵列上,K×N 那一块是装进阵列驻留的 —— K 是阵列的高,N 是阵列的宽, 两个都被硬件尺寸卡死。而 M 只是有多少行数据流过去想多长有多长。
这一条顺手解释了这门课的一个现象我们一直在说 K=128 浪费一半、N=128 浪费一半, 却从来没提过 M —— 因为 M 根本不占地方,它只占时间。
⚠️ 那 N=128 为什么只剩一半?—— 同一个算术,完全不同的原因 先把阵列想成一块 256 × 256 的格子地。 要做 A[M,K] × B[K,N],硬件干的事是: 把 B 那一块 K×N 铺进这块地K 是高,N 是宽), 然后让 A 的每一行从左边流进来

一个格子要干活,得同时满足两个条件:
① 它头顶那一行,得真有数据流进来;
② 它脚下那一列累出来的和,得真有人要。

两种浪费,各违反一条:
· K=128(高只占一半)—— 下半边 128 行头顶没有输入 能不能塞点别的进去?不能 —— 同一列的格子会把结果加进同一个数, 塞什么都会污染那个和。
· N=128(宽只占一半)—— 右半边 128 列脚下没人要 它们照样能算,只是算出来的是没人需要的输出通道 —— 而你的输出就只有 128 个通道,没有更多的了

算术很直白利用率 = (K ÷ 256) × (N ÷ 256)所以 128 那一边占一半,另一边满,就是 50%。 ⚠️ 这个式子是从阵列结构推的,不是官方公式 ——  但它跟官方那句「head_dim 是 128 时利用率无法超过 50%」对得上。

⭐⭐ 最值钱的一条:这两种浪费,能不能救是不对称的。
· N能救 ——  只要有另一个矩阵乘用的是同一个输入 A, 就可以把两边的 B 在宽度方向拼起来,一次算完。 Q、K、V 三个投影融成一个大矩阵乘,干的正是这件事 —— 它们吃的都是同一个 X,所以把三个 N 拼成一个大的。
· K救不了 ——  K 方向是求和,拼进去的东西会被加进同一个结果里。 这是它跟 N 的根本区别。

⚠️ 而注意力里 PV 那一步的 N=128 恰恰救不了:要拼就得有别的矩阵乘共享同一个 P而每个头的 P 都不一样。
⭐ 顺着往下推两步 —— 一个看似的救星,和两条真出路 ⛔ 先堵一个几乎人人都会想到的主意:「让所有头共享同一个 V 不就行了?」 MQA/GQA 确实这么干了。但方向反了。
N 需要的是「同一个 A、不同的 B」; 而共享 KV 给你的是「同一个 B、不同的 A」。 后者只能往 M 方向摞 ——  而 M 是时间不是空间,摞多少都填不满那半块阵列。
(所以 MQA/GQA 省的是 KV cache 的显存和带宽, 它一点都没改善这里的阵列利用率。两件事别混。)

⭐ 真出路只有两条:
① 改 head_dim 它要是 256,两头的浪费一起消失。 但这是模型架构的决定,不是调参 ——  也就是说,模型选的那个 head_dim,直接决定了注意力在这块硬件上能拿到几成峰值
② 把矩阵乘转置着算。 PV 会撞 N=128; 但改成算它的转置 Vᵀ · Pᵀ,三个字母全换了位置: M 变成 128,而 KN 都变成了长的那两维。
这正是「M 是时间、K 和 N 是空间」那条的直接应用 ——  把 128 从空间挪到时间 128 占着阵列的宽是浪费,占着「流多少行」一点都不浪费。
⚠️ 「转置之后还是同一个东西吗?」—— 数学上完全等价,但有两笔账
等价来自一个恒等式:(P · V)ᵀ = Vᵀ · Pᵀ这是精确相等,不是近似。
① 你拿到的是转置过的结果。 要么下游直接吃转置版,要么你得翻回来 ——  而在 TPU 上翻一次是要花钱的(那是跨 lane 的搬运,向量单元的活)。
⭐ 所以它叫「转置流水线」,不是「转置一步」得让整条链都用转置的布局、中间不来回翻,省下的才不会赔回去 (Splash Attention 的接口里就把 Q/K/V 的布局做成了参数 —— 说明这个选择本来就是暴露给你的,不是硬编码。)
② 数学等价 ≠ 逐位相同。 累加顺序变了,浮点末位可能对不上。 做数值对齐时这一条要先说在前头,否则会被当成 bug 查半天。
⚠️ 这条路是别人报出来的(有把效率从三成多拉到五成多的报告), 我们自己还没复现 —— 最小复现试了三轮都是负收益, 但那个载体的基线跟生产差了八倍,所以既不能说它成立,也不能说它不成立。 ⛔ 别把它当结论用。
Vᵀ · Pᵀ 凭什么等于 (P · V)ᵀ? —— 拆开走一遍(形状 → 累加 → 数字 → 计算量)

第一步:形状先对上。

  • P[q, kv]V[kv, d] → O = P·V[q, d]
  • Vᵀ[d, kv]Pᵀ[kv, q] → Vᵀ·Pᵀ[d, q]

[q, d] 转置过来正好是 [d, q] —— 形状先对上了,剩下的是里面的数对不对。

第二步:一格一格看,被点乘的是同样那两条向量

原来那格:O[i, j]P 的第 i(横的,长 kv) 点乘 V 的第 j(竖的,长 kv)。

转置那格:(Vᵀ·Pᵀ)[j, i]Vᵀ 的第 j 点乘 Pᵀ 的第 i
而按转置的定义,Vᵀ 的第 j 行就是 V 的第 j 列Pᵀ 的第 i 列就是 P 的第 i 行

所以两边点乘的是一模一样的两条向量,只是先后换了个个儿。

第三步:为什么「换个个儿」不影响结果 —— 这是全部的关键。

矩阵乘之所以不能随便交换(A·B ≠ B·A), 是因为交换之后「谁的行去配谁的列」变了。 但这里我们同时把两个矩阵都转置了 ——  行列也跟着换了一遍

两次交换互相抵消,配对关系一点没变。 剩下的只是每一格里那两个数谁写前面 ——  而它们是标量,标量的乘法本来就可交换。

写成式子就一行:
O[i,j] = Σ P[i,k]·V[k,j] 与 (Vᵀ·Pᵀ)[j,i] = Σ V[k,j]·P[i,k]
同一串乘积、同一个求和,只是每一项里两个因子调了位置。

第四步:拿数字走一遍,看它逐项对上。

P = [ 1  2  3 ]        V = [  7   8 ]
    [ 4  5  6 ]            [  9  10 ]
                           [ 11  12 ]

O = P·V  的左上角  = 1×7 + 2×9 + 3×11 = 7 + 18 + 33 = 58

Vᵀ = [ 7  9 11 ]       Pᵀ = [ 1  4 ]
     [ 8 10 12 ]            [ 2  5 ]
                            [ 3  6 ]

Vᵀ·Pᵀ 的左上角     = 7×1 + 9×2 + 11×3 = 7 + 18 + 33 = 58

注意不是「结果碰巧相等」—— 是 7+18+33 这三项一项一项都一样。 整体是 O = [[58,64],[139,154]], 而 Vᵀ·Pᵀ = [[58,139],[64,154]] —— 正好是 O 的转置。

第五步:计算量一模一样。

原来:q × d 个输出,每个要 kv 次乘加 → q·d·kv
转置:d × q 个输出,每个还是 kv 次乘加 → d·q·kv
一次不多一次不少。

那硬件上凭什么会快?—— 因为变的不是乘加的数量, 是它们怎么摆进那块 256×256 的地里

  • 原来:M=qK=kvN=d=128 → 阵列的宽只用了一半
  • 转置:M=d=128K=kvN=q → 高和宽都能占满,那个 128 跑到 M 上去了

同样多的乘加,摆法不同,空转的格子数就不同。 —— 这就是「把 128 从空间挪到时间」那句话的全部含义。

⛔ 但转置不是免费的 —— 它把一笔账换成了另一笔 前面说「M 想多长有多长」,那只是占地方这一笔的账。 M 大确实不占阵列 —— 但 M 小另有代价。

脉动阵列不是一按就出结果数据要从左边一路淌到右边、部分和要从上面一路累到下面, 这段「填充 + 排空」的长度,跟阵列的边长是同一个量级
装一次权重,只流 128 行过去 —— 头尾那段就摊不动了。 流几千行时它可以忽略,只流 128 行时它是主要开销。 (⚠️ 稳态下阵列是的 ——  不满的是头尾那两段。所以说法要准确:不是「一直用不满」,是「用满的时间太短」。)

⚠️ 顺带纠正一个很自然但落不了地的想法「那把多个 head 的 P 连着输进去不就行了?」
转置之后,流动的是 Vᵀ被装载的反而是 Pᵀ 而不同 head 的 Vᵀ 必须配自己那份 Pᵀ —— M 方向照样拼不了,跟前面 N 方向拼不了是同一个理由。

所以转置的真实面目是:把「阵列占一半」换成了「流水线摊销差」。 是不是划算,取决于这两笔谁更贵 —— 而这不是能在纸上算出来的。
⚠️ 这很可能正是我们自己复现三轮都拿到负收益的原因 —— 但这是推测,我们当时没有把这一项单独量出来。 别把它当成已经查清的结论。
再往里一层:右矩阵是怎么装进阵列的?有没有「两个矩阵都在流」的算法? (机制向)

① 权重是移位进去的,装满一块要 256 个周期。

它不是「一下子拍进去」—— 权重从一个 FIFO 里一行一行往下推, 像卷帘一样铺满整块阵列。 TPU 初代那篇论文里写得很直白:装一块权重要 256 个周期

② 那是不是要等它装完,左矩阵才能开始流?—— 靠双缓冲,通常不用等。

每个格子里有两套权重寄存器:一套在参与计算,另一套同时在装下一块。 所以那 256 个周期是藏在「上一块还在算」的时间里的 —— 论文里那句话的原话就是「用双缓冲来藏掉这 256 个周期」。

⛔ 但「藏得住」是有前提的 —— 而 128 正好破了这个前提 藏得住的条件很简单:这一块算的时间,得比装下一块的时间长 装载 ≈ 256 个周期;而流 M 行过去 ≈ M 个周期。
⚠️ 为什么「算」只要 M 个周期? 因为阵列一旦灌满,左矩阵每一拍往里推进一行,每一拍也有一整列结果落地 —— 所以流完 M 行就是 M 拍,跟这一块里有多少次乘加无关。 乘加是在 65,536 个格子里同时发生的,它不占额外的时间。

⭐ 于是 M = 128 的时候:算 128 个周期,装要 256 个周期 —— 装的比算的还慢,双缓冲藏不住了。 你会卡在等权重上。
⚠️ 这一步是把两个公开数字放在一起推出来的 (「256 周期装一块」+「流 B 行用 B 个流水周期」), 不是官方给的结论;但它把上面那句「M 小另有代价」变成了一个能算的数

③ 装进去之后怎么「固定」? —— 每个格子里就有一个权重寄存器,值放进去就不动了, 整块 tile 算完之前一直是它。 这套做法有名字,叫权重驻留(weight stationary)。

④ 有没有「两个矩阵都在流」的算法?—— 有,而且是三大数据流之一。

脉动阵列的经典分类就三种,区别只在「谁留在格子里不动」

  • 权重驻留(WS):格子里放 B 的一块 —— 阵列装的是 K × N,流过去的是 M TPU 走的是这条。
  • 输出驻留(OS)两个矩阵都流A 从左边进、B 从上面进,结果留在格子里累加 —— 阵列装的是 M × N,流过去的是 K 这就是你问的那种。
  • 输入驻留(IS):格子里放 A 的一块 —— 阵列装 M × K,流 N

输出驻留的好处正是你想到的那个没有「装权重」这一步, 两个操作数一起流进来。 代价是结果攒在格子里,最后要单独排出去一趟。

⭐⭐ 把三种数据流并排一看,这一整节的问题就露出真面目了 三种数据流其实只干了一件事:换「哪一维当时间」
· 权重驻留:空间是 KN,时间是 M
· 输出驻留:空间是 MN,时间是 K
· 输入驻留:空间是 MK,时间是 N

head_dim = 128 的麻烦在于:它两头不靠
· 当空间维 —— 嫌它不起来(只占 256 的一半,另一半空转)
· 当时间维 —— 嫌它(128 个周期摊不动 256 周期的装载)

所以换数据流救不了它,只能换个位置摆 —— 这也正是那条出路是「把矩阵乘转置」而不是「换一种阵列」的原因。 而转置之后 128 落到了时间维上,于是撞的就是上面那个「藏不住」的问题。
⚠️ 上面这段推理链是自洽的,但我们没有实测把它坐实 —— 当作理解框架用,别当作定论。

这一整串画成了一张图 ——  三种驻留方式、真尺寸怎么切块、乒乓怎么藏装载、段数怎么换重搬次数, 四条带一次走完。

一块矩阵乘是怎么在 MXU 上跑完的 —— 谁驻留、谁流动、以及权重什么时候偷偷装进去 用一个真尺寸走完:Q 投影 M = 128K K = 7168 N = 1536,阵列 256 × 256 ① 三种用法,区别只有一个:谁不动 左驻留(IS) 格子里放 A 的一块 A 不动 流进来的: B 从上面流入 出去的: C 流出 空间 = M × K 时间 = N 右驻留(WS) ★ TPU 走这条 格子里放 B 的一块 B 不动 流进来的: A 从左边流入 出去的: C 向下累出 空间 = K × N 时间 = M 中间累加驻留(OS) 格子里放 C 的一块 C 不动 流进来的: A 左入 + B 上入 出去的: 算完整块排出 空间 = M × N 时间 = K ⭐ 三种做法只是在换「哪一维当时间」—— 空间那两维不够大就有格子空转,时间那一维不够长就摊不动装权重的钱下面两条带讲的都是右驻留(TPU 的那条)。 ② 切块:右矩阵切 168 块,左矩阵切 64 段 右矩阵 W = [7168, 1536] bf16 一共 21 MiB —— 一次就全进 VMEM 了 K 方向 7168 ÷ 256 = 28 块 N 方向 1536 ÷ 256 = 6 块 一共 168 块 每块 256×256 左矩阵 X = [131072, 7168] M 方向按 2048 行切一段—— 切出 64 段 ……共 64 段 一段 2048 行 × 256 列(K 的一片) 这一段的局部和(占 VMEM) 2048 × 256 × 4 B = 2 MiB ⛔ 为什么 M 不能一口气流完 那样每个 n 块的局部和要占 131072 × 256 × 4 B = 128 MiB —— 比整块 VMEM(64 MiB)还大 所以 M 必须切段。 而切段就必然要重搬右矩阵 ⚠️ 这不是实现不好, 是账本身如此。 乒乓装载:每个格子有两套权重寄存器,一套在算、一套在装 阵列在算 后台在装 ⛔ 只有第一块要干等 256 周期,之后全藏住 B0 算 B0(2048 周期) B1 256 周期 算 B1(2048 周期) B2 256 周期 算 B2(2048 周期) B3 256 周期 算 B3(2048 周期) B4 256 周期 算 B4(2048 周期) B5 256 周期 算 B5(2048 周期) …… 装一块权重要 256 个周期(TPU 初代论文原话),而流一段 2048 行要 2048 个周期 —— 装的那 256 完全藏在算的那 2048 里面 ⚠️ 所以藏得住的条件很直白:一段的行数要大于 256。这里 2048 是它的 8 倍,稳。(这一条是把两个公开数字放一起推的,不是官方结论。) ⛔ 反例就在隔壁:注意力转置之后 M = 128 —— 连 256 这个门槛都够不着,乒乓就藏不住了 ④ 段切得越碎,右矩阵就要重搬越多次—— 而段能开多长,由那块局部和缓存顶住 一段多少行 局部和占 VMEM B 进阵列的总次数 乒乓藏得住吗 256 0.25 MiB 86,016 ⛔ 藏不住(不大于 256) 512 0.5 MiB 43,008 2048 2 MiB 10,752 8192 8 MiB 2,688 ⭐ 两头夹的又是同一件事 段开长 → 右矩阵少搬 → 省搬运 段开长 → 累加器变大 → 吃片上容量 所以「一段多少行」这个旋钮,拧到底还是被那口灶台顶住的。 —— 跟 3.6 里 GPU 那堵墙是同一堵。
四条带,从「谁不动」一路走到「要重搬几次」: ① 三种驻留方式的区别只有一个 —— 谁不动,而它直接决定哪两维是空间; ② 右矩阵切 28 × 6 = 168 块,左矩阵按 2048 行切 64 段; ③ 乒乓装载 —— 装一块权重 256 周期,藏在算的那 2048 周期里, 只有第一块要干等; ④ 段切得越碎,右矩阵重搬越多次 —— 而段能开多长由片上累加器顶住。 ⛔ 注意左下角那个反例:注意力转置之后 M=128,连 256 这个门槛都够不着
拿一个真尺寸从头走一遍:Q 投影是怎么在 MXU 上跑完的 (M=128K,K=7168,N=1536)

先把三个数摆好。 左矩阵 X[131072, 7168], 右矩阵 W[7168, 1536], 结果 Q[131072, 1536]

① 一次能放多大一块?—— 这个没得选:256 × 256 阵列就这么大,右矩阵只能按这个尺寸切。

② 一共切成多少块?

  • K 方向:7168 ÷ 256 = 28
  • N 方向:1536 ÷ 256 = 6
  • 一共 28 × 6 = 168 块。
⚠️ 「搬」有两种,别混 —— 这是最容易算错账的地方 · 从 HBM 搬到片上:整个 W 只有 7168 × 1536 × 2 B = 21 MiB —— 一次就全塞进 VMEM 了(那儿有 64 MB)。这一步只发生一次。
· 从片上移位进阵列:每用一块就要 256 个周期 —— 这一步要发生上万次
说「搬 B」的时候先问清是哪一种,否则差着几个数量级。

③ 时间线长什么样。假设我们把左矩阵按 2048 行一段来喂:

装 B(k=0, n=0)          ← 256 个周期。第一块没得藏,只能等
      ↓
流 X 的 2048 行         ← 2048 个周期
  (同时在装 B(k=1,n=0))  ← 256 周期,藏在上面那 2048 里
      ↓
流 X 的 2048 行         ← 用 B(k=1,n=0)
  (同时在装 B(k=2,n=0))
      ↓
      … 28 次之后 …
      ↓
这一段 2048 行、这 256 个输出列,累加完了 → 写出去
      ↓
换 n=1,再走 28 次 …  六个 n 走完 → 这 2048 行彻底做完
      ↓
换下一段 2048 行 → 168 块 B 全部重来一遍

④ 「装完才开始流」吗?—— 只有第一块是。 之后靠双缓冲:装下一块算这一块同时进行。 藏得住的条件是「流的行数 > 256」 ——  这里流 2048 行,8 倍富余,稳稳藏住

⑤ 每块 B 只用一次吗?—— 不是。 它被重复搬的次数,正好等于左矩阵切成了几段

左矩阵一段多少行片上累加器要多大 B 进阵列的总次数装载藏得住吗
2560.25 MiB86,016藏不住(256 不大于 256)
5120.5 MiB43,008
20482 MiB10,752
81928 MiB2,688

看这张表就明白左矩阵为什么不能一段太短段越长,B 重搬得越少;但累加器要一直待在片上,它跟着变大。 所以「一段多少行」是被片上容量顶住的 ——  又绕回那口灶台。

⑥ 那能不能让 M 一口气流完、B 只搬 168 次? —— 算一下就知道不行:那样累加器要 131072 × 256 × 4 B = 128 MiB 比整块 VMEM 还大。 所以 M 必须切段,而切段就必然重搬 B。这不是实现不好,是账本身如此。

⭐ 这个例子顺手证了前面那条「M 小要吃亏」 这里 M 有 13 万行,随便切一段都远超 256,装载稳稳藏住。 —— 投影这类矩阵乘根本不担心这个问题。
而注意力转置之后 M = 128 ——  连一段的门槛(256)都不到。 同一个机制,一个稳稳藏住,一个连门槛都够不着。

光看这两个数没有感觉。拿一个真实的形状套进去就有了。

注意力的 head_dim 常取 128

GPU:128 ÷ 16 = 8 —— 切成八条指令,一点不浪费。
TPU:128 对 256 —— 只喂满一半另一半空着。

那空着的那一半,找点别的活填进去不行吗?

不行。收缩维是一条累加链 ——  塞进去的任何别的数据,都会被加进同一个部分和。谁也顶不上来。

反过来问:那 GPU 为什么不吃这个亏?

不是因为它有调度器。 调度器换的是 warp,填的是时间上的气泡 —— 它填不了收缩维里空着的那一半。
是因为它的 K 只有 16,几乎什么形状都是 16 的整数倍。

而它敢把 K 做这么窄、把对齐留到运行时收拾,代价就是 3.2 里 TPU 少掉的那五样。

⚠️ 这里最容易翻车的一点先说在前面: 16256 是一条指令的形状,不是一个周期做多少事 —— 把这两件事除在一起,算出来的峰值必然荒谬。

两边的峰值怎么对账,以及三个最容易算错的地方 (要复核数字再展开)
⛔ 这里最容易翻车:K=16 是一条指令的形状,不是一个周期做的事 最容易犯的推法是:「一条指令才 16 深,一个 SM 一个 Tensor Core, 乘一乘怎么也凑不出 2,250 TFLOPS。」
两个环节全错:数错了部件,又把指令形状当成了每周期吞吐完整推法折在下面。
对一下账就通了4,096 × 2 FLOP × 148 SM × 1.83 GHz = 2,218 TFLOPS, 官方 2,250,差 1.4% —— 那点缺口来自时钟不确定,不是公式错。

所以指令形状决定的是「一次交代多少活」,也就是控制开销能摊多薄; 每周期能做多少乘加是另一件事,由阵列大小定。 把这两件事除在一起,必然得出荒谬的结论。
⭐ 顺手把 TPU 侧的峰值链也对一下 —— 和上面那条 GPU 的配成一对 TPU 那边的峰值链是官方逐字给出的
262,144 FLOP/周期/MXU × 2.2 GHz × 4 个 MXU = 2,307 TFLOP/s
官方公布 2,307,一分不差
顺手把 3.1 那个钩子接上。那张图上的 131,072 是格子数(两个 MXU), 这里的 262,144 是每个 MXU 每周期的 FLOP —— 两个数单位不同,别对着比。
一除就通了:262,144 FLOP ÷ 65,536 格 = 每格每拍 4 FLOP, 也就是 2 次乘加 —— 一次乘加 = 一乘一加 = 2 FLOP,两次正好 4。
这正是 3.1 说「图上没有」的那一步。 —— 两边都能从「一拍多少次乘加」一路乘到公布峰值,这一节的数就都站得住了。

至于「592 份小的」和「4 份大的」这笔总账怎么合起来,是 3.7 的事 —— 这一节只要记住一个数:收缩维 16 对 256。 而这两件事一旦分开,3.6 里为什么两边要切不同大小的块,就是自明的。
⚠️ 一条必须带走的跨代警告 TPU 那 131,072 里,有一半来自「每个 cell 每周期做 2 次乘加」, 这是 256×256 这几代才有的;128×128 的老几代(v3 / v4 / v5e / v5p)都是每 cell 1 次跨代沿用这个常数会直接错一倍。
另外三个「听起来很合理」的坑折在下面,回查用。
那条错误推法错在哪,外加搭这个模型时的三个坑(回查用 · 讲的时候跳过)
※ 那条常见的错误推法,错在哪两步(从主线下沉过来) ① 数错了部件。 一个 SM 有 4 个 Tensor Core(四个处理块各一个),不是 1 个; CUDA Core 是 128 个,不是 4,096。全片是 592 个 Tensor Core、18,944 个 CUDA Core4,096 这个数确实存在,但它是每 SM 每周期的乘加次数(4 个 Tensor Core × 1,024), 不是核数 —— 数值眼熟,量纲完全不同。
② 把指令形状当成了每周期吞吐。 mma.sync 的 m16n8k16 一共是 16 × 8 × 16 = 2,048 次乘加;一个 Tensor Core 每周期做 1,024 次 —— 所以这条指令占它 2 个周期它是一包活,不是一拍的事。 最大那条 tcgen05.mma(128×256×16)是 524,288 次乘加, 一个 SM 每周期 4,096 次 → 要 128 个周期;两个 SM 配对就是 64 个。
⚠️ 三个坑,每一个都「听起来很合理」 ① 那个 16 不是 GPU 阵列的宽度,是指令的收缩深度 Tensor Core 每周期做 1,024 次乘加是确定的;但它物理上排成几乘几, 公开资料没有说。—— 所以不能拿 16 去平方当「面积」,那是把两个不同量纲的东西套进同一个公式。
② 128 也不是 16 的平方(那是 256)。128 = 131,072 ÷ 1,024, 是每周期乘加数之比,跟阵列的边长没有直接关系。
③ SM 数是 148,不是 128。 两个数长得像,但一个是「有多少份」, 一个是「每份差多大」 —— 它们接近正是上面那条抵消成立的原因, 但不是同一个数。
⭐ 这一节的落点 同一个形状,在一边是刚好,在另一边是打对折。
这不是哪边设计得差 —— 是「按谁的粒度对齐」这个问题本来就有两个答案。

⭐ 接回融合那条线。一次融合到底能融多大,由两个数共同卡死 ——  一个是工作台有多大(3.2b 那个 227 KiB 对 64 MiB,289 倍), 另一个就是这里的粒度有多粗。
台子小、粒度细,就只能融小块、多融几次; 台子大、粒度粗,一次能吞下的融合范围就大得多。3.6 并排走的时候, 这两个数会同时出现在切块那一步。
⭐ 把「289 倍」换成「你到底能开多大的块」 倍数不指导任何决定,能不能放下才指导。同一道算术,两边各套一遍 ——  head_dim = 128、bf16、K/V 双缓冲、分数矩阵按 fp32 估; 真实 kernel 会把 Q 和累加器留在寄存器里,所以这是量级估算,不是逐字节账。)
Br × BcK+V(双缓冲)Q分数矩阵合计
64 × 6464 KiB16 KiB16 KiB96 KiB
128 × 6464 KiB32 KiB32 KiB128 KiB
128 × 128128 KiB32 KiB64 KiB224 KiB ← 顶到天花板
256 × 256256 KiB64 KiB256 KiB576 KiB ← 爆了
512 × 10241.0 MiB128 KiB2.0 MiB3.1 MiB
2048 × 20482.0 MiB512 KiB16.0 MiB18.5 MiB
GPU 一个线程块上限 227 KiB —— 128 × 128 那 224 KiB 几乎是顶着天花板放进去的,再往上一档直接爆两倍半。
TPU 一个核 64 MiB —— 2048 × 2048 那 18.5 MiB 装得下,还余三分之二。

所以「真实的 FlashAttention kernel 块行数一律取 64 或 128」 不是调出来的,是被 227 这个数卡死的。 把上限换成几兆,人家立刻就开大了 —— TPU 那一侧就是这么开的。 ⚠️ 别忘了上面 3.2b 那道闸:64 MiB 是总面积,一次能用多少另有额度。 表里最后一行放得下,不等于编译器就会给你开那么大。

3.5 各自多出来的那一块

上半节到这儿为止,数的都是「一边有、另一边没有」。还有两块,是各自额外长出来的

但这一节要问的不是「它们是什么」,是这个 ——  宣传页上写着「支持」的东西,你到底用不用得上?

这一节就两个例子,两边各一个。

第一个例子在 GPU 这边,叫块量化。 宣传口径是「B200 支持 FP4」。

这句话是真的。门槛在于:带块量化的指令有两条通路, 而它们不在同一颗 die 上 —— B200 只有其中一条。 (哪条对哪颗 die,图上写了。)

块量化在硬件上到底怎么跑的(NVFP4 与 MXFP4、 Blackwell 加了哪三样、训练侧那个 16×16)—— 整套机制在 专题八 · 精度与量化

第二个例子在 TPU 这边,叫 SparseCore —— 一颗专门查嵌入表的核。 宣传口径是「TPU 有个专门查 embedding 表的核」。

这句话也是真的。门槛在于:它只对「同一批行被反复查」的负载划算。 推荐系统那边少量热行被反复命中,重复度很高 —— 那是它的主场。

语言模型的取行重复度不够高 ——  注意不是「没有重复」:一条十几万 token 的序列里,不同的词只有几千个, 而且绝大部分命中的是最高频那一两千行重复确实有,只是跟搜广推差着量级 ——  SparseCore 的收益门槛是按那种集中度定的,这点重复度不划算编译器根本不往那儿派,生产任务里它一次表都没查过。

⛔ 但「用不上」不等于「没在用」—— 这颗核你天天在用 说「语言模型用不上 SparseCore」是错的。 用不上的只是查表这条用途。

它在大模型训练里真正干的活是卸载集合通信 ——  把 All-Gather、Reduce-Scatter 从 TensorCore 手上接过去让 TensorCore 专心算,通信在旁边并行跑完。 这正是「算和通信重叠」这件事在 TPU 上的落地方式之一。

出处都是公开的:Google Cloud 博客 《Training large models on Ironwood TPUs》把它单列成一条调优手段; MaxText 公开仓库的 XLA flag 库里就有 SparseCore All Gather

所以这一节的教训还要再狠一层:宣传口径不只会让你 用不上它说的那个能力还会让你看不见你已经在用的那部分 —— 你会拿「查表的核」这个名字去 profile 里找查表, 找不到就以为这颗核在闲着。

那它到底怎么处理不规则访存?—— 把这颗核拆开看一眼就清楚了。

SparseCore 拆开看 —— 「处理不规则访存」处理的是什么 地址由数据算出来、编译期不知道 —— 这件事难在两处,而这颗核对着这两处各配了一个解法 TensorCore 本课前六小节拆的都是它 MXU × 4 256×256 脉动阵列 VPU 向量寄存器 8 sublane × 128 lane VMEM —— TensorCore 专用。SparseCore 不直接管它 ⛔ 它是编译期就排死的机器:地址得提前知道 SparseCore 按 device 看 2 颗(物理 4 颗/chip)· 占的面积很小 标量子核 × 1 · 标量运算 · 动态索引 · 发起 DMA 和 stream · 自带 SMEM 向量子核 × 16(文档里也叫 tile) 每个子核自带 VMEM + SMEM,数据流各走各的 SIMD 宽度 16(F32)/32(BF16)—— 一次动的是十几个数,不是一千个 共享 VMEM OpenXLA 文档里 叫它 SPMEM 很小、很快、 编译器显式管,不是缓存 ⭐ 它原生支持数据相关的控制流和访存—— 地址可以是刚算出来的 HBM —— 192 GiB(v7)。大表、中间结果都在这儿。两颗核都直接连它 ⭐ 所以「操作 VMEM 还是 HBM」这个二选一本身是个陷阱:两块都碰—— 只是它碰的那块 VMEM 是它自己的,不是 TensorCore 那块。 那「最小粒度」到底是多少 —— 先把两件被混为一谈的事分开 TensorCore 那个 (8,128) 它是向量寄存器的形状(8 sublane × 128 lane = 1,024 格), 也是 VMEM 里的布局单位 —— fp32 下一块 4 KiB。 它不是「DMA 的最小粒度」。把这两件事说成一件,是我讲这一段时犯过的错。 但方向是对的:你要的若只是散落的几行,按这个单位对齐取数就会付冤枉钱。 SparseCore 的 DMA 粒度 64 字节 —— 这是可执行的官方口径,不是文档里的形容词: pltpu.get_tpu_info().sparse_core 在 TPU 7x 上报 num_cores=2, num_subcores=16, num_lanes=16, dma_granule_size_bytes=64 ⭐ 所以回答「能不能不搬 (8,128)」:这个问题问的是两台机器的两套单位—— 它按 64 字节走。 一次 gather 在这张图上怎么走 索引放在 SparseCore 自己的 VMEM 里 → ② 标量子核照着索引发一堆 DMA → ③ 十六个向量子核各追各的地址,从 HBM 里取回来 Pallas 里就是一行:sync_copy(data_ref.at[indices_ref], target_ref)—— data 在 HBM,indices 在 VMEM。scatter 是同一条路反着走。 这条路不只用来查 embedding:Ironwood 上 Qwen 3.5 那篇公开调优记录里,SparseCore 干的是按路由索引把 token 从 HBM 间接 gather 出来 直接喂给 TensorCore 做 GMM —— 同一个形状,换到稀疏注意力上就是「把 top-k 那批零碎的 KV 取回来」 ⚠️ 取回来之后落在哪,公开资料没有明写。那篇只说「写进一块连续的虚拟缓冲,绕开了在 HBM 里物化中间张量」—— 绕开 HBM 是明写的, 是不是直接落进 TensorCore 的 VMEM,没有一处公开文档这么说;而 OpenXLA 那篇讲 embedding 的又说 SC 与 TC 之间「frequently involves HBM as an intermediary buffer」。两处口径不一致,这里不下结论。 ⭐ 它扛延迟的方式,不是让每一次取数变快 —— 是同时欠着很多次取数 散乱访存的痛点从来不是带宽,是每一次都得等。十六个子核各自独立的数据流,就是为了把一大把请求同时抛出去、各自回来。 换个说法:TPU 把 TensorCore 上砍掉的「运行时才知道地址」那套能力,单独做了一颗小的放在旁边。主核保持纯静态、拿满算力;需要动态的时候,交给它。 ⚠️ 规格出自 JAX Pallas SparseCore 文档与 OpenXLA SparseCore 文档;「为什么这套本事也适合卸载 collective」是推的,官方没有公开解释
难点有两处,它对着两处各配了一个解法: 地址由数据算出来 → 原生支持数据相关的控制流与访存; 要的只是散落的几行 → 粒度做细(v7 上 DMA granule 64 字节, 出自 pltpu.get_tpu_info().sparse_core)。 ⚠️ 「操作 VMEM 还是 HBM」是个陷阱二选一 —— 两块都碰, 但它碰的那块 VMEM 是它自己的(每个子核一份 + 一份共享, OpenXLA 文档里叫 SPMEM),不是 TensorCore 那块。 ⭐ 它扛延迟靠的不是每次更快,是十六路各追各的地址、同时欠着很多次取数

知道了它不是什么,接着该知道它是什么 —— 掀开盖子看一眼

把 SparseCore 拆开 —— 里面有哪些子核、各干什么、线怎么连 「SparseCore 对比 TensorCore」那张把它当成一个整体在用;这一张掀开盖子。形状上它更像一台十六路的搬运机,而不是一台计算器。 一颗 SparseCore —— v7 上每颗芯片 4 个物理核,按 device 算 2 个。整颗核占的面积很小,但它是这一节的第二个主角。 ⭐ 读法:橙色虚线=派活(控制与 DMA 请求),蓝色实线=真正的数据搬运。两种线分开画,是因为这颗核最反直觉的一点就是—— 发命令的人自己不搬数据 标量子核 × 1 scalar subcore · 标量运算(一次一个数) · 动态索引 —— 地址可以是    刚从数据里算出来的 · 发起 DMA 与 stream · 自带 SMEM(它的私有便签) ⭐ 它是调度中枢: 不算数据,只决定谁去搬什么 向量子核 × 16 —— 公开文档里也直接叫 tile 十六个完全同构,每个自带内存、数据流各走各的 ① tile 0 tile 1 tile 2 tile 3 tile 4 tile 5 tile 6 tile 7 tile 8 tile 9 tile 10 tile 11 tile 12 tile 13 tile 14 tile 15 ⛔ 别把它当成「小一号的 TensorCore」:这里面没有 MXU 共享 VMEM 文档里叫 SPMEM · 十六个 tile 都能访问 · 跨 tile 交换数据   走的就是这块 · 很小、很快 · 编译器显式管   不是缓存 ⛔ 不是 TensorCore    那块 VMEM ① 把一个 tile 放大看 十六个长得一样,看懂一个就看懂全部 向量 ALU SIMD 16 lane(F32)/32(BF16) 本地 VMEM 文档里叫 TileSPMEM / local SPMEM 本地 SMEM 这个 tile 自己的标量便签 ⭐ 一次动的是十几个数,不是一千个 派活 数据 HBM —— 大表、权重、中间结果都在这儿。每次 DMA 的最小粒度 64 字节pltpu.get_tpu_info().sparse_core 直接报得出来) 三种角色都直连 HBM—— 标量子核往那儿发 DMA,tile 往那儿取数、写回,SPMEM 里周转的东西也从那儿来。 ⭐ 对照一下就知道这颗核为什么存在:TensorCore 那边按向量寄存器的形状(8 sublane × 128 lane)成块地取,要的若只是散落的几行,就得为整块付钱;这边按 64 字节走。 一次 gather 沿着这些线怎么走 1. 索引先到位 —— 要取哪些行,这份索引本身也是数据,放在 SparseCore 自己的内存里 2. 标量子核照着索引发 DMA —— 橙色虚线—— 它一次抛出一大把请求,不等任何一个回来 3. 十六个 tile 各追各的地址 —— 蓝色实线—— 从 HBM 取回自己那份,互不排队 4. 要跨 tile 汇总时经 SPMEM —— scatter 就是同一条路反着走 它扛延迟的方式不是让每一次取数变快,是同时欠着很多次取数。 它擅长的四类活(官方文档原话,不是我归纳的) 小向量算术 —— 一次十几个数的加减乘 gather / scatter —— 按索引取、按索引写 排序 · 去重 · 计数 · 直方图 —— 全是「先看了数据才知道下一步」的活 ragged 操作 —— 每一行长度都不一样的那种 ⭐ 四条共用一个形状:要么地址是算出来的,要么形状是不齐的—— 正好是静态编译的机器最难受的两件事。 ⭐ 为什么这台「搬运机」后来被拿去扛集合通信 All-Gather、Reduce-Scatter 这些活,拆开看恰好也是「发一大把 DMA、各自回来、中间顺手加一下」—— 跟 gather 是同一个形状,只是对面从 HBM 换成了别的芯片。 于是就有了这一节最后那件事:把集合通信从 TensorCore 手里接过去,让计算和通信真正并行。Pallas 里这条路是公开的 —— VectorSubcoreMesh 让你 「用写 TensorCore collective 的同一套模型,在 SparseCore 上写 collective」(JAX Pallas 官方文档原话)。这件事在 XLA 里就是一组 flag 开关。 ⚠️ 「为什么这套本事也适合卸载 collective」是从形状推的—— 官方公开了开关和收益,没有公开这段设计理由。
一颗 SparseCore = 1 个标量子核 + 16 个同构的向量子核(tile)+ 一块共享 SPMEM, 三种角色全都直连 HBM。两种线的分工是这张图的重点:橙色虚线是派活 (标量子核发 DMA 请求),蓝色实线是数据(tile 自己去搬)——  发命令的人不搬数据,这正是它能同时欠着一大把取数的原因。 ⛔ 十六个 tile 里没有 MXU,别当成小一号的 TensorCore。 规格与操作清单出自 JAX Pallas SparseCore 官方文档。

图上那条「集合通信」的线,落到 XLA 里就是一组开关列出来是为了让「卸载」这两个字有个能上手的抓手 —— 全部出自公开材料

flag它开的是什么该知道的那一条
① 主开关 —— 哪些集合通信交给 SparseCore
--xla_tpu_enable_sparse_core_collective_offload_all_gather All-Gather 交给 SparseCore Ironwood(v7)上这三个默认就是 true
也就是说:你不设任何 flag,它已经在替你干活了
…_offload_reduce_scatterReduce-Scatter
…_offload_all_reduceAll-Reduce
…_offload_2d_all_gather 两层网格上的 All-Gather 要显式开
② 地基 —— 主开关生效需要跟着一起设的
--xla_tpu_use_tc_device_shape_on_sc 让 SC 沿用 TensorCore 那套 device shape MaxText 把这四个打成一组叫 ENABLE_SPARSECORE_OFFLOADING_BASE_FLAGS
取值 true / false / false / true单开主开关往往看不到效果,坑就在这儿
--xla_sc_enable_instruction_fusionSC 侧的指令融合
--xla_sc_disjoint_spmem 共享 SPMEM 是否按 tile 切成互不重叠的块
--xla_sc_disable_megacore_partitioning 关掉 megacore 划分
③ 看得见 —— 不开这几个,profile 上那段是黑的
--xla_tpu_enable_all_gather_offload_tracing
…_reduce_scatter_offload_tracing
…_all_reduce_offload_tracing
把卸载出去的那段打进 trace 调优时才开。「看 trace 不看宣传页」这条规矩, 在这里需要你先把灯打开
④ 顺带一个不属于 SparseCore、但每次都要一起调的
--xla_tpu_scoped_vmem_limit_kib 单个 op 最多能占多少 VMEM,剩下的留给下一个 op 预取 MaxText 公开配方:稠密模型 98304、MoE 81920

⚠️ 两条必须一起记住的约束。 第一,Continuation Fusion 与 SparseCore 卸载只能二选一——  MaxText 的注释原话是「Either one of CF or SC can be enabled at a time」。 两者都是「让通信和计算重叠」的手段,走的是两条不同的路,不能叠加以为「都打开更快」是这一组 flag 上最常见的误用。 第二,98304 KiB = 96 MiB比 v7 单核 64 MiB 的 VMEM 还大——  这个数在公开配方里是跨代通用的一个上限申请,实际生效值受目标机器约束。 这里只如实转述配方,不替它解释。

⛔ 先钉一个必须带走的:「Tensor Core」在两边差着两个数量级 这是跨平台读规格表最容易错的一处,而且错了之后所有「每个核多少算力」都跟着错。
NVIDIA 的 Tensor CoreTPU 的 TensorCore
是什么SM 里面的一个执行单元 整颗 device 的主计算核
里面装着就是矩阵乘阵列本身 MXU + VPU + 标量单元 + VMEM
一颗芯片上几个一百多个 SM,每个里面还有若干 v7 上 2 个
所以「TPU 的 TensorCore」对应的不是「GPU 的 Tensor Core」, 对应的是「GPU 的一整颗 die」那一层。 —— §3.7 那张「592 个 Tensor Core 对 4 个 MXU」的账表, 就是踩在这个换算上的;换算错了,那张表会读出完全相反的结论。
⭐ 第二条:标量单元和向量单元不是算力档次的差别 看到上面那张图里「1 个标量子核配 16 个向量子核」, 最容易的误读是「标量算力不够所以只配一个」。不是。

· 向量单元干的是:对一排数做同一件事 —— 一千个数,一条指令,全都加二。
· 标量单元干的是:做一个决定 —— 循环转几圈?下一个地址在哪?这批要不要跳过?

区别不是量,是性质 —— 决定只有一个答案,你没法把它做一千遍。 所以 1 配 16 是分工,不是配比失衡。
—— 而且这个三件套(标量/向量/矩阵)不是 SparseCore 特有的: TensorCore 是这三层,GPU 的 SM 也是这三层。整台机器的骨架都是这个形状。 按维度看就是 0 维/1 维/2 维 —— 这个直觉是对的, 错的只是把维度读成了强弱。
⭐ 第三条:两个优化都打开,不等于叠加 上面那张 flag 表里最容易踩的不是某一个 flag,是那条互斥。 Continuation Fusion(把通信切碎塞进计算的缝隙里,让 TensorCore 自己 交替着算和搬)和 SparseCore 卸载(整段外包给旁边那颗核)——  目标完全一样,是同一件事的两种实现
编译器只能挑一个来重写那条指令,所以两个都开 = 两个 pass 抢着改同一个 op。

能带走的判据:看到「两个优化都打开会不会更快」, 先问它们是不是同一件事的两种做法。 是的话,打开两个不等于叠加 —— 等于让编译器替你随便挑一个。 CF 的机制、那句「实现是挑出来的」原文出处,见下面延伸一。
⭐ 第四条:MMA 三个字母,以及 TPU 为什么没有它 MMA = Matrix Multiply-Accumulate,矩阵乘加,一条指令算 D = A×B + C
其中一点值得单独记住乘和加融在一起,而且累加位宽比输入高得多 —— 输入 FP8/FP4,累加一直是 FP32,中间不落地不舍入。 ⭐ 这才是四位输入能用的真正前提:不是四位够准, 是它只负责乘,加法那一头从来没降过精度。

TPU 有没有 MMA?—— 数学上有,形式上没有。 差别不在「有没有矩阵指令」,在谁决定它什么时候执行: GPU 那边是运行时由线程发射、硬件调度、记分板跟踪的一条指令; TPU 那边每一拍发什么在编译期就排死了。
一句话:GPU 把矩阵乘做成了一条指令,TPU 把它做成了一条流水线 —— 这跟 §3.2d 那张延迟图说的是同一件事,只是换了个部件。 三代演化线(warp → warpgroup → 单线程)与出处,见下面延伸三。

课后延伸 三块「课上未必讲、但值得自己看一遍」的

下面三块都是现场追问逼出来的,主线上跳过不影响听懂, 但它们各自都堵住了一个很容易含混过去的地方。

延伸一:CF 是什么?为什么它跟 SparseCore 卸载只能二选一
CF = Continuation Fusion,直译是「接力式融合」。 它跟 SparseCore 卸载的目标完全一样:让集合通信和计算重叠。 不一样的是手段。

CF 的做法:把一次 All-Gather 切成很多小块,塞进相邻的计算里 ——  TensorCore 算一小段、搬一小块、再算一小段,在同一颗核上交替进行。 这一轮没搬完的进度,作为一个中间状态传给下一个融合块,一路接力下去 —— continuation 这个名字就是这么来的。

SparseCore 卸载的做法:整段通信外包给旁边那颗核,TensorCore 一直在算。

那为什么只能二选一?MaxText 的配置文档里有一句话把这件事说死了 —— 「实现(比如 BC-offload 或者 continuation fusion)是根据其他 flag 的取值出来的」。 挑。它们是同一个 collective 的几种可选实现, 编译器只能选一个来重写那条指令。两个都打开,就是两个 pass 抢着改同一个 op。

能带走的一条:看到「两个优化都打开会不会更快」,先问它们是不是 同一件事的两种做法。是的话,打开两个不等于叠加 —— 等于让编译器替你随便挑一个。 出处:MaxText 公开仓库 src/maxtext/configs/README.mdbenchmarks/xla_flags_library.py; 「continuation state 从一个 async collective fusion 传到下一个」 的描述见公开技术博客,NVIDIA/Google 均未发布 CF 的正式设计文档
延伸二:标量、向量、矩阵 —— 三种单元到底差在哪(我怎么老也分不清)
先肯定一个直觉:按维度分,是对的。 标量一次一个数(0 维),向量一次一排数(1 维), 矩阵一次吃两个二维块(2 维)。形状上就是这么回事。

但最容易错的理解,是把它们当成算力档次 —— 好像标量单元是个弱鸡版的向量单元。 它们干的根本不是同一类活。

· 向量单元干的是:对一排数做同一件事 一千个数,一条指令,全都加二。
· 标量单元干的是:做一个决定 这个循环转几圈?下一个地址在哪?这一批要不要跳过?

区别不是量,是性质 —— 决定只有一个答案,你没法把它做一千遍。 所以标量单元「弱」不是缺点,是它那件事本来就只需要做一次。

打个比方:向量核是流水线上一百个工人,同时拧同一种螺丝; 标量核是旁边那个拿着单子说「这批走三号线」的人。 他就一个,也不需要一百个他。

回到上面那张图:SparseCore 里 1 个标量子核配 16 个向量子核, 这个比例不是因为标量算力不够,是分工本来就长这样。 橙色虚线(命令)从标量子核出发,蓝色实线(数据)是十六个 tile 各自去搬 —— 「发命令的人自己不搬数据」,说的就是这个分工。

最后一句最有用:这个三件套不是 SparseCore 特有的。 TensorCore 也是标量单元 + 向量单元(VPU)+ 矩阵单元(MXU)三层, GPU 的 SM 里同样有这三档。整台机器的骨架都是这个形状 —— 你在别处再碰到,就不用重新分辨一次了。
延伸三:MMA 这三个字母是什么?TPU 上有没有这东西
MMA = Matrix Multiply-Accumulate,矩阵乘加。 一条指令算的是 D = A×B + C

硬件上它特别在三点,每一点都对应一笔真实的省钱:
  • ① 一条指令做一整个小矩阵乘,不是一次标量乘加。 取指、译码的成本摊到几百次乘加上 —— 这是省调度
  • ② 乘和加融在一起,累加位宽更高。 输入 FP8/FP4,累加 FP32,中间不落地、不舍入⭐ 这是四位输入能用的前提 —— 不是因为四位够准, 是因为它只负责乘,加法那一头一直是三十二位。
  • ③ 操作数在单元内部被复用。 标量循环里 A 的每个元素要为 B 的每一列重读一次; MMA 单元读一次就在整块里反复用 —— 这是算术强度上去的来源, 跟第 1 节那条线直接接上。
顺着几代看,有一条特别清楚的演化线:指令越来越大,发起者越来越小。
谁来发这条指令操作数放在哪
Volta ~ Ampere一个 warp(32 线程)同步执行 各线程私有寄存器里各拿一片
Hopper一个 warpgroup(128 线程),异步 A 可以直接来自共享内存
Blackwell一个线程发出去就不管了 共享内存 + Tensor Memory,累加器也在里面

Blackwell 那一行的「必须由单个线程发起」是官方文档的措辞, 理由很干脆:这条指令要用的数据一个字节都不在私有寄存器里, 全在 CTA 共享的内存空间中 —— 所以矩阵单元跟 warp 调度器解耦了。

那 TPU 有没有 MMA?—— 数学上有,形式上没有,而这个差别正好是本讲的主线。

MXU 做的当然是同一件事:矩阵乘加。 差别不在「有没有矩阵指令」,在谁决定它什么时候执行 GPU 这边,MMA 是运行时由线程发射、由硬件调度、由记分板跟踪的一条指令; TPU 那边,整台机器是超长指令字,每一拍发什么在编译期就排死了 —— 没有记分板,没有乱序,连「线程」这个概念都没有。

一句话对照:GPU 把矩阵乘做成了一条指令,TPU 把矩阵乘做成了一条流水线 指令要被发射、被调度、被跟踪;流水线只是到点了,数据自己流过去。
⚠️ 顺带澄清一个长期混淆:「Tensor Core」在两边差着两个数量级 · NVIDIA 的 Tensor Core:是 SM 里面的一个执行单元。 一颗 B200 上有一百多个 SM,每个 SM 里还有若干个。
· TPU 的 TensorCore:是整颗 device 的主计算核, 里面装着 MXU、VPU、标量单元和 VMEM。v7 一颗芯片上只有 2 个

所以「TPU 的 TensorCore」对应的不是「GPU 的 Tensor Core」, 对应的是「GPU 的一整颗 die」那一层。 跨平台读规格表时,这是最容易错的一处 ——  而且错了之后所有的「每个核多少算力」都会跟着错。

最后一个观察,也是这一讲想让你带走的东西: Blackwell 这一代的动作 —— 累加器搬出寄存器、单线程发射、 操作数放进专用内存 —— 方向上是在朝 TPU 靠。 它在矩阵乘这一块主动放弃了「每个线程各管一片」的动态性, 换来更大的块和更少的调度开销。 而在别的地方,GPU 依然是那台什么都能干的动态机器。 两条路没有合并,但在最赚钱的那个部件上,它们靠近了一步。

出处:NVIDIA PTX ISA 文档(mma / wgmma / tcgen05.mma 三代指令)、NVIDIA CUTLASS 的 Blackwell 功能文档、 Colfax Research 的 Blackwell Tensor Memory 教程。 「Blackwell 在朝 TPU 靠」是我的判断,不是任何一方的官方说法。
⭐ 上半节的账:五层拆下来,少掉的东西是同一类东西 整颗 → 一个核 → 核里那块地 → 并行层级 → 一条指令 —— 五层,每一层 TPU 那边都少了点什么。
但少掉的从来不是「算的部件」。 少掉的是 cache、是记分板、是那几层用来「等到跑起来再决定」的机构 ——  每一样都是为「不知道你要跑什么」准备的。

所以第 0 节那句话,到这里已经被验了五遍处处都不同,而每一处不同追到底都是同一句「早就知道你要跑什么」。
—— 下半节把这五层零件同时装回去,看它们合起来是什么样。
第 3 节 · 下

⭐ 装回去:同一个 FlashAttention,两边并排走完一遍

上半节拆出来的零件,现在全部要同时上场
这半节做两件事:先把算子走完一遍(3.6), 再回头兑现前面立过的两个说法 ——  3.6 末尾兑现第 0 节那句「处处不同、只有一个成因」, 3.7 兑现第 1 节那个没解释过的巧合:两边峰值为什么在同一量级。

3.6 ⭐ 高潮:同一个 FlashAttention,两边并排走完一遍

这一节只回答两句话:这一锅能做多大? 以及两边分别是被什么卡住的?

上半节拆出来的全是零件,这一节把零件装回去 ——  第 2 节挑的那个算子,现在真的在两块硬件上各跑一遍

🍳 先用一个厨房里的比方把整节铺开 —— 后面所有数字都挂在它上面 把这道算子想成一道要三个步骤的菜:切、炒、装盘。
笨办法是这样:切完了,把半成品送回冷库;要炒了,再从冷库取出来; 炒完再送回去;要装盘了,又取一次。三个步骤,冷库来回跑四趟。
—— 而这道菜的麻烦在于:那个半成品特别大。 原料一共才一百多兆,半成品三十多个 G,比原料大 256 倍。 来回搬四趟就是一百多个 G 全花在路上,而这一路上一刀都没切。

FlashAttention 干的事就一句话:三个步骤在灶台上一口气做完, 半成品根本不下灶台。

这个比方只有三个本体,记住它们,后面就不用再翻译了:
· 冷库 = HBM(片外主存,大但远)
· 灶台 = 片上暂存(GPU 的共享内存 / TPU 的 VMEM,小但近)
· 刀有多宽 = 一条指令一次吃多大一块(就是 3.4 那个 16 对 256)
—— 开头那两句话,画面就在这口灶台上: 一锅能做多大 = 灶台有多大;被什么卡住 = 撞的是灶台还是刀。

比方铺完,现在换成真数字。先回答一个还没正面回答的问题:为什么非分块不可? 第 2 节说「需求比供给多 5 倍」是个比值,这里给绝对量。

先看这一节要解决的到底是什么 —— 128 GiB 全花在路上,一次乘法都没做 3.6 的引子 HBM 往返:真正的开销 留在片上:省下来的 Q / K / V / O:无论如何都要搬的 一个头、一个样本、序列 128K、head_dim 128、bf16 131,072 序列长度 n × 131,072 n × 2 B bf16 32 GiB 注意力矩阵 S 一个人就这么大 而 Q / K / V / O 四个加起来只有 128 MiB。中间产物比输入输出大 256 倍。 ❌ 朴素写法:S 要在 HBM 里过四趟 每一步都是「算完写回去,下一步再读出来」 ① 算 S = QKᵀ 写 S → HBM 32 GiB ② 算 softmax 读 S → HBM 32 GiB (还是第 ② 步) 写 P → HBM 32 GiB ③ 算 O = PV 读 P → HBM 32 GiB 合计 128 GiB 的 HBM 往返 —— 这一大笔里,一次乘法都没有 ✅ FlashAttention:S 从来不落 HBM 分块 + online softmax,一块算完就地更新结果 片上暂存(共享内存 / VMEM) ① 取一块 K/V + 一块 Q ② 算这块的 S,更新跑动最大值 m ③ 旧结果先乘 exp(旧 m − 新 m) 缩到同一把尺子 ④ 再把这块加上去;分母同样跟着缩 循环下一块;最后除一次分母 —— 与看完整行等价 代价要说清楚 K/V 要按 Q 的分块重复读若干趟,这一 项不是零。所以收益取决于「S 那一项原 本占多大」—— 序列越长越划算,短序列 可能不值。 S 那 128 GiB 整项消失 —— 而结果与朴素写法数学等价 🔢 三笔账,只有一笔变了 计算量 FLOPs 前向不变 反向多重算一次:7 次对 6 次(见 2.3) HBM 读写 O(n²) → O(n²/块大小) 省的全在这一行 显存占用 O(n²) → O(n) 32 GiB 的中间产物不存在了 ⭐ 落点:这一节问的「一个数怎么从 HBM 走到计算单元」,不是学术问题 过去五年最重要的那个 kernel,做的事跟计算单元一点关系都没有。它没有换算法、没有减前向的 FLOPs、没有用新指令 —— 它只是把数据在路上的走法改了一下。 所以接下来要一站一站走完这条路,而且两边并排走:同一个 FlashAttention,在 GPU 上和在 TPU 上,每一站分别落在哪、由谁决定。 ⚠️ 出处分层 当场算的:131,072² × 2 B = 32 GiB;四趟 = 128 GiB;Q/K/V/O = 4 × 131,072 × 128 × 2 B = 128 MiB。式子都写在图上,可以自己复核。 公开结论:前向 FLOPs 不变、显存 O(n²)→O(n) —— FlashAttention 原始结论。HBM 读写这里写成 O(n²/块大小) 是简化说法,论文原式是 O(N²d²M⁻¹)(M 为片上容量)。「短序列可能不划算」也是公开说法,本图不给具体门槛。
图 P-38 图 P-38 128 GiB 全花在路上,一次乘法都没做。序列 128K 时注意力矩阵 S = 32 GiB,朴素写法要写 S/读 S/写 P/读 P 四趟;而 Q/K/V/O 加起来只有 128 MiB。FlashAttention 的全部内容就是把这一项删掉 —— 前向的 FLOPs 一个都没省(反向要重算,7 次对 6 次)。图上那四步把 online softmax 拆开了:新块把最大值抬高之后,旧结果先乘 exp(旧 m − 新 m) 缩到同一把尺子上再相加 —— 等价性全挂在这一步。右边那张黄卡如实标了代价:K/V 要按 Q 的分块重复读,不是零。
这张图和 §1 §2 那次算子融合是同一件事(回指,展开看对照)
⭐ 你刚看完的这张图,讲的就是 §1 §2 那个算子融合 —— 这次是全程摊开的样子 🍳 回到那个比方:融合就是「三步在灶台上一口气做完,半成品不下灶台」。 上图左半边红的那一列,就是「每做一步送一趟冷库」;右半边绿的,就是灶台上做完。
上图那三步之间,32 GiB 的 S 每次都要落一趟 HBM; 融成一个 kernel 之后,S 整项消失。

所以那三笔账的形状,是所有融合共有的,不是 FlashAttention 独有的FLOPs 一分不省,省的全在「中间产物不落地」这一行。 —— 这就是为什么第 1 节说「算力除以带宽」是这门课的总闸门: 融合是唯一一种不改数学、只改数据走法就能往屋脊线右边挪的手段。

⚠️ 也正因为它不改数学,它的天花板不由算法定,由硬件定 ——  能融多大,取决于片上放不放得下。接下来两条路要走的, 说到底就是同一个融合在两块硬件上各能融到多大

要删掉哪一项清楚了,接下来看两边各自怎么删。先各走各的,走完再并排 ——  这个顺序有意为之:直接看对照表会以为两边处处不同, 各走一遍才看得出绝大多数站是一样的。

一个数走完全程 —— 从 HBM 到乘加单元,中间几站、每站谁在搬 片外内存 硬件自动管:有 tag、会 miss 软件/编译器显式管:不会 miss,也没有兜底 官方只给容量、没给带宽 NVIDIA B200 五站。中间的 L2 和 L1 是缓存 —— 命中不命中,要到运行时才知道。 HBM3e 192 GB 8.0 TB/s 片外 · 官方数字 L2 缓存 126 MB 4 个分区,每 die 2 个 整颗达到 21 TB/s(数据只落本 地分区时)、跨 die 16.8 —— 第 三方 TPU 没有这一站 L1 共享内存 256 KiB / SM 同一块硅,两种身份:左半硬 件管、右半软件管 共享部分最多 227 KiB / 线程块 · 128 B / 周期 / SM(三处 公开测量一致)· L1 命中约 39 周期 寄存器堆 256 KiB / SM 64K 个 32-bit 每线程最多 255 个 · 编译期分 CUDA Core 128 / SM FP32 / INT32 向量通路 硬件自动 miss 才往下走 硬件自动 程序管不着 要写指令 ld.shared 直接读 零延迟 TMA / tcgen05.cp 异步搬运:整块搬进 TMEM,绕开寄存器堆,搬运期间这个 SM 可以接着干别的活 TMEM 256 KiB / SM 128 lane × 512 列 Blackwell 新加的一层 · 只有 Tensor Core 用得到 Tensor Core 4 / SM 1,024 乘加 / 周期 ← 推导值,推导链见图 G-2 直接喂 不过寄存器 这条链上最该记住的三件事 中间是「一站半」缓存。L2 和 L1 要存 tag、要 做替换,一部分硅面积和功耗花在「猜你接下来要 什么」上;共享内存不用 —— 它是暂存,和 TPU 的 VMEM 同一类东西,只是小两个数量级。GPU 两种都留着。 猜错了怎么办?换一个 warp 上来接着算。GPU 的延迟不是被消除的,是被别人的工作盖住的 —— 这就是它要塞 64 个 warp 的原因。 Tensor Core 那条支线是新东西。Blackwell 之 前,矩阵操作数必须先落进寄存器堆;TMEM 让它 整条绕过去了。 TPU v7 四站,而且中间那站不是缓存 —— 什么时候搬什么,编译期就钉死了。 HBM3e 192 GiB 7.4 TB/s / chip 片外 · 官方数字 · 96 GiB / device VMEM 64 MiB / core 片上暂存,MXU 只从这里取数 没有 tag、不会 miss · 容量见 JAX 源码;带宽未公开 向量寄存器 官方未公开 形状是 8 × 128 的二维块 数量未公开;8×128 见于 Pallas 文档 VPU 向量单元 逐元素算子都在这儿 激活、归约、缩放 结果的回程 → VMEM → HBM 还是 DMA,还是编译期排好 对照:GPU 的回程要穿过 L2 DMA 引擎 编译期排好班 向量 load 直接读 算完往回 权重与数据直接推进阵列从来就不经过向量寄存器 —— 这一点 GPU 到 Blackwell 才追上 MXU 256 × 256 65,536 个乘加单元,一条指 令喂满 每 chip 几个:官方文档自相矛 盾,见 G-8 累加器 1 MiB / MXU 128 × (8×256) × 4 B 结果攒在这儿,不回寄存器 算完直接落 同一位置,TPU 的三件事 少一整层。HBM 直接进 VMEM,中间没有 L2。省 下来的不只是面积,还有「不知道会不会命中」这 件事本身。 没有 warp 可以换。猜错了没人替你顶班 —— 所 以 TPU 不能猜,必须由编译器在编译期算准每一 拍的数在哪。 片上容量和带宽官方没公开,所以这里不填数字。 能确定的是层级结构和搬运方式 —— 那才是这张图 要讲的。 把两条链叠起来看 —— 差别不在快慢,在「谁负责知道数在哪」 缓存 vs 暂存 同样是片上 SRAM,性格完全不同 GPU 的 L1/L2 是缓存:你只管发访存指令,命中不命中它自己处理 —— 代价是要存 tag、要做替换、要维护一致性,而且时间不可预测。TPU 的 VMEM 是暂存:编译 器显式发 DMA 把数据搬进来,没有 tag、没有 miss、时间可预测 —— 代价是编译 器算错了就是真的慢,没有兜底。 谁发起这次搬运 同样是「把数弄过来」,发令的人不一样 GPU 侧:计算单元自己发一条 load,地址是它算的,什么时候到不知道 —— 搬运是 取数指令的副作用。TPU 侧:搬运是一条独立的 DMA,描述符里写清「从哪到哪、多 大、什么步长」,由专门的引擎执行,计算单元完全不参与。一边是「我要,你给我 找」,一边是「你先搬好,我到点来取」。谁来盖住这中间的几百个周期 —— 那是下 一张图的事。 GPU 正在往这边挪一步 TMEM 是个信号 Blackwell 新加的 TMEM,是一块只给 Tensor Core 用、由指令显式搬进搬出、不 参与缓存机制的片上 SRAM —— 这个描述几乎就是 TPU 的 VMEM。方向很清楚:在矩 阵这条路上,「让硬件猜」的收益越来越小,不如把控制权交回给编译器和 kernel 作者。
图 G-6 图 G-6 B200 侧五站,中间两站是缓存。命中不命中要跑起来才知道 —— 这就是 2.1 主路那 4 个红框的物理来源。 —— 出自《GPU 显微镜
一个数走完全程 —— 中间那一站是「暂存」,不是「缓存」 片外:HBM 片上暂存 计算 搬运(DMA,由编译器发起) 灰色虚线 = 官方未公开 五站,每一站只问一个问题:这一步是谁决定的 容量和带宽是每份 TPU 材料都会列的东西。真正决定你写代码时会撞上什么的,是下面那一整行 「谁决定搬」 —— 从头到尾没有一站的答案是「硬件自己看着办」。 ① HBM 片外主存 96 GiB 3,433 GiB/s / core 谁决定搬 编译器插一条 DMA 指令,描述符由标量单元发 一次多少 描述符说了算,最小 32 B 落空了怎么办 没有「命中/未命中」这回事 —— 只有「到了」 和「还没到」 ② VMEM 片上暂存(不是缓存) 64 MiB / core 谁决定搬 编译器静态分配,像分配寄存器一样,运行时不 会变 一次多少 一块 tile = (8, 128) = 4,096 B 落空了怎么办 放不下在编译期就知道 —— 要么自动分块,要么 编译失败 ③ 向量寄存器 VPU 的输入端 查不到 v7 的深度与个数 谁决定搬 编译器分配(VLIW 的槽位里直接写死用哪几个) 一次多少 一条向量指令 = 8 × 128 = 1,024 个元素 为什么画成虚线 v2/v3 的论文给的是每 sublane 32 深;v7 这一层官方没有公开,本文不拿旧代的数字顶替。 另外:矩阵操作数是从 VMEM 直接进 MXU、还 是也要过这一层,公开资料同样没有明写 —— 本 文不下结论。 ④ MXU 阵列 256 × 256,权重驻留 65,536 个乘加单元 / MXU 谁决定搬 不需要「决定」—— 权重整趟驻留,激活按拍推 进(见 §4) 一次多少 每拍吃一列 256 个激活,吐一列部分和 落空了怎么办 喂不满就是空转,没有别的活能顶上来(见 §3) ⑤ 累加器 结果的落脚点 1 MiB / MXU(推导见下) 谁决定搬 硬件自动累加;K 超过 256 时连续几趟都不落 一次多少 128 个 × 形状 (8, 256) × 32 bit 1 MiB 是怎么来的 128 × 8 × 256 × 4 B = 1,048,576 B —— 正好 1 MiB DMA 载入 喂入 落地 回程:累加器 → VMEM → HBM,同一套 DMA、同一批描述符 —— 回程同样是编译期排好的,不是算完了「顺手写回去」 「暂存」和「缓存」差在哪 —— 这是全图最要紧的一格 问题 GPU 的 L1 / L2(缓存) TPU 的 VMEM(暂存) 放什么进去 硬件按访问历史猜 编译器写死 有没有命中率 有,而且是主要调优指标 没有这个概念 猜错 / 排错的后果 变慢(多跑一趟内存) 停住(没有别的活可切) 你能控制到什么程度 间接:改访问顺序去哄它 直接:改分片和 tile 形状 搬运不是「后台自动发生」的 —— 它占着指令流 HBM ↔ VMEM 的每一次搬运都由 DMA 完成,而 DMA 的描述符是标量单元写出来的。也就是说「什么时候搬、搬多少、搬到哪」跟乘 加指令一样,占着同一条指令流里的槽位 这解释了 §2 里那个看起来很怪的设计:为什么标量单元在一颗以矩阵乘为业的芯片上还这么重要 —— 它不算数,它安排搬运 带宽落差是这条通路的真正约束:片上暂存的读写带宽比 HBM 高约一个数量级具体数值官方未公开,这里只给量级)。所以「尽量 让数在暂存里多待一会儿」不是风格建议。 这张图想让你记住的一句话 这条通路上没有任何一步是硬件在运行时决定的。搬什么、搬多少、什么时候搬、放在暂存的哪个位置 —— 全部在编译期写进指令流里。 所以 TPU 上的性能问题几乎不长成「缓存没命中」的样子,而是长成「形状不对,编译器排不出好班」的样子。这也是为什么 §7 那张图必须存在:把决定权全交给编译期,就得看看编译期到底能排出什么。
图 T-6 图 T-6 v7 侧这条路(到累加器为止),中间那站不是缓存,是暂存。每一站只问一个问题:这一步是谁决定的。从头到尾没有一站的答案是「硬件自己看着办」—— 这张图是 2.1 里「TPU 侧 0 个红框」的逐站展开。 —— 出自《TPU 显微镜

两条路各走完了,现在把它们并排放在一起 —— 这是本课的主图。

同一个 FlashAttention,两条路并排走完 —— 竖着看走向,横着看差别 3.6 主图 GPU · B200 TPU v7 结构上真正不同的地方 FlashAttention 在这一站做的事 片外主存 只有 Q / K / V / O 住这儿。 S 从来不出现。 硬件缓存 全图最大的结构差异 就在这一层。 片上暂存 主战场。tile 住这儿, 块能开多大看它的容量。 操作数缓冲 进计算单元之前, 要不要先落一次寄存器。 计算单元 一直在两种单元间跳 矩阵乘两次,夹一次 softmax。 GPU · B200 HBM3e 192 GiB · 8.0 TB/s 官方数字。整颗芯片共用。(厂商表上写 192 GB,物理上是二进制:一颗 stack = 8 层 × 3 GiB = 24 GiB,8 颗 = 192 GiB = 206 GB —— 两边同口径。) 8.0 TB/s L2 缓存 126 MB · 4 个分区 硬件自动,有命中率。程序管不着它留什么、赶走什么。 L1 / 共享内存 256 KiB / SM · 共享 ≤ 227 KiB / 线程块 左半:硬件管的 L1 右半:软件管的共享内存 tile 住这半边 —— 作者亲手 搬进去。 每块循环一次 寄存器堆 + TMEM 支线 各 256 KiB / SM Blackwell 之前矩阵操作数必须先落寄存器堆;现在 TMA/tcgen05.cp 整块搬进 TMEM,把寄存器绕过 算:两种单元轮流上 Tensor Core 4 / SM QKᵀ / PV CUDA Core 128 / SM online softmax 来回 TPU · v7 HBM3e 192 GiB · 7.37 TB/s / chip 官方数字。96 GiB / device(v7 是 2 device / chip)。 没有这一站 HBM 直接进片上暂存。 省下的不只是面积,还有「不知道会不会命中」这件 事本身。 一步到位 VMEM 64 MiB / core 整块都是软件管的 —— 没有 tag,不会 miss Pallas 用 BlockSpec 声明每块搬多大,剩下的编译器排 —— 不是作者亲手搬。 每块循环一次 向量寄存器 8 × 128 的二维块 · 个数未公开 ⚠️ 矩阵操作数是从 VMEM 直接进 MXU,还是要过这一层,公开资料没有明写 —— 本课不下结论。(图 T-6 把它画成 MXU 的输入端;那也是一种读法。) 算:两种单元轮流上 MXU 256 × 256 QKᵀ / PV VPU 向量单元 online softmax 来回 ⑥ 那个「来回」是谁安排的 —— 第 5 节那条主线,落在一个具体 kernel 上 GPU:warp 调度器 每个周期挑一次 运行时从几十个 warp 里挑一个数据到位的发。算得慢就换一个上来 —— 延迟是被别人的工作盖住的 TPU:编译器 + VLIW 槽 编译期排死,精确到周期 编译期定好哪条指令、第几周期、哪个发射槽。没有第二个任务顶班 —— 排错了就是真的停住。 📐 第 ③ 站两边差多少 —— 与其写个数字,不如画成面积 = GPU 共享内存 227 KiB(左上角那一格) 绿框整片 = TPU VMEM 64 MiB / core 64 MiB ÷ 227 KiB ≈ 289 倍。左边那 289 个小格里,GPU 只占蓝色那一格 ⚠️ 这是每单元(一个 core 对一个 SM),不是总量 —— 按总量比方向相反,见 3.2b。 而块能开多大,看的正是每单元这一格 —— 块越大,K/V 重复读的趟数越少。 ⚠️ 顺带统一一个口径 本课此前一处写 TPU「五站」、一处写「四站」——两个都对:一处沿 MXU 支线数,一处沿 VPU 主路数。数的 是两条不同的路,所以这张图不报总数,只问谁在这一层。 ⭐ 落点:这两条路上只有两处是结构性的,其余都是参数 第 ② 站:GPU 有一整层硬件缓存,TPU 那里是个窟窿。  第 ⑥ 项:那个来回由谁安排 —— 一边每周期现挑,一边编译期排死。其余四站都是「同一件事,两边各有一个部件」,只是容量和名字不同。 而这两处恰好决定了 FlashAttention 在两边是两种性质的工作 —— 下一张图专门收这个口。 ⚠️ 出处分层 沿用图 G-6 / T-6 的标注,含那里已写明的成色:L2 带宽是第三方实测、TPU 片上带宽与向量寄存器个数官方未公开 本图当场算的:64 MiB ÷ 227 KiB = 288.7 ≈ 289 倍。格阵 17×17 恰好 289 格,一格不多一格不少。
图 P-39 图 P-39 这一节的主图:同一个 FlashAttention,两边一站一站并排走完。横着读,每一行是同一站。六行里只有两行是结构性的 —— 第 ② 行(GPU 有一整层硬件缓存,TPU 没有)和第 ⑥ 行(切换由谁安排)。中间那条黄带顺手统一了本课此前不一致的「TPU 到底几站」:两处数的是两条不同的路,所以不该报总数。
⭐ 上面那张图,用大白话说就是一句 图注点名的第 ② 行(多一层自动缓存),上半节 3.2 到 3.2c 已经讲透了。 真正没展开过的是第 ⑥ 行 —— 谁决定什么时候换手。 这道菜要在两种炉子之间来回跳 —— 矩阵那个炉子和向量那个炉子GPU 是干活的时候临场挑,谁空了上谁;TPU 是提前排好班,几点几分谁上,一分不差。 —— 这一条就是第 5 节的全部主题,这里先埋个头。

拿第 1 节那条判据在这儿数一遍:六站里 只有这两站是「一边有、另一边整个没有」其余四站两边做的是同一件事—— 不是别处没差,是别处的差都在第 ②③ 格里,下一代就变了。
能融多大,是被两个数一起卡死的(回指 3.4,展开看那两个数)
⭐ 回到 3.4 结尾那句:能融多大,是被两个数一起卡死的 第一个数刚在上图里画成了面积 —— 工作台多大。 GPU 一个线程块最多 227 KiB
装三块 128×128 的 bf16 tile(Q/K/V)= 3 × 128 × 128 × 2 B = 96 KiB,再留出双缓冲,基本就到顶了
块开多大这件事,在 GPU 上是被容量直接顶住的。 TPU 那边 64 MiB 是它的 289 倍,容量在这一侧根本不是那个卡住人的东西。

第二个数是 3.4 那个粒度 —— 一条指令吃多宽。 GPU 收缩维 16,TPU 是 256。 所以同一个 head_dim=128:在 GPU 上是 16 的整 8 倍,怎么切都齐; 在 TPU 上只填满 256 那条边的一半
—— 这就是两个约束的分工: 🍳 回到比方,这两堵墙就是一句话。
GPU 是灶台太小。只能一小块一小块地做,同一批料要反复往上端 ——  K/V 就是这样被重读的。
TPU 灶台大得根本用不完,它的问题换成了刀太宽。 一刀二百五十六宽,你的料只有一百二十八宽,砍下去半条刀刃是空的。
同一件事,两边撞的是不同那堵墙 ——  这也是为什么 3.4 说这两个数要一起看,单看哪个都会得出错的结论。
⭐ 同一个融合,落到两块硬件上差在哪 —— 三条,都是「两边」才有的话 ① 卡块大小的墙,一边一堵,另一边三堵。
GPU 那边只有容量一堵 —— 227 KiB 顶死,方向反倒清楚:能开多大开多大。 而块开不大,代价是同一批 K/V 被反复端上灶台。
TPU 那边是三堵:往上撞容量(先爆反向),往上还先撞并行度 (块一大,KV 方向只剩一块,流水塌 —— 装得下却更慢),往下撞碎块开销。 所以「块越大越好」在 TPU 上是错的。

② GPU 必须回答「谁切、谁共享」,TPU 根本没有这个问题。
GPU 是几百个小单元一起啃,就得决定切哪一维; 切错了就要在中间做归并、同步、加起来。
TPU 是一个核把一个头的一个 Q 块从头做到尾 ——  并行来自向量单元那 8×128、核里的几个 MXU、以及一颗芯片上的两个核, 不存在「分给几百个人再合」这一步 (切哪一维才不用合、FA 两个版本为此换过两次分工 ——  那是专题三第一节的事。)

③ 要算的乘加完全一样,能拿到的峰值不一样。
同一个数学,FLOPs 一分不差。但 GPU 多付搬运(K/V 重读), TPU 多付空转
TPU 空转的原因是形状head_dim = 128 撞上 256 的 MXU, 而且两个矩阵乘各撞一次 ——  QKᵀ 那步 128 落在收缩维,PV 那步 128 落在输出维。 于是这个算子的 MXU 利用率封顶 50%,调 kernel 参数救不了 —— 这里说的是 kernel 参数那一层。真出路有两条,都在 3.4: 改 head_dim(模型架构的决定,不是调参 —— 32 头 × 128 换成 16 头 × 256, 实测 MFU 在 8K/16K/32K 上分别 +21% / +32% / +46%),或者全转置流水线

这三条能收成一句话阵列有两个方向是空间、一个方向是时间; 空间那两维不够大就空转,时间那一维不够长就摊不动装权重的钱。 head_dim = 128 卡在中间 ——  当空间嫌它大不起来,当时间又嫌它短 GPU 那边阵列小得多,128 怎么摆都齐,所以它撞的是容量; TPU 阵列大到 128 填不满,所以它撞的是形状 (这条判据怎么来的、三种数据流为什么都救不了 ——  在 3.4 那两个折叠里。)

再往下就是 FlashAttention 自己的事了 ——  那张内外循环的经典图、两个版本为什么把循环掉了个个儿、 块大小的实测扫描、以及三层效率天花板的拆解, 都在 专题三 · 注意力演进 第三节 这一讲只借它把两块硬件并排走一遍。

灶台的第二个上限:端菜的速度(厨房比方的补充)
🍳 灶台除了面积,还有第二个上限:端菜的速度 上面那两堵墙管的是「能融多大」。§1 那条 64 管的是另一件事 —— 融完之后还快不快料从冷库挪上灶台,这趟运输没有消失, 只是换了一段路,而那段路也有它自己的宽度。 所以融合之后要重新对一次账:拿新的强度跟 64 比,不是跟 312 比。

最后一张收账。不看站点,只看「你要写多少东西」 ——  这是这趟旅程留给写代码的人的那一面。

同一个算法,两种性质的工作 —— 一边亲手搬,一边只声明形状 3.6 的收尾 GPU:写在 kernel 里 TPU:写在声明里 容易说过头的地方 先说两边完全一样的部分 —— 不一样的只有最后一行 数学 online softmax:逐块更新最大值与分母,新块来了先把旧结果按新最 大值重标定一次 FLOPs(前向) 一个都不差,与朴素写法相同 · 反向要重算,7 次对 6 次(2.3) 结果 与不分块的注意力数值等价 不一样的是:谁来安排搬运。 GPU · 手写 CUDA kernel 复用被显式搬进软件管的那半边共享内存 你要亲手写的 块开多大:算到 Q / K / V 块加中间量刚好塞进 ≤ 227 KiB 发搬运指令cp.async / TMA 把下一块从 HBM 拉进共享内存 双缓冲:搬下一块的同时算这一块,两个缓冲区自己轮换 warp 分工:一组 warp 专发 TMA 当生产者,另几组专发 wgmma 当消费者 —— 搬的和算的分开,流水线才叠得起来 对齐:Ampere 上是 __syncthreads()Hopper 起换成 mbarrier 异步屏障 —— 搬完第 k 块就报到,算的那组等到 就开工,同时搬 k+1 要不要占 L1cp.async.ca 走 L1+L2,.cg 只走 L2 硬件替你兜的 warp 调度器每周期挑一个数据到位的 warp。你没搬完的那段延迟,有机会被别的 warp 的计算盖住 —— 所以 GPU 上「 搬慢一点」往往不是致命伤。 TPU · Pallas / Splash Attention 只声明块的形状,搬运的时序交给编译器 你要写的 BlockSpec:每块多大,以及第 i 块取原数组的哪一段(index_map grid:一共几块,循环怎么套 编译器替你做的 DMA 什么时候发、双缓冲怎么错开、VMEM 怎么分 —— 全在编译期排好,源码里没有对应的那几行。 顺带白拿一样:Splash Attention 支持块级掩码causal / sliding window 下整块用不上的,直接不算 —— 省的不 是搬运,是 FLOPs。 ⚖️ 右栏比左栏短,这件事本身就是结论少写的那四条没有消失,只是挪进了编译器。 ⚠️ 这句话只对一半:「FlashAttention 绕开了缓存」 L2 绕不开 —— 所有 HBM 访问都要过它,没有哪条指令能跳过。真正能选的只有 L1cp.async.ca 走 L1+L2,.cg 只走 L2。 绕开的不是缓存本身,是对缓存的依赖。复用被显式安排进共享内存之后,命中率高不高就不再决定性能了 —— 这才是那句话想说的意思。 ⭐ 落点:第 5 节那条主线,在一个具体 kernel 上长这样 GPU 上,FlashAttention 是一段和硬件的猜测协商的代码:硬件准备了缓存和 warp 调度器来兜住不确定性,而这个 kernel 不需要那种兜底,于是自己接管了搬运。TPU 上没有可绕开的东西,于是只剩下声明形状。 代价也对称GPU 的手写 kernel 换一代硬件要重调;TPU 的声明写错了,编译器排出来就是真的停住,没有第二个任务顶班。同一个算法,两种工程性质 —— 这就是运行时与编译期的分工,落在一个 kernel 上。 ⚠️ 出处分层 查到的cp.async.ca.cg 缓存行为出自 PTX ISA;BlockSpecindex_map / grid 出自 Pallas 文档;Splash Attention 的块级掩码出自其公开实现。 沿用本课已有标注:共享内存 ≤ 227 KiB / 线程块出自图 G-6。「FlashAttention 把复用显式放进共享内存」是公开实现的通行做法,不是某一份文档里的原话。
图 P-40 图 P-40 同一个算法,两种性质的工作。数学、FLOPs、结果三样完全相同 —— 差的只有「谁来安排搬运」
GPU 侧 6 条要 kernel 作者亲手写:块多大、发 cp.async/TMA、双缓冲、warp 分工、对齐、要不要占 L1。
TPU 侧只有 2 条声明BlockSpec 与 grid。右栏比左栏短,这件事本身就是结论。红带按住了最容易讲过头的那句:L2 绕不开,绕开的是对缓存的依赖。
⭐ 走完一遍之后,第 0 节那句话兑现了 六行里四行两边是一样的。—— 这就是「处处都不同,但只有一个成因」的实证: 不是我们挑了个有利的例子,是把全程摊开数出来的
(那两行本来就是同一处 —— 有没有自动缓存、谁安排换手, 问的都是这个决定要不要留到运行时做。)
而第 2 节那 4 个运行时决策点,走到这里不再是抽象的「决策点」, 它们变成了左栏那几行你必须亲手敲进去的东西

⚠️ 但千万别把「右栏短」读成「TPU 更省事」。 那两条声明写错一个字,编译器不会替你兜底 ——  左栏是「你要做很多决定」,右栏是「你只有一次机会做对」。

3.7 回到第 1 节那个巧合:两边峰值为什么在同一量级

第 1 节开场把两个峰值并排摆出来(2,307 对 2,500),只说了「同一量级」, 没说为什么。走完全程之后,这个「为什么」现在算得出来了 ——  而答案恰好是上半节那两个差得最离谱的数,自己把自己抵消了。

🍳 下面那张图那一堆数字,一句话的画面版 两颗芯片的刀刃总量几乎一样,切法完全不同 ——  一边切成很多把小刀,一边切成几把大刀。

⭐ 所以这一节从头到尾在讲的不是「谁的算力大」, 是「同样多的算力,切成几把、每把多大」

看图的时候盯住右边那张账。同一把刀有两个尺寸,图上都给了,别混成一个 —— 刀有多宽(一次吃多深)就是 3.4 那个收缩维,16 对 256; 一下能切下多少(每拍多少次乘加)是 1,024 对 131,072,差 128 倍。

同一把尺子上 —— 两个方块面积等比,总量几乎一样切分粒度差 128 倍 B200:一个 Tensor Core TPU v7:一个 MXU 推导值 —— 推导链与它的验尸见正文 §2 / §8 NVIDIA B200 一整颗 148 个 SM × 每 SM 4 个 Tensor Core 592 个 Tensor Core × 每个 1,024 乘加 / 周期 = 606,208 1 个 Tensor Core 1,024 乘加 / 周期 (推导值) 面积比 1.16× TPU v7 一整颗 chip 2 个 TensorCore × 每核 2 个 MXU —— 官方数字,见右下 MXU 256×256 131,072 乘加/周期 MXU 256×256 131,072 乘加/周期 MXU 256×256 131,072 乘加/周期 MXU 256×256 131,072 乘加/周期 4 个 MXU 网点只示意密度:真按 256×256 画,每 cell 只有 0.27 像素 × 每个 131,072 乘加 / 周期 = 524,288 把账摊开 B200 TPU v7 chip 倍数 每周期乘加 · 总量 606,208 524,288 1.16 × 切成多少个独立单元 592 个 Tensor Core 4 个 MXU 148 × 单个单元多大 1,024 乘加/周期 131,072 乘加/周期 128 × ↑ 两行为什么不等 148 ÷ 1.16 = 单元大小差 128 × 自洽 公布的峰值 ˟ 2,250 TFLOPS FP16 2,307 TFLOPS BF16 1.03 × 这些数怎么来的 148 第三方拆解,×4 官方; 每核 1,024 官方 + 推导 4 MXU × 262,144 FLOP/ 周期 × 2.2 GHz,三个数全 是官方的 见左 右边这一列不是推导,是官方的。Google 工程博客逐字给出:262,144 FLOP/周期/MXU × 2.2 GHz × 4 MXU = 2,307 TFLOPS  → 262,144 ÷ 2 ÷ (256×256) = 每 cell 每周期 2 次乘加  但 128×128 那几代(v3/v4/v5e/v5p)全部还原成每 cell 1 次 所以这个常数是分代的。本文上一版把老架构上验过的「每 cell 1 次」顺手套到了 256×256 上,算出 8 个 MXU —— 错了,返工记录见正文 §8 ˟ 2,250 是 HGX B200 的口径。NVL72 里那颗 GB200 更高 —— 每 GPU dense BF16 约 2,500比域内算力 时要用后者,别混 SKU。 这 128 倍意味着什么 —— 三个方向,没有哪边天然更好 GPU 为什么必须切碎 细粒度买来的是「什么都能跑」 一颗 B200 上同时挂着许多互不相干的线程块,谁先算完谁先走。要让通用调度器管 得住,单元就得小到一个 warp 就能独占一个。代价是这 592 份里每一份都要自带 操作数通路、累加器和控制逻辑 —— 控制的开销乘了 592 遍 TPU 为什么敢切粗 粗粒度买来的是「几乎没有开销」 一个 MXU 一条指令就吃满 65,536 个 cell(131,072 乘加 / 周期),这意味着 取指、译码、控制这些开销被摊到六万多个乘法上,趋近于零。但它只有在「确实有 这么大一块矩阵要算」时才成立 —— 而这件事得由编译器在编译期保证,做不到就空 转。 所以什么形状喂得满 回到图 G-4 那个例子 GPU 的收缩维 K 一直是 16,几乎什么形状都是它的整数倍,喂满很容易。TPU 的 MXU 收缩边是 256 —— 注意力里常见的 head_dim=128 只能喂满一半。同一个模 型配置,在两边的「浪费」完全不在同一个位置,这就是为什么调优经验不能直接搬。 别把这张图读成排名 —— 它是一张取舍图,不是分数表 总量接近、峰值接近,说明两家在同一代工艺上做出的算力密度是可比的。真正的分歧在前面几张图里:谁来知道数在哪(G-6)、谁来盖住延迟(G-7)、切多细(本图)。这三个选择互相咬合 —— 换掉任何一个,另外两个都得跟着换:切得细就必须有大寄存器堆去养替 补,养了替补就不需要编译期排班,不排班就只能靠 cache 兜住不确定的延迟。反过来那条链同样成立。
图 G-8 图 G-8 总量几乎一样,切法完全不同。两个方块面积按每周期乘加次数等比,内部按真实单元数切。两个比值别混:份数差 148 倍,单个单元多大差 128 倍 —— 3.4 说的是后者。 —— 出自《GPU 显微镜
⭐ 这张图最值得记的是右边那个 148 ÷ 128 它不是约等于,是精确等于148 ÷ 128 = 1.156, 而 606,208 ÷ 524,288 = 1.156
—— 「份数多 148 倍」和「每份小 128 倍」这两个数几乎抵消掉了, 这才是两边峰值差不多的真正来源 剩下那 1.16 倍的差,TPU 用时钟补回来 —— 2.2 GHz 对 1.83 GHz 高两成, 2,307 反超 2,250,就是这张图右下那格的 1.03 倍。
⚠️ 这条链算的是 HGX 板上的 B200。第 1 节开场那对是 2,307 对 2,500 —— NVL72 里那颗 GB200,时钟约 2.06 GHz,只高不到一成, 压不过那 1.16 倍,于是变回第 1 节那个「GPU 高 8%」。 —— 份数和单元大小两边完全一样,这才是「峰值始终在同一量级」的来路; 谁在上面,看你比的是哪一颗。

所以整节课那些倍数,说的全是「份的大小」,不是「算力大小」。 两边的硅在同一个量级上,只是一个切成 592 小份,一个切成 4 大份。
第 4 节

64 颗连在一起:两边连的方式不一样

这一节是读第 6 节那组实测的前置
那组数是 64 对 64 —— 而「64 颗怎么连起来」这件事, 两边的答案在结构上根本不是一回事。
这一节只画硬件:几条链路、每条多宽、连成什么形状、最远几跳。 哪种通信模式吃亏、EP 为什么对拓扑最挑剔 —— 那是「切法」的事,在专题五。

64 颗连在一起 —— 同样是 64,两边连的方式不是一回事 这门课那组招牌实测就是 64 对 64。摆那两个数之前,得先知道这 64 颗各自是怎么连起来的 ⛔ 这张图只说结构,不说快慢 哪种通信模式吃亏、EP 为什么对拓扑最挑剔 → 专题五 GB300 · NVLink 5 + NVSwitch 每颗 GPU 18 条,一条接一台交换机 = 18 条轨 9 tray × 2 = 18 颗 16 个 compute tray × 4 颗 = 64 第 10 台交换机 = 第 10 条轨 四色都汇到这一台,其余 60 颗也在 四色 = 被点名的四颗,每颗都连满 18 台;其余 60 颗一样(1,152 条全画成一团) 一条轨长什么样,轨与轨之间又是什么关系 · 一条轨 = 一台交换机 + 它到全部 64 颗的链路,也就是一张完整的星 · 交换机彼此之间没有链路 → 18 条轨互不相干,谁也不经过谁(单层非阻塞) ⭐ 于是「任意两点一跳」在图上是数出来的:任取两颗,它们在每一条轨上都碰头 所以中间永远只隔一台交换机,而且有 18 条并行的路可选 —— 谁跟谁都一样,位置无关 64 张卡,而域的上限是 72 —— 64 整个装得下同一个域 TPU v7 · ICI 三维环面 没有交换机:每颗只连六个邻居(±x ±y ±z) 红色虚线 = 环绕链路(每个方向画一条示意,实际三个方向都首尾相接) x z y(往里) 这一颗的 6 条 全是直连邻居 x 环绕 y 环绕 z 环绕 一维上的 4 个点 首尾一接 —— 最远 2 步 (不接环绕要 3 步) 每一维 4 个点。不接环绕最远走 3 步,接上环绕只要 2 步 三维加起来: 2 + 2 + 2 = 6 跳 —— 最远的一对;近邻 1 跳 ⭐ 这一侧谁跟谁说话,贵不贵取决于离多远 ⚠️ 那 6 跳全靠环绕撑着 —— 所以申请的是「一个 4×4×4」,不是「64 颗」 ⭐ 同一套 ICI 一路铺到 9,216 颗,中途不换协议 ⚠️ 两边的「1 跳」不是一个单位 —— 这是这张图最容易被误读的地方 GB300 那一跳 是「卡 → 交换机 → 卡」:两段链路 + 一次交换。代价与位置无关,但它不等于零。 TPU v7 那一跳 一条直连线,没有交换机。所以 6 跳指的是「走 6 条线」,不是「慢 6 倍」 ⛔ 因此 1 对 6 只能读成结构差别,读不出延迟比。 每颗几条链路、每条多宽、这个数的官方口径为什么对不上 —— 「从一颗到一个 pod」那张图专门算这笔账
⭐ 同样是 64 颗,两边连的方式不是一回事。左边那 64 张卡整个装在 一个 NVL72 域里(域上限 72)—— 经交换机任意两点一跳,位置无关; 右边那 64 颗是 4×4×4 的三维环面,没有交换机, 最远 6 跳,谁跟谁说话贵不贵取决于离多远。
底带那一格是这张图唯一不能跳过的限定两边的「1 跳」不是一个单位 —— GB300 那一跳是「卡 → 交换机 → 卡」,TPU 那一跳是一条直连线。 1 对 6 只能读成结构差别,读不出延迟比。
这张图只说结构,不说快慢:torus 上的集合通信实测这门课一次都没跑过。

把这张图压成一句:NVL72 是一个「交换式的域」,ICI 是一张「直连的网」。
—— 交换式的域有上限(72 颗),但域内任意两点一跳、位置无关; 直连的网没有交换机、只连邻居,但同一套协议能一路铺到 9,216 颗。
而 64 这个数,恰好落在两边最不一样的地方它在 NVL72 的上限之内(整个域一跳可达), 而在 ICI 上它是一个 4×4×4、最远 6 跳的环面。

⚠️ 顺带堵一个特别容易混的数:两个 900 Hopper 的双向值是 900,Blackwell 的单向值也是 900。 两个 900 长得一样,说的不是一件事。
Blackwell 是双向 1,800、单向 900;而 GB200 一块板上有两颗 B200, 所以整块超级芯片对外是 3,600 GB/s每颗 GPU 仍然是 1,800—— 这类坑的通用形状:同一个数字在两代、两种口径下都出现过。 看到眼熟的数先问一句「这是哪一代、哪个方向」。
从一颗到一个 pod —— 关键不是能连多少,是在哪儿被迫换一套编程模型 TPU v7:ICI NVIDIA B200:NVLink 换协议的那一刀 灰色虚线 = 官方未公开 为什么是「环面」而不是「网格」 每颗芯片有 6 条 ICI 物理链路,对应三维的正负方向。把最边上的一颗和最那头的一颗接起来 —— 多接这一条,直径就少一半 红桩 = 环绕链路,接到那一头去;第三维同理,图上没画 环绕:第 3 颗的邻居就是第 0 颗 ← 一条轴 一颗芯片的 6 个出口 chip X− X+ Y+ Y− Z+ Z− 每条 200 GB/s(双向),6 条合计 1,200 直径的差别是实打实的:4×4×4 如果只是网格,最远要走 3+3+3 = 9 跳;接成环面之后是 2+2+2 = 6 跳。规模越大差得越多 —— 这决定了 all-reduce 的最坏时延。 但环面不是白拿的:它要求切片在物理上必须是连续的一块立方体。所以 TPU 上你申请的不是「64 颗芯片」,是「一个 4×4×4」 —— 形状本身是调度的一部分,这一点在 §3 那张表里就已经埋下了。 同一根对数轴上,两边各能走多远 横轴是一个互联域里的加速器颗数(对数)。真正要看的不是端点,是那条红线:过了它,你的通信代码就得换一套写法。 1 8 64 256 1,024 4,096 16,384 TPU v7 全程同一套 ICI,3D 环面一路铺到底 DCN 1 chip 1 台主机 1 个 cube 4×4×4 1 个 pod 9,216 颗 = 144 个 cube B200 NVLink 域 RoCE / RDMA 1 台机器(上一代 HGX) 1 个机柜 换协议就在这儿 128 倍的差距不在带宽上,在「不换协议能连多远」上。9,216 ÷ 72 = 128 —— 而且这两个数量级之间,TPU 那一侧集合通信的写法 一个字都不用改这句只在一个 pod 之内成立,跨 pod 走 DCN 时并行配置照样要改)。越过红线那一侧,实测 all-reduce 从 840 掉到约 325 GB/s2.6 倍,不是一个量级),而且通信库要换一条实现路径 反过来说也别夸大:单颗算力两边几乎打平(NVL72 里那颗 GB200 每 GPU dense BF16 约 2,500 TFLOP/s,TPU v7 一颗 chip 2, 307 —— 注意别拿 HGX B200 的 2,250 来比,那是另一个 SKU),而且在 72 颗以内 NVLink 的每颗带宽还更高(1.8 TB/s 对 1.2 TB/s)。这张图比的是拓扑能延展多远,不是单芯片谁快。 口径警告:「pod」有两个官方定义 同一批官方材料里,「pod」既被用来指 9,216 颗芯片的整机规模,也被用来指一个 256 颗的可售单元。两个都是官方说法, 相矛盾,而且没有一处说明哪个作准 所以看到「一个 pod」这四个字,先问是哪个 pod。本文提到 pod 一律指 9,216 那个,并且每次都把数字写出来。 这一节里我查不到的 1. 实际能一次调度到的最大切片。物理上 9,216 颗连成一个环面是官方数字,但「一个作业最多能拿到多大一块」取决于调度系 统,公开资料里没有一个可引用的上限 2. 环面在多大规模上会退化成非环。边缘切片能不能拿到环绕链路,公开资料同样没说。不猜。 1,200 GB/s 这个数,官方自己写拧了 官方正文写的是「每双向 200 GB/s」。可是三个轴 × 200 = 600,对不上同一页表格里的 1,200 只有把它读成「每条链路 200」才自洽:6 条 × 200 = 1,200。本文按这个读法画,并且在这里写明原文是另一种措辞 —— 遇到官 方文档自相矛盾,正确做法是标出来,不是挑一个顺手的悄悄用。 两份文档合起来的那一句话 GPU 那份的最后一张图讲的是:一颗 B200 里有 592 个 Tensor Core,而一颗 TPU v7 里只有 4 个 MXU —— 同样一块矩阵乘的活,份数差 148 倍,而单个单元多大128 倍 —— 本文反复说的那个 128 指的是后者,别顺口说成 592 对 4GPU 的协调主要发生在芯 片内部。 这张图讲的是另一半:TPU 一颗芯片里只有两个核要协调,但不换协议能一路连到 9,216 颗,而 GPU 在 72 颗上就得换。TPU 的协调主要发生在芯片之间。 两边不是「谁更强」,是把同一份复杂度放在了不同的地方 —— 这也是这整份材料从头到尾在说的同一件事。
图 T-8 图 T-8 3D 环面一路铺到 9,216 颗,全程同一套 ICI。9,216 ÷ 72 = 128 —— 这个数只衡量拓扑能延展多远,不衡量谁跑得快(图右上角那段专门按住这个误读,直接指过去)。 —— 出自《TPU 显微镜

但拓扑能延展多远,跟你实际拿得到多少是两件事。 下面是我们自己跑的数 —— 不是厂商标称,是实测掉了多少。

⚠️ 先把这组数的身份说清楚,它跟上面两张图不是一回事。 上面画的是结构(两边怎么连),这组是一侧的实测 ——  只有 GPU 侧,而且是同一个 NVL72 域内从 8 卡扩到 32 卡, 不是两边对照。TPU 侧的对应实测这门课没跑过(环面上的集合通信一次都没测)。 放在这里只为说明一件事:「域内没有距离」不等于「域内没有代价」。 至于哪种通信模式在哪种拓扑上吃亏 —— 那还是专题五的事。

实测:规模一大,all-to-all 就掉 —— 以及一个塌掉的对照项 Copy(源文档标注:节点内) Reduce(同样标注:节点内) Dispatch(跨节点) Combine(跨节点) 纵轴=相对 8 GPU 的百分比,不是绝对带宽 主图以 8 GPU 为 100% —— 画绝对值会把后三条压成贴地直线 80% 85% 90% 95% 100% 105% 8 GPU (2 节点) 16 GPU (4 节点) 32 GPU (8 节点) +2% -18% -9% -19% 相对 8 GPU 的带宽保持率 怎么读 —— 先讲我们错在哪,再讲能确定什么 ① 我们本来挑了 Copy 当对照项 → 它走错了路 原打算:Copy 不掉 → 链路够用 → 掉的只能是别的原因。但源文档写明 Copy 是「节点内 GPU 间拷贝」 —— 它不走 dispatch / combine 走的那条跨节点路。一个不出节点的量,对「出节点那条路够不够」没有信息。 ② 能确定的只有现象:目标数从 7 变成 31 8 卡时每张卡把 token 分给 7 个目标,32 卡时分给 31 个。单笔消息必然变小,而小块分散写的链路利用率低于大块连续传输。 源文档那个「每目标约 100 / 22 GB/s」是拿总带宽除目标数倒算出来的,不是独立测量 —— 拿它解释掉幅属于循环论证,这里不用。 ③ Combine 掉得更快,但不是因为「归约变深」 每个 token 只把 topk = 6 份部分结果加回去,这个宽度是配置死的,与规模无关,8 卡和 32 卡都是 6 份。更站得住的说法是: 要等齐的对端从 7 个变成 31 个,归约必须等齐才能收尾,等的人越多,最慢那个的影响越大。 而且 Reduce 也掉了 18% —— 它同样标着「节点内」 紫线和红线在图上几乎叠在一起。要么这条「节点内」标注不准,要么「节点内不受规模影响」这句话本身要打折。这处我们没查清, 标出来。 绝对值另附 —— 单位 GB/s,出处:本仓库 gpu/a4x/06-deepep-test/ 操作 8 GPU / 2 节点 16 GPU / 4 节点 32 GPU / 8 节点 Copy   源文档标注:节点内 5,600 5,500 5,700 Reduce  源文档标注:节点内 2,100 1,870 1,730 Dispatch 跨节点 all-to-all 700 660 636 Combine  跨节点 all-to-all + reduce 724 683 590 ⭐⭐ 本节落点,以及一条比结论更值钱的方法论 能确定的:规模一大,all-to-all 这类通信就掉,而且掉幅跟「要面对多少个对端」同向。不能确定的:掉幅里有多少来自消息变小、多少来自跨节点链路上的竞争。本图分不开这两者 —— 唯一的对照项 Copy 不出节点,而跨节点那条路我们没有独立的对照项。 方法论(这一条比上面的数据更该被带走):挑对照项时,第一个要问的不是「它稳不稳」,是「它跟被测对象走的是不是同一条路」一个走错路的对照项比没有对照项更危险 —— 它会让你以为自己排除了什么。 | 源文档对 16 卡那次下降写的是「RDMA 竞争加剧」、对 32 卡写的是「碎片化」。这两条其实不矛盾:跨节点确实走 RDMA(部署要点里 NCCL_NET=gIB,8 节点那次的失败根因就是 ibv_modify_qp),两个因素同时在,只是本图分不开。 | 主图归一化的理由: Copy 5,600、Dispatch 只有 700,画在同一根绝对值轴上,后三条会被压成贴地直线,而「掉了多少」恰恰是全部论点。
图 P-6 图 P-6 我们自己跑的数:同一个 NVL72 域从 8 卡到 32 卡,Dispatch 掉 9%、Combine 掉 19%。最下面那条橙带同样重要 —— 它写的是我们没能分清什么,以及为什么分不清
⛔ 这一节最容易被读成的那个错 9,216 ÷ 72 = 128 这个数只衡量「拓扑能延展多远」,不衡量谁跑得快。 把它读成「TPU 集群快 128 倍」是这门课里最容易犯、也最难自己发现的一个错。

而这一节能带走的那一句,是给第 6 节用的下一节摆那两个数的时候,记得它们跑在两种结构不同的网上 ——  一边域内一跳,一边最远六跳。 那组数里有多少是「算得快」、多少是「连得巧」,这门课没有拆开量过。 ⚠️ 而且 torus 上的集合通信实测,这门课一次都没跑过 —— 所以这一节只讲结构,不讲快慢。
—— 四种通信原语、NVL72 域内结构、跨芯片寻址长什么样, 在 L300 第 4 节哪种并行切法配哪种拓扑,在专题五。
第 5 节

⭐ 把那个词正式拆开:运行时,还是编译期

这对词你已经见过四次了 ——  2.1 那个「4 对 0」、3.3「哪几层是运行时才定的」、 3.3b 那三套电路、3.6 六站里两边分岔的那两站。
前面每一节都停在同一个决定上,只是每次换了个部件。这一节把那件事本身拿出来讲 —— 「谁来安排」这个决定,从硬件一路流到你写代码的那只手上。

5.1 硬件层:延迟归谁管

最底下这一层最能说明问题。取一次数要等几百个周期,这段时间干什么? 两边的答案不是「快慢」之分,是「谁负责」之分

延迟怎么被藏起来 —— GPU 让很多人轮流用同一个单元,TPU 让不同单元同时开工 这一拍在发指令 / 在干活 在等数据回来(虚线框) 气泡:没人能干活,这一拍白丢 尚未参与 / 已结束 NVIDIA B200 一个处理块(sub-core) 调度器每一拍只能发一条指令。谁的数据回来了谁就能上 —— 所以真正决定性能的不是单个 warp 多快,是手上有多少个 warp 可以 周期 → (示意,非等比) warp 0 warp 1 warp 2 warp 3 warp 4 warp 5 调度器发射槽 有没有指令被发出 气泡 气泡 第 6、7 拍两个气泡:六个 warp 全在等数,调度器无人可选。如果驻留的是 8 个 warp 而不是 6 个,这两拍就补上了 —— 能不 能补上,取决于寄存器还剩多少。 TPU v7 一个 TensorCore 没有「换一个 warp 上来」这回事。这里的四条泳道是四个不同的物理单元,本来就能同时动 —— 编译器要做的是让它们的起止时刻 正好对上。 周期 → (示意,非等比) 标量单元 个位数周期 W 向量单元 VPU 几十个周期 矩阵单元 MXU 几百个周期 DMA 引擎 几百个周期 单元占用 有没有单元在动 一条 bundle 322 位 · v2/v3 标量 ×2 向量 ×4 矩阵 推/取 ×2 杂项 ×1 立即数 ×6 发射槽 ×9 —— 这 9 个是同一拍并发出去的 立即数 ×6 不占发射槽 没有一个气泡。「矩阵槽是一推一取」这件事本身就说明问题:MXU 不是被调用的函数,是一条推进去、过一会儿取出来的流水线 —— 中间那段时间,别的槽照常在发指令。 这两条合成条不能直接比高低 —— 它们问的不是同一个问题。左边问「调度器这一拍发出指令了吗」(六个 warp 抢同一个发射端口,所以会空);右边问「有没有任何一个单元在动」(四条泳道是四个不同的物理单元,天然更容易全绿)。图上还做了一处简化:假定每个 warp 手上只有一条依赖取数结果的指令 —— 真实 kernel 里编译器会插入无关指令来填这些拍,气泡没有图上这么整齐。 那「有多少个 warp 可以换」由什么决定? —— 寄存器 一个 SM 有 64K 个 32-bit 寄存器,最多驻留 64 个 warp(2,048 线程)。warp 是整个现场都留在寄存器堆里才能随时切回来 的 —— 切换零开销的代价,就是所有人的寄存器必须同时占着。所以: 每个线程用的寄存器 这个 SM 能同时驻留几个 warp 占满 64 的比例 够不够盖住上面那 8 拍 32 个 64(撞上限) 100% 够,而且有富余 64 个 32 50% 128 个 16 25% 开始吃紧 255 个(上限) 8 12.5% 藏不住,气泡成片 这就是 CUDA 调优里那条最基本的取舍。循环展开、把中间结果存在寄存器里 —— 这些让单个 warp 变快的手段,同时也在减少能替 它顶班的人 那 TPU 凭什么能排得这么准? —— 因为延迟是常数 没有 cache,就没有「命中没命中」这个变量。取一个数要多少拍,编译期就是已知的:标量个位数、向量几十、矩阵几百。既然是 常数,编译器就能把 WAIT 放在正好那一拍 1 矩阵槽推进去 MXU 开始算,接下来几百拍它自己忙自己的 2 向量槽同时在动 把下一块权重推进权重 FIFO(后台那套权重寄存器) 3 DMA 槽同时在搬 把再下一块数据从 HBM 搬进 VMEM 4 杂项槽放一条 WAIT 算好放在第几拍 —— 早了空转,晚了 MXU 干等 5 标量单元检查同步位 MXU_BUSY 已清 → 直接跳下一条,不停顿 代价也很清楚:算错了没有兜底。GPU 猜错只是慢一点,还有别的 warp 顶着;TPU 这边 WAIT 放早了就是纯空转,放晚了 MXU 就 真的在干等。 所以这两句话是同一件事的两面 GPU:把不确定性交给硬件 有 cache,延迟就不是常数;不是常数,编译期就排不了班。于是只能准备一大批随 时能上的替补 —— 这直接决定了 SM 长什么样:256 KiB 的寄存器堆(装现场)、 64 个 warp 槽(装人)、4 个独立调度器(挑人)。这些硅不算一次乘法,它们的 全部工作是盖住等待 TPU:把不确定性消灭在编译期 没有 cache,延迟就是常数;是常数,就能在编译期把每一拍排满。于是不需要替补、 不需要大寄存器堆、不需要调度器 —— 省下的是这些电路本身,不是把它们换成了更 多乘加单元。图 G-8 数的是每周期乘加总量,不是面积占比 —— 而那张图的结论是 两边总量同一量级(切法差 148 倍)。「谁的硅用得更值」需要 die 面积,而那个 数官方不公布,本课不主张。 为什么这条差异比带宽差异重要 带宽差个百分之几十,是量的差别;「谁负责知道数在哪」是质的差别。它决定了两 边的编译器长什么样、kernel 怎么写、调优时该盯哪个指标 —— GPU 盯 occupancy,TPU 盯的是编译器排出来的时间线有没有空隙。
图 G-7 图 G-7 GPU 怎么藏延迟:一个 warp 卡住就换下一个。藏得住的前提是同时驻留的 warp 够多 —— 这就是「占用率」这个词的全部含义。 —— 出自《GPU 显微镜
延迟怎么被藏起来 —— 班表在编译期就排好了,跑的时候改不了 标量:发 DMA、算地址 向量:VPU 矩阵:MXU 杂项 灰色 = 这一拍这个槽是空的(NOP) 一个 VLIW 指令包 = 一拍要干的所有事,打成一包 合计 322 bit · TPU v2 / v3,公开论文 没有乱序、没有记分板 —— 哪条指令在第几拍发、发给哪个单元,全部写死在指令流里 标量 标量 向量 向量 向量 向量 矩阵 矩阵 杂项 立即数 ×6 这张槽位构成是 TPU v2 / v3 论文里的公开数字。v7 的 bundle 宽度和槽位构成,官方没有公开。这里画它,是因为「一拍多槽、由编译器填」这个结构本身跨 代没变,变的只是每种槽有几个。 同一段依赖,编译器怎么排 —— 示意,不是实测 trace DMA 在飞的这几拍,矩阵槽只能空着 —— 编译器要么找到别的活填进来,要么认了 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 标量槽 ×2 发 DMA 算下一块地址 发下一条 向量槽 ×4 上一块的归一化 / 激活(独立的活) 继续 矩阵槽 ×2 矩阵乘:tile 到位才能开始 杂项槽 ×1 同步 同步 第 14 拍之后所有槽都空了 —— 独立的活用完了。这四拍不会有任 何东西自动顶上来,因为没有第二个线程可切。 第 1–6 拍的向量槽里塞的是上一块的后处理 —— 和这次搬运没有依赖关系,所以能提前挪过来。这就是编译器「藏延迟」的全部手段:找独 立的活。 同一个延迟,两边怎么处理 GPU:运行时换一个跑 warp 卡住 → 调度器立刻挑一个能跑的。代价是要同 时驻留几十份上下文,那 256 KB 寄存器堆就是为它 准备的。 好处:形状不规则也能跑得不太难看。 坏 处:同一份代码,两次跑的耗时可以差很多。 TPU:编译期提前挪 编译器把独立的活挪进空拍。不需要驻留上下文,那 片硅省下来给了 MXU 和 VMEM。 好处:耗时高度可 预测,同样的形状每次都一样快。 坏处:挪不动就 是真空转,运行时没有第二次机会。 这个取舍在日常里长什么样 —— 三件事其实是同一件事 ① 形状一不规则,性能就掉得很难看 不是编译器不肯优化,是可挪的独立指令本来就少了。batch 小、序列不齐、专家路由不均 —— 这几种情况的共同点都是「后一步紧跟着前一步」,空拍没东西填。 ② 编译很慢,而且慢得有道理 v7 上一次 XLA 编译常见 10–17 分钟。它不是在「翻译」,是在替硬件把整张班表排完 —— GPU 那边这件事是每次运行时由调度器现做的,成本摊在了跑的时候。 ③ 但跑起来非常稳 同一个形状重复跑,步时几乎没有抖动 —— 因为根本没有运行时决策可抖。可预测性是这个设计 买来的东西,不是附带效果:容量规划、性能回归检测在 TPU 上都因此简单不少。 把 §2 那句话补完整 §2 说 TPU「省下了调度器、寄存器堆、记分板那片硅」。这张图是那句话的另一半:省下来的东西并没有消失,它被搬到了编译期。调度这件事总得有人做 —— 区别只在于是硬件每次运行时重做一遍,还是编译器一次做完。 所以「TPU 更简单」是个误解。它不是把复杂度删掉了,是把复杂度换了个地方放 —— 从硅上换到了编译器里,也从运行时换到了你写模型配置的那一刻。
图 T-7 图 T-7 TPU 怎么藏延迟:编译器提前把班排好。灰格子是真的空着,而且不会有任何东西自动顶上来。上面那排 bundle 构成是公开论文里 v2/v3 的,下面的甘特图是示意,不是实测 trace。 —— 出自《TPU 显微镜
两种藏法,各自会在什么时候失效 这一对图真正的价值不在机制,在失效条件 ——  GPU 那套在「备选不够」时失效。
负载一变得寄存器吃紧,能同时驻留的任务就变少 ——  而「同时驻留了几个」,就是 occupancy(占用率)。 备选一少,延迟就藏不住了。 调优第一步常常是砍寄存器用量,原因就在这。
TPU 那套在「算不准」时失效。
形状是动态的、访问位置要跑起来才知道 —— 编译期排的班就成了空班。
—— 所以两边怕的东西正好相反:一边怕资源紧,一边怕不确定。 记住这句,L300 5.5 那本移植账单就不用背了,能自己推出来。

5.2 代码层:同一件事,两种写法

硬件上的这个差别,会原样出现在你敲的字里

同一件事,两种写法 —— 你描述一个线程,还是描述整个数组 GPU / CUDA:SIMT,单线程视角 TPU / XLA:SPMD,整张图视角 灰底=这一侧付出的代价 GPU · CUDA —— 写一个 kernel,描述一个线程干什么 __global__ void add(float* a, float* b, float* c, int n) { int i = blockIdx.x * blockDim.x + threadIdx.x; if (i < n) c[i] = a[i] + b[i]; } 看这段代码里出现了什么:blockIdxthreadIdx、还有一句 if 边界判断。「我是第几号、我负责哪一格」写在代码里,所以切 分方式是你决定的 —— 也只能由你决定。 换个形状、换张卡,这段代码照样跑,但快不快就不好说了。 TPU · JAX / XLA —— 写整张图,描述整个数组怎么变换 c = a + b c = jax.lax.with_sharding_constraint( c, NamedSharding(mesh, P('data', 'model'))) 这段里一个下标都没有。你没说「谁算哪一格」,只说了「这个数组按哪根轴切」 —— 剩下的切分、通信、重叠,全由编译器生 成。 换个形状,整张图要重编译。v7 上实测十到十七分钟 把差别摊成四行 —— 每一行都是上面那个选择的直接后果 问的是同一个问题 GPU / CUDA TPU / XLA 形状什么时候定 运行时可变。同一份 kernel 喂什么形状都能跑 编译期必须确定。形状一变就重新编译 控制流 随便写。if / while 想怎么分支怎么分支 数据相关的分支很贵。MoE 路由这类写起来别扭 优化发生在哪 人写 kernel + 调库。优化是一处一处做的 编译器看得到整张图,做全局优化 出问题怎么查 打印、单步、profiler,看得见每一行 读编译产物、读 profile。看得见结果,看不见过程 静态的代价 形状一变就重编译。v7 上实测 10–17 分钟 —— 交互式调试基本别想。 变长序列只能绕。分桶 + padding,桶分粗了浪费算力,分细了编译次数爆炸。 数据相关的操作很别扭。MoE 路由、动态 KV 分配,都要改写成静态形状。 静态的红利 编译器看得到整张图。跨算子融合、通信与计算重叠、内存复用,这三样不用你操心,也不写在你的代码里 显存规划是确定的。所以能不占卡就提前算出会不会 OOM —— 这一条 GPU 侧没有对应物 —— 完整版 L300 §5.6 单讲。 而且这三样是免费的。你什么都不做就已经吃到了 —— 代价是不想要的时候也关不掉 一句话记住这一节 CUDA 把控制权交给你,XLA 把控制权交给编译器。所以两边的痛苦完全不是同一种:一边是「我写不出足够快的 kernel」,一边是「编译器不听话,而我插不上手」 注意这里没有哪一种痛苦更轻 —— 它们只是落在了不同的人身上:前者要求你队里有人能写 kernel,后者要求你队里有人读得懂编译产物。
图 P-1 图 P-1 同一件事,两种写法。左边 CUDA 里有 threadIdx,右边 JAX 一个下标都没有 —— 差别写在代码里,不在形容词里。下半两栏刻意一样长:这是取舍,不是优劣。

再往上一层,连集合通信也是同一个模式: GPU 那边 all-reduce 是你在训练循环里亲手写的第二步, TPU 那边你从头到尾没写过它 —— 它是编译器按你声明的并行方式自己插进去的。 (这一对代码在 L300 5.3。)

5.3 那编译器的边界在哪

说到这儿会有一个自然的反应:「那什么都交给编译器不就行了?」 —— 不行,而且边界画得相当清楚。

编译器能替你做什么,做不到什么 —— Flash Attention 是最诚实的那个例子 编排:编译器能自动做 算法:只能人来做 两边都得手写的那一格 编译器领地 · 把已有的算子编排好 算子融合 把相邻的小算子并成一个 kernel,中间结果不落回 HBM。 通信与计算重叠 看得到整张图,就知道哪段通信能藏在哪段计算底下。 内存复用 谁和谁生命期不重叠,就让它们共用同一块地址。 布局与切分选择 同一个矩阵摆成什么形状、按哪根轴切,编译期试着挑。 这四样的共同点:它们都不改变「算的是什么」,只改变「怎么排」。所以编译器有资格自己决定 —— 排错了顶多慢,不会错。 人的领地 · 换一个算法 改变数学上的计算顺序 Flash 把 softmax 拆成可增量合并的形式,于是注意力矩阵根本不必整个存在。这不是把已 有算子排得更好,这是换了一个算法 发明一个不存在的算子 编译器只能编排它认识的东西。一个从没有人写过的 kernel,它没有理由凭空想出来。 越过这条线,编译器就帮不上忙了 —— 两边都一样。 Flash Attention —— 一个两边都失效的反例,比任何一边的成功案例都更能定义边界 GPU 侧 CUTLASS / Triton 手写。NVIDIA 的编译器没有自己想出 Flash,是研究者写出来、再被库收编的。 TPU 侧 Pallas 同样手写。XLA 也没有自己想出 Flash,Splash 那一支同样是人一行行写的。 没有任何一侧的编译器自己长出了它。 Flash 改的是数学上的计算顺序 —— 分块算、增量合并 softmax、注意力矩阵不落 HBM。那是算法,不是编排。算法永远是人的活。 讲到这里要防一个误解:这不是说编译器没用。上面左边那四样,在 GPU 侧同样是 Triton / CUTLASS 这些工具在替人做 —— 区别只在于自动化的程度,不在于有没有边界。边界两边都有,位置也差不多。 那越过边界之后,两边拿什么写 —— Triton 与 Pallas 定位几乎一样,差别在底下 Triton · GPU 用 Python 写 kernel,编译到 PTX。你操作的心智单位是一个 program(一组线程)处理一块,共享内存和同步要自己安排。 Pallas · TPU 同样用 Python 写 kernel,编译到 Mosaic。你操作的心智单位是一块 tile 在 VMEM 里进出,搬运由 BlockSpec 描述, 不是自己写 load/store。 给「把控制权交给编译器」钉一个边界 编排是编译器的活,算法永远是人的活。这条线两边都存在,位置也差不多 —— 所以「TPU 要手写 kernel」不是 TPU 的缺点,GPU 上同样要写,只是写的人多、库更厚 真正的差别在越过边界之后有多少现成的东西可抄。这一条 GPU 侧目前确实占优,但它是生态差距,不是架构差距 —— 两者会随时间收敛,别把它们混成一件事讲。
图 P-3 图 P-3 编译器的边界在哪。左边四样是「编排」,它有资格自己决定;右边是「换算法」,越过这条线它就帮不上忙。Flash Attention 在右边,而且两边都得手写。
⭐ 注意 FlashAttention 落在哪一边 图注那句「两边都得手写」,把第 3.6 节那张对照表的意思补全了: TPU 那边少写的是搬运,不是算法。 推理引擎移植要付的完整账单、以及 AOT 编译能提前算出什么, 在 L300 5.5 / 5.6
⭐ 融合这条线走到这里收口:「融合」底下其实是两件事 你刚才可能已经绊了一下:上图左边第一格明明写着「算子融合」是编译器的领地, 落点却说 FlashAttention 编译器做不到。两句都对 —— 因为那是两种融合。

① 不用改数学的融合 —— 编译器替你做。 就是上图左上那一格:算的还是原来那些式子,只是不再走一趟冷库。 这一类你一行都不用写,两边的编译器都在默默做。
② 要改数学才能成立的融合 —— 永远归人。
FlashAttention 之所以能融,是因为先把 softmax 重写成了可以逐块增量合并的形式。
—— 原来的 softmax 融不了,卡在分母是整行求和: 没看完整行就算不出任何一个数,也就没法只留一小块在灶台上。 online softmax 换的就是这一点:边走边更新最大值和分母,最后补一次缩放,结果一模一样。 不是先有融合再顺便改算法,是为了让融合成立而换了算法。 —— 编译器有资格决定「怎么排」,没有资格决定「算的是什么」。

所以「融合由谁决定」这个问题,答案要拆成两半:
· 能不能融、能融多大 —— 硬件说了算 (3.6 那个灶台和刀宽,两个数一起卡死)
· 谁动手融 —— 看要不要改数学 (不改的编译器做,改的只能你自己写)
这也解释了一件乍看矛盾的事:融合是这门课里唯一贯穿始终的话题, 但它既不是纯硬件问题,也不是纯软件问题 ——  它恰好卡在两者交界上,所以研究硬件最后都会落到它上面。

⚠️ 顺带按住一个常见误读:第 3.6 节说「FLOPs 一分不省」, 指的是两次矩阵乘的量;online softmax 每块要多做一次缩放, 但那是 O(n·d),和 O(n²·d) 比可以忽略。 「不改数学」说的是结果等价,不是「一条指令都没多」。

5.4 回头接那句反驳:CUDA Graph 不就是 GPU 也编译期了吗

这一节讲到这里,最强的一个反驳一定会出现,而且它有一半完全成立: 整个 step 的 kernel 图提前录好、之后一条道跑到黑,收益是真的, 很多负载上开与不开是数量级的差别。

但要判断它推翻了什么,得先讲准它究竟为什么快 ——  最常见的解释是错的:省掉的不是「分支」,是 CPU 一条一条发 kernel 的开销。 —— 所以问题就落在位置上了。

冻结发生在哪一层 —— CUDA Graph 冻的是最上面那层,TPU 编译器冻到底 这一层是静态的(跑之前就定死) 这一层是活的(芯片在运行时决定) ① 编排层 下一个 kernel 是什么、 什么时候提交 ② 落地层 这个 kernel 的块,放 到哪个 SM 上 ③ 发射层 核在跑的时候,下一拍 执行哪条指令 GPU · 不开 graph 三层全是活的 运行时 CPU 每次现发一条。发得慢,GPU 就闲着。 运行时 硬件分发器启动时挑一个 SM,程序员指定不了。 每个周期 warp 调度器每拍挑一个数据到位的发 GPU · 开 CUDA Graph 只冻住了最上面一层 capture 时定死 整张 DAG 在 capture 那一下固定,之后只发一条 replay。这一层确实 变成静态的了。 运行时 一点没变。graph 里每个节点仍然是一次正常的 kernel 启动,块照样由 硬件现场分发。 每个周期 一点没变。调度器、记分板、warp 槽、常驻上下文 —— 全都还在,而且 全都还在工作。 TPU · XLA 编译 三层全冻,而且冻到周期 编译期 整个 step 编成一个程序本来就没有「主机一条条发」这回事 —— 不需 要 graph。 编译期 没有「放到哪儿」这个问题:用哪个部件写在指令里。 编译期 VLIW:哪条指令、第几个周期、哪个发射槽,全排死。卡住就是真的空转。 🧳 一个能记住的说法:行程单 vs 航班内部 CUDA Graph 相当于把整趟旅行的行程单提前打印好 —— 先坐哪班飞机、再转哪趟车,一条一条都定死了,不用到了机场再现查。省下来的就是「每一段都要现查一次」的那些时间。 但每一班飞机内部一点没变:怎么排队、谁先登机、空乘怎么调度,还是现场决定。TPU 编译器管的恰恰是这一层 —— 它连「第几分钟谁走哪条通道」都提前排好了。 📏 两边冻结的「颗粒」,差着三到四个数量级 CUDA Graph 冻结的最小单位是「一个 kernel」 —— 典型是微秒量级。  TPU 编译器冻结的最小单位是「一个时钟周期」 —— 纳秒量级。 微秒除以纳秒,中间差着三到四个数量级(这一条是我拿两个量级相除推的,不是谁给的数。)所以「都是静态的」这句话,掩盖掉的正是这三四个数量级。 ⭐ 落点:那句反驳对了一层,错了两层 —— 而错的那两层最贵 对的那一层:编排层确实被冻住了,而且收益是真的(P-32 那笔账)。  错的那两层:落地层和发射层一点都没变 而恰恰是这两层在耗硅片面积。既然它们没变,为它们服务的那批部件就一个都省不掉 —— 下一张图把这笔账算完。 ⚠️ 出处分层 三层这个切法是我定的讲法,不是谁的术语 —— 换个切法可能切成两层或四层。但每一层里「谁在决定」这件事是有出处的:块分发由硬件、warp 每周期挑、VLIW 编译期排槽。 「差三到四个数量级」是推的:kernel 的微秒量级 ÷ 时钟周期的纳秒量级。两个量级都不是精确规格,所以这里只说数量级,不给具体倍数。
图 P-33 反驳落到实处,是一个位置问题:到底哪一层被冻住了。
把通路切成三层:① 编排层(下一个 kernel 是什么)、② 落地层(块放到哪个 SM)、③ 发射层(下一拍执行哪条指令)。
⭐ 中间那一栏就是答案:CUDA Graph 只冻住了 ①,②③ 一个都没动
graph 里每个节点仍然是一次正常的 kernel 启动 —— 调度器、记分板、warp 槽、常驻上下文全都还在工作。
而 TPU 是三层全冻,且 ③ 冻到具体哪个周期、哪个发射槽。
⚠️ 三层这个切法是讲法,不是术语。
那 CUDA Graph 编译出来的到底是什么?谁在重放?(四个追问,一次答完)
① 它生成的不是代码 —— 一行新代码都没生成。 实例化之后得到的是一组工作描述符:每个节点记着调哪个已经编译好的 kernel 入口、grid 和 block 多大、共享内存多少、参数块在哪、依赖谁。 那些 kernel 还是原来那些 CUBIN,一个字节没动。 —— ⭐ 注意 NVIDIA 给这个 API 起的名字是 cudaGraphInstantiateinstantiate 不是 compile。这个用词是准确的。

② 算子融合?零,而且是做不到,不是没做。 CUDA Graph 看不见 kernel 里面 —— 对它来说每个节点就是一个不透明的 入口地址加一块参数。它连你在算什么都不知道。
跟 XLA 根本不在一个层次上:XLA 工作在 HLO 层,它看得见数学, 所以能融算子、定布局、做缓冲区分配、调度集合通信,然后吐出新的 kernel; CUDA Graph 什么都不吐。
一句话对照:XLA 是编译器,它改变「算的是什么代码」; CUDA Graph 是一张提交单,它只改变「这些代码怎么被交上去」。 两个名字里都有 graph,纯属巧合。
📌 顺带:两者是互补的 —— XLA 的 GPU 后端本身就支持把生成好的 kernel 再包进一个 CUDA Graph 里砍发射开销。先融合,再打包,不是二选一。

③ 「重放」在哪儿发生?—— ⚠️ 这里要先拆掉一个几乎人人都有的误解。
kernel 的代码本来就一直在 GPU 上,普通 launch 从来不上传代码。 CUBIN 在模块加载时就躺在显存里了,启动一万次也不动窝。
那普通启动那五到十微秒花在哪?花在 CPU 侧现场攒一个启动包 —— 打包参数、校验、写命令缓冲区、按门铃。 活儿不大,但有多少个 kernel 就要干多少遍。
所以 CUDA Graph 省掉的不是「上传代码」,是「CPU 每次重新攒那 N 份启动包」。 被搬到 GPU 上的是描述这些启动的元数据,不是 kernel 本身。
这个区分很实用:以为省的是上传,就会预期「kernel 越大收益越大」; 实际正好相反 —— kernel 越小越多,收益越大,因为省的是次数不是体积

而它省的不止这一笔,还有两笔容易漏算的
· 依赖关系改由硬件强制执行 —— NVIDIA 原话是「用硬件加速执行依赖,而不是只靠 stream 和 event」。
· 节点之间的间隔本身变小了 —— 这一笔是纯设备侧的,跟 CPU 无关。

那到底谁在走节点?—— GPU。 NVIDIA 的措辞很明确:首次启动「负责把这个图的工作描述上传到 GPU」, 而重放的定义是「启动一个已经上传到设备上的图」。
还有一个更硬的旁证:重放时的 CPU 开销跟图有多长基本无关 (NVIDIA 那篇文章的标题就叫「直线型 CUDA Graph 的常数时间启动」)。 如果是 CPU 在一个一个重放,开销必然跟节点数成正比。它是常数, 就说明走节点这件事不在 CPU 上。
📌 CUDA 12 起还能从 kernel 内部启动图,那才是字面意义上的 「GPU 里面重放」—— 但那是另一套 API,用途也不同,见下。

④ 重放之后,下面两层变了吗?—— 一点都没变。 每个图节点仍然是一次正常的 kernel 启动,带着一个 grid。 block 落到哪个 SM,是运行时由工作分发器看谁有空闲资源来分的 —— 图里没记这个,也记不了。cluster 同理。
进了 SM 更是原样:哪个 warp 这一拍发指令还是记分板说了算, 操作数到没到还是硬件在跟,这条 load 命中不命中还是运气(§3.2d 那个 30 对 659)。

⭐⭐ 根因一句话:CUDA Graph 操作的是 kernel 的「外面」。 它连 kernel 里面有什么都不知道,自然什么也管不了。
—— 所以上图那个三层的说法可以再说硬一点:不是「它恰好只冻了第一层」, 是它在架构上就够不着后面两层 提交层面的东西,管不到执行层面。

📌 附:主机侧启动和设备侧启动,谁更重? 它们解决的不是同一个问题主机侧解决「同一件事要做很多遍」(量的问题); 设备侧解决「下一步做什么,得看这一步算出了什么」(能不能的问题) —— NVIDIA 给它的定位就是动态控制流
对今天的大模型负载,压倒性是主机侧 ——  因为 step 基本是静态的(形状提前定好、路由用固定容量补齐,就是为了让图保持静态)。 ⚠️ 这条判断是从各自的设计定位和主流框架用法推的, 我没有两者的实测对比数据。要下更硬的结论得自己跑。 ③ 里那几条措辞与「常数时间启动」出自 NVIDIA 开发者博客 《Constant Time Launch for Straight-Line CUDA Graphs》。
⭐ 第 3–5 节的落点:这个反驳是前面几节最好的佐证 位置定下来之后,真正值得说的是反过来的那个方向,三条理由指向同一处:
CUDA Graph 自己也要求形状静态 —— shape 一变就得重录, 这正是 TPU 那边被诟病的同一笔静态税
TPU 压根不需要它:XLA 本来就把整个 step 编成一个程序 ——  CUDA Graph 是 GPU 在补一个自己架构带来的问题。
③ 最要紧的一条硬件成本由「必须支持的最坏情况」决定, 不由「你实际跑的情况」决定 —— 哪怕百分之百的负载都开 graph, 3.2 里那五样部件一个都省不掉,因为芯片流片的时候 并不知道你会不会开

—— GPU 想要这份性能,办法就是让自己更像 TPU; 而它能往前挪的只有最上面那一层,下面两层挪不动 —— 挪得动的话,那些晶体管早就省下来了。

5.4b 第二句反驳:那 TMA 呢 —— 搬运这条路,不是也在往 TPU 那边挪吗

这一问比 CUDA Graph 那句更值得答,因为它说的方向是对的

把 3.3b 那张四代图竖着读一遍,趋势藏不住:

四代下来,那条通路的终点长这样:整块搬、按描述符搬、 不占线程、落在一块软件说了算的专用暂存里。 —— 而这正是 TPU 第一天的样子。

⭐ 但这条趋势线只走了一半,剩下那一半才是本课的主线 收敛的是「怎么搬」:粒度(整块)、寻址的主体(专用硬件,不再是三十二条 lane)、 操作数的落点(专用暂存,不再是寄存器堆)。这三样确实在往同一个终点走。
没收敛的是「谁决定」 —— 而这一层一步都没动
· TMA 那张描述符是运行时建的,不是编译期烤进二进制的。 形状照样可以变,变了就重建一张。
· L1/L2 还在,命中不命中照样跑起来才知道 ——  §2.1 那四个决定里,有三个原样活着。
· warp 调度器照样每个周期现挑

—— 所以准确的说法不是「GPU 在变成 TPU」,是 「GPU 把喂料这件事做成了 TPU 的样子,而谁来发号令那一层一动没动」。 两边的分歧从来不在终点,在谁来发这条 DMA
⚠️ 这条趋势为什么必然发生 —— 它不是抄袭,是同一个约束 矩阵单元越快,「喂料」这件事就越不能占着算的人。 当一条矩阵指令能在几拍里吃掉几十 KB,你再让三十二个线程轮流去算地址、 去过一遍寄存器堆,那些线程就成了自己算力的看门人
两家面对的是同一道物理题,于是给出了同一个答案 ——  只不过一家走了四代,另一家出厂就在终点。 这也是全课那句话在时间轴上的样子:不是谁更聪明,是同一个决定推到底的后果。
第 6–7 节

跑完之后:拿到两个数,能不能比

3.6 那六站走完了 —— 但下面这两个数,不是那六站跑出来的。 FlashAttention 那一遍全程是结构对照与理论上界 (2.3 那张图自己就标了「真实 Flash 会重复读 K/V,强度到不了 65,536」); 而两边同口径的实测只有一组,跑的是 Hunyuan3-295B 整网训练。
—— 所以它兑现的是第 1 节那条线,不是 3.6 那次并排。 这一节先说这两个数该被当成什么,再把它们摆出来。

6.1 先把「怎么比」定下来

最常见的两种比法都不对:比峰值(那是芯片能干什么,不是你的活跑多快), 比 MFU(分母自己选,换个分母就换个结论)。 —— 剩下唯一站得住的,是图上第 ③ 档。

该怎么比 —— 一种比一种贵,也一种比一种说得上话 不花钱 一次实验 数周量级 四档比法 · 代价与结论必须一起读 只看右边那一栏会以为该直接跳到第 ④ 档。但每往下一档,代价是跳着涨的 —— 大多数场合停在 ② 就够,只是必须承认它证明的是什么。 比法 代价(对数示意) 能证明什么 / 别忘了什么 比峰值 零成本 几乎什么都证明不了。 峰值只是分母,而且口径地雷全埋在这一层:dense/sparse、哪个 SKU、per chip / per device。 比 MFU 两边各跑一次 这套软件栈把这块硬件用掉了多少。 分子分母都是选择(前两张图)。而且 MFU 高不等于跑得快 —— 见右格。 同模型同规模,两边各自调到最好 数周 + 两边的卡 最接近真话的一档。⭐ 我们做的是这一档。 贵在两边都得有人会调只调一边,比出来的是调参投入的差距。 每美元 / 每瓦 ③ + 两个新口径 采购真正要的那个答案。 价格是谈出来的,不是牌价;功耗要问清是芯片、是整机、还是含 冷却 ⚠️ MFU 高 ≠ 跑得快 第 ② 档最常见的误用。不是经验之谈 —— 一条恒等式直接推出来的。 吞吐 = MFU × 峰值 一个算例 · 槽长=峰值,填充=实际吞吐 卡 A 400 峰值 1,000 · MFU 40% 卡 B 500 峰值 2,000 · MFU 25% 卡 B 的 MFU 低了 15 个点,却快 25%。 MFU 回答的是「这套栈把手上这块硬件用满了没有」——它是效率指标,不是速度指标。 所以它适合纵向比:同一块硬件,我这周比上周调好了多少。 横向比谁更快,只能看吞吐本身—— 优先用 tokens/s 或「跑完一步几秒」,它们比 TFLOP/s 少一层公式(见上一张图的甲格)。 两个都报最省事吞吐说明谁快,MFU 说明还剩多少没榨出来。 ⭐ 第 ③ 档为什么贵:只调一边,比出来的是调参投入的差距 同一个模型、同一个规模,v7 上从 445 调到 666.6 TFLOP/s/chip —— +50%。(⚠️ 6.3 那张实测图上的 v7 是 662.3,不是这里的 666.6 ——两者不是同一次 run:666.6 是调优记录里的最好一次,662.3 是拿去跟 GB300 并排比的那一次。差 0.65%,拿去对 854.0 的一律用 662.3。)(这里面有 8.6% 是把频率档从默认锁到最高换来的,属于硬件工作点不是调优,剥掉约 +38%;起点另有一份文档记的是 457,那样算 +45.9%三个数都列出来,正是这一节在讲的事。这个幅度比很多「硬件对比」结论本身都大 —— 只调 了一边的对比,读者看到的是「哪一边的人更熟」。⚠️ 而我们 GPU 侧的调优记录只到一次 7 组配置的 sweep(492 → 527,+7.1%)—— 所以下一节那组数,我们自己也只把它挂在 ③ 档的下沿。 第 ③ 档的实测在下一节 · 代价条为对数示意,非测量值 · v7 与 GPU 侧调优幅度均取自本仓库的调优记录
图 P-14 这是 6.3 的入场券 —— 下一小节要摆两边的实测,而这张图决定了那组数该被当成什么。

⚠️ 图上那个 ④(每美元/每瓦)才是采购真正要的答案, 但它买不到捷径:④ 的分子就是 ③ —— 没有 ③ 的实测, ④ 只是把两个不确定的数相除。

「换个分母就换个结论」不是吓唬人 —— 这门课自己就栽过一次(方法论,283 字,展开看)
⭐ 「换个分母就换个结论」不是吓唬人 —— 这门课自己就栽过一次 同一次运行、同一个分子 854.0,我们先后报出过两个 MFU:

· 854.0 ÷ 2,70031.6% (2026-08-05 写进仓库,挂了 26 天)
· 854.0 ÷ 2,50034.16% (2026-08-31 更正,现在的数)

差 2.6 个百分点 —— 而这中间没有任何一次重跑,实测数一个字都没变。

错在哪:那个 2,700 是出来的,不是出来的。
写它的人拿 GB200 的 2,500 乘了个 1.2 —— 理由是「Ultra 总该快点吧」。
但官方从来没有 2,700 这个数。 GB300 NVL72 的 dense BF16 峰值与 GB200 完全相同。
Blackwell Ultra 提升的是 dense FP4(1.5×)和 attention(2×) —— 不是 BF16。

这一笔最值得带走的不是「要细心」,是它为什么难被发现
2,700 长得完全像一个官方规格 —— 量纲对、数量级对、比 GB200 大一点也符合直觉。 没有任何一处会报错。 而 31.6% 这个下游数字更隐蔽:它是老老实实算出来的,算式一点毛病没有。
—— 所以「这数是算出来的吗」这一问, 问的不是你,是你引用的那个数 这是我们八笔撤回中的一笔,完整台账(含它们朝哪边偏这个问题) 在 L300 附录 A

6.2 ⭐ 分子那一侧:同一次运行,能报出两个不同的利用率

上面那张入场券问的全是分母该拿哪个峰值。但分子也有两种取法, 而且这两种都有名字、都在被人用 ——  不知道对方报的是哪一种,两个数根本不能放一起。

区别只有一句话:分子是「模型数学上需要多少次运算」,还是 「硬件实际执行了多少次运算」。

🍳 还是那把刀:一个数的是菜谱要几刀,一个数的是你挥了几刀 分母一样 —— 这块硬件一秒最多能挥多少刀(峰值)。
MFU 的分子 = 菜谱上一共需要多少刀HFU 的分子 = 你实际挥了多少刀(包括切废了重来的、切空气凑数的)。
—— 所以 HFU 永远 ≥ MFU,差的就是白挥的那些刀

该看哪个,取决于你在问什么:
· 「这把刀有没有闲着」→ 看 HFU
· 「这顿饭多久能好、要花多少钱」→ 看 MFU (白挥的刀不会让饭早一秒好)
MFU 是一个分数 —— 上下两头都是选择,而且都不会报错 能把结果改一倍以上 改得少,但同样要交代 这一列才是这台机器该用的 甲 · 分子:我算出来的那个数 框架日志里那个 TFLOP/s 不是量出来的,是一条公式算出来的 —— 而公式有版 本。 算力公式漏了一个白名单 约 5× 算力统计函数按模型家族分叉。漏加模型名时它不报错 —— 只是用错误的宽度去量 专家、并漏掉共享专家。我们移植时中过这一枪。 重算的 FLOP 算不算 4/3 算进去叫 HFU,不算叫 MFU。全量重算下前向被算两遍,两者相差 4/3 ≈ 1.33× —— 这是定义推出来的,不是测出来的。主流框架报的都是 MFU 口径。 attention 平方项按 causal 折半吗 看序列长度 短序列上无所谓;128K 那一档它就是大头。 中 · 同一次 run,四个 MFU 实测 503 TFLOP/s/GPU,一行代码没改。下面四个格子只是换了除数。 2,250 / 4,500 HGX B200 的峰值 2,500 / 5,000 GB200 —— 实际跑的这台 ÷ BF16 峰值 22.4% ← 那份文档主报 20.1% ÷ FP8 峰值 11.2% ← 表注里又给了 10.1% 10.1% 22.4% 2.2 倍 · 而这四个格子只动了分母 甲那三个、乙那四个,一次都没动,而且能叠乘。2.2 倍是下限 乙 · 分母:我选的那个峰值 四个都有官方出处。难的不是找到一个,是说清用了哪一个。 dense 还是 sparse 官方脚注写着 sparse 是 dense 的两倍头条数字默认给 sparse。 哪一个 SKU 1.11× 见上一张图。 per chip 还是 per device 见上一张图。 拿哪一种精度的峰值 一次 FP8 的 run,用 BF16 峰值量还是 FP8 峰值量。两种都有人做,都说得通。 ↖ 中间那张表只演示了其中两条:「哪个 SKU」和「哪种精度」。 ⭐ 所以报 MFU 的时候,把这一整行一起报出来 —— 这是这一节唯一要带走的动作 MFU 20.1% = 503 TFLOP/s ÷ 2,500(GB200 · BF16 · dense · per GPU)· 分子:框架内建公式,不含重算 · 公式版本=当时那个 commit —— 一行,抄走就能用。 写不出这一行,就别报百分比。退一步报吞吐本身(tokens/s,或者「跑完一步要几秒」)—— 那个数虽然也有口径,但至少能直接感觉到快慢,而百分比不行。下一张讲的就是:什么时候该退这一步。 实测:A4X(GB200 NVL72)2 节点 8 GPU · Qwen3 类 MoE · seq 16384 · FP8 训练,实测 503 TFLOP/s/GPU · 峰值口径见上一张图
图 P-13 图 P-13 上一张刚在「这个数的单位是什么」上绊过一次。这一张是同一个问题换了一层:单位没错,除数错了。

图上那个 4/3上界(整层重算)。 本课这个算子落在哪儿 —— 当场算:FlashAttention 反向要重算一次 前向的 S,整层 7 次矩阵乘里有 1 次白做(前向 2 + 反向 4 + 重算 1): 7 ÷ 6 ≈ 1.167×有用的占 85.7%1.167 和 1.33 都是从定义推出来的,不是测出来的 ——  而它们差了一倍,正说明「开了重算」这四个字本身说明不了任何事

⭐ 上面那两个问题答完了,还有第三个,而且它最容易搞错:画屋顶线该用哪个分子 问「我会不会撞到屋顶」—— 必须用 HFU 那一侧的分子, 也就是含重算、含补零的那个数。
道理很硬:那些重算出来的运算,是真的要占矩阵单元的周期的 —— 硬件不认识哪些是无用功。

⚠️ 拿有用的 FLOP 去画屋顶线,错的方向是单向的:它只会低估。 因为有用的永远小于等于实际执行的,你会以为自己离屋脊还很远, 于是接着加重算,然后直接撞穿。
算一个给你看:一个算子里 30% 的运算是重算 —— 312.5 × 70% = 218.8有用强度才刚到 219,含重算的强度就已经顶在 312 上了。 你以为还在带宽那一侧留着一大截余量,其实已经越线。

—— 所以同一次运行,两个问题,两个分子: 判在哪一侧用 HFU 那个,报产出用 MFU 那个。 这也是第 1 节那条线的正确用法,第 2 节末尾那个折叠里有推导。
⚠️ 一个很实际的坑:比两块硬件之前,先问「重算策略一样吗」 两边如果一边全关重算、一边开着,HFU 可能很接近,而 MFU 差一大截 —— 反过来也成立。 两个数长得都像「利用率」,但它们量的不是同一件事

—— 记住这一条,6.3 末尾那张「这个倍数还缺什么」图的第 ③ 条才读得懂: 它说的正是两边的重算策略根本不是一回事。

6.3 两边的实测

入场券拿了,摆数。看这张图有个诀窍:先别看那两个大数,先看下面那两段红色。

两边的实测 —— 同一个模型、同一个规模,两边各自调到目前的水位 GB300 NVL72 · 64 GPU TPU v7 · 64 chips 浅槽=该平台的 BF16 dense 峰值,深段=实测 两段差值,长度几乎相同 甲 · 两条共轴的条:槽是峰值,填充是实测 同一根绝对刻度的横轴。下面那两段红色标注是这一格的重点,别急着看数字。 横轴:TFLOP/s(BF16 dense) 满程 = 2,500 GB300 NVL72 64 GPU 机型 a4x-max · 单 NVLink 域 16 节点 854.0 峰值 2,500 MFU 34.2% · 6,242 tok/s / 计算单元 · step 21.0 s · GBS 2048 · TP1 PP2 VPP8 EP32 · MBS 1 ⚠️ 这一侧没到顶:同一张扫点表里 EP 32→16 是 882.3 / 6,450(+3.3%),未跟进复测。所以下面所有倍数都是下限。 TPU v7 64 chips(4x4x4) 128 device · 3D torus 一个切片 662.3 峰值 2,307 MFU 28.71% · 4,770 tok/s / 计算单元 · step 20.61 s · 等效 GBS 1536 · DP1×FSDP128 · pdbs 12 · dvfs 7 这一侧撞墙了:tile、XLA flag、SparseCore 卸载、推 batch 四个方向共 8 格实验,无一正收益 实测差 191.7 = 662.3 的 29% 峰值差 193.0 = 2,307 的 8.4% 同一条基线上比长度 → ⭐ 这两段在图上几乎一样长 —— 193.0 对 191.7 TFLOP/s。但一段是 2,307 的 8.4%,另一段是 662.3 的 29% 差距要相除,不能相减。相减出来的两个数一样大,相除出来的差 3.4 倍 —— 而下一格那个 1.084 × 1.190 之所以是乘法,理由就在这里。 乙 · 把这 1.29 倍拆开 —— 只拆得动一半 854.0 ÷ 662.3 = 1.29 = 1.084 × 1.190 而按每芯片 token/s:6,242 ÷ 4,770 = 1.31 —— 下文一律用这个 1.084 1.190 ↑ 段长按对数切 —— 两项是相乘的,按 0.084 : 0.190 切就是把乘法当成了加法 ① 峰值之比 —— 能算清的那段 2,500 ÷ 2,307 官方 dense BF16,两边都按 P-12 的口径取。这一段不用调,买回来就是它。 ② 峰值以外 —— 余项,不等于「软件」 34.160% ÷ 28.708% 这不是量出来的,是除剩下的。软件在里面,非峰值的硬件也在里面:HBM 带宽、片上 SRAM、MXU 收缩维形状、互联拓扑。 ⚠️ 这个式子恒成立,所以它不是一个发现。因为 MFU ≡ 实测 ÷ 峰值,任意两个数都能这 么拆。 它证明的只有一件事:这 1.19 倍不来自峰值差。至于它有多少来自软件 —— 这张图答不了, 下一张才答。 ⚠️ 换一个单位,倍数就变 —— 而变掉的那 1.5% 正好是第 6 节讲的那件事 按 TFLOP/s 算,GB300 是 v7 的 1.29 倍;按 tok/s 算,是 1.31 倍(6,242 ÷ 4,770)。两个数都对,差在分子那条公式 ——同一个模型、同一条 token,两边的分析式 FLOP 公式给出的答案差 1.5%差多少不重要,差本身才重要。 token 是能数出来的,TFLOP/s 里还夹着一条公式。所以两个都报的时候,把 tok/s 放在前面。这不是吹毛求疵:1.5% 在这里无关紧要,但它证明了那条公式确实是两边各写各的 ——下一次遇到的不一定还是 1.5%,而 P-13 甲格里那个漏白名单的例子是 5 倍。 同一个 Hunyuan3-295B-A21B(80 层 · 192 专家)· seq 4096 · 合成数据 · 两边都不落盘 checkpoint(但重算策略不同,见 P-18 第 ③ 条)· 两边都是我们自己跑的
图 P-15 图 P-15 全课唯一一张两边真数并排的图。下面那两段红色一样长,而两个大数看上去差很多 —— 差的是分母不是本事。乙格右下那个红框是这张图的结论,一个字都不能改。

最难的一步在后面:不把这个数说过头。 这个 1.31 倍还缺什么?—— 先自己列一遍,再看下面这张对答案。
⚠️ 先对一下口径:上图的 1.29 是每芯片 TFLOP/s(854.0 ÷ 662.3),下文一律用每芯片 token/s 的 1.31(6,242 ÷ 4,770)—— 差的就是图里那 1.5%。

不能拿它说的话 —— 六条,每条都标出它往哪边推 这一条把倍数推高(利于 GB300)· 共 4 条 这一条把倍数压低(利于 TPU v7)· 共 2 条 底带那条没有方向 —— 它是适用范围,不是偏置 global batch 不一样 → 利于 GB300 问① GB300 每步 2,048 条序列,v7 等效 1,536问② 更悬殊 —— 4,0961,408,差 2. 9 倍大 batch 摊薄固定开销。但 v7 的 batch 是被 94.74 GiB 顶住的,不是被让的 — — 这一条本身就是硬件差异。 64 这个规模是 GPU 的主场 → 利于 GB300 GB300 的 64 张卡全在一个 NVL72 域内(域内没有「距离」),v7 是 4x4x4 环面(对角 要绕好几跳)。而这个模型吃的正是 all-to-all —— 第 4 节论证过它是唯一在两种拓扑上 表现分岔的原语,而 TPU 的账要过了 72 颗才开始好看。 重算策略不是同一件事 → 利于 GB300 两边图脚都写「不落盘 checkpoint」,但那说的是不写盘,不是不重算GB300 侧重算全关 (吃到 230 / 288 GB);v7 侧 remat_policy=custom + offload(91.94 / 94.74 GiB)。关重算本身就值 +5.2%。 两边软件成熟度差很多 → 利于 GB300 · 且会变 这一条是整节结论最重要的边界,所以必须在清单里而不只在 P-16 里。如果差距和软件有关, 那么软件会变 —— 这组数是 2026 年 8 月那两套栈的状态,不是硬件属性。换个季度重测, 第一个动的就是它。 v7 那 662.3 含一个频率档 → 利于 v7 dvfs_p_state=7 把频率锁到最高,单独值 +8.6%第 6 节的口径认为它属于硬件工作点, 不算调优GB300 侧没动过等价旋钮。这一条对我们自己不利,是我们自己的数。 「每芯片对每 GPU」已经是最优待 → 利于 v7 v7 一颗 chip 是 2 个 device,GB300 一张卡是 1 个。按 device 比就是「1 颗 v7 die 对 1 整张 GB300」,那样 v7 更吃亏 —— 本课选的这个口径,已经是 v7 能拿到的最 好的一个。见 P-12 乙格。 ⚠️ 还有第七条,它没有方向 —— 只有一个模型、一个规模。而第 7 节说过「三张图之后你会发现它是硬约束」,所以这里得演示它会怎么动 把规模从 64 推到 72 以上,这个倍数会往哪边动,第 4 节已经答过:GB300 一侧要跨出 NVLink 域,all-to-all 从「域内无距离」变成「过交换机、过网卡」;v7 的环面则继续线性延伸,没有这个台阶。这一节测的 64,正好停在那个分岔点之前。 所以「只有一个规模」不是客气话:它是这组数唯一一条我们能预判方向、却没有数据的边界 —— 而预判的方向恰好和上面那六条相反。要推翻这一节最省事的办法,不是质疑这 64 个单元上的任何一个数字,是去测 128 个。 这一张是 P-17 的续页 —— P-17 问「这个倍数还缺什么」,这一张是答案
图 P-18 图 P-18 上一张那道题的答案:四条红、两条绿。1.31 是一个点,不是一个界 ——两个方向相反的修正,都没量过。
⚠️ 这一节真正的产出不是那个倍数 四条红两条绿里,红绿是反向的 —— 也就是说 这个数往哪边挪都有理由,而我们两个方向都没量过。 一份负责任的材料到这一步该做的不是把数字说圆,是把没量的部分列出来。
—— 所以这一节能带走的是这四句问法这个数是算出来的吗、二进制还是十进制、per chip / per device / 还是 per core、 物理量还是可用量。换成别的两块硬件,这四句照样用。 (收尾那张图会把它们再列一遍。) 调优是怎么从 457 走到 662.3 的、以及我核数时漏过哪些, 在 L300 §7 / §8
第 9 节

那到底各自擅长什么

走完全程,最后回答开课时那个没答的问题 ——  而答案只有一句,剩下的都是它的限定。

9.1 落点

从封装拆到实测,两块硬件走完了一整趟。开场说「处处都不同,但只有一个成因」, 3.6 把全程摊开数过,六行里四行两边一样 ——  现在可以说那个成因到底是什么了。

把控制权交给谁 —— 这门课看到的所有差别,都是同一个决定的后果 全课收尾 · 三 GPU 那一侧:交给硬件在运行时处理,也就是交给人 TPU 那一侧:交给编译器在编译期算准,也就是接受它的脾气 右格四句:换成别的两块硬件照样用 ⚠️ 红/绿只标是哪一侧,不含好坏 —— 这门课唯一的两边实测,赢的恰好是红那侧 整门课只有一个决定要问 运行时才知道的那些事(形状、访问落在哪、谁忙谁闲),交给谁处理? 交给硬件,在运行时处理 —— 也就是交给了人 交给编译器,在编译期算准 —— 也就是接受它的脾气 ① 同一次访存,要在路上做几个决定? §2.1 · §3.6 主路 4 个运行时决策点 —— 命中不中要跑起来才知道 0 个 —— 全在编译期定死,走哪条路是算出来的 ② 所以,代价出现在什么时候? §5.5 · P-8 每一次访存都付一点,藏在运行时里,看不见也不用管 一次付清,但付在你身上 —— 冷启动 19 分编译,热启动仍 7 分半,其中 16 次失败 重编每次都白付;而且要有人会写 Pallas ③ 所以,前提不成立时怎么办? §2.1 落点 运行时还能换一条路 —— 硬件自己会绕 运行时无处补救 —— 只能回去改代码,重来一遍编译 带出门的四句 跟这两块硬件无关 —— 换成别的两块照样用。这四句是这门课真正能 迁移的部分。 1. 这个数是算出来的吗? 是的话,算式写在它旁边了吗 P-19 2. 二进制还是十进制? 官方 HBM 那栏写的是 GiB(二进制)192 GiB = 206 GB,差 7.4% —— 别把 192 GiB 当 192 GB 往上乘 P-21 乙① 3. per chip、per device、还是 per core? v7 是 2 device / chip,日志按 device 报。这一步错, 结论差一倍 P-21 乙② 4. 物理量还是可用量? 94.74 GiB 是可用的;2,500 是 dense 不含稀疏 P-21 乙③ ⭐ 两边的差别不是「谁更强」,而是「把控制权交给谁」 交给人,就得有人有能力写;交给编译器,就得接受它的脾气。 ⚠️ 这句话必须跟下面三条一起带走 —— 只带上面那句出门,它就只是一个立场 ① 唯一的两边实测:GB300 快 1.31 倍 它是一个点,不是一个界。两个方向相反都没量过的修正:EP16(882.3)没复测 → 可能 1.35;六条边界四条利于 GB300 → 可能更小。P-18 点名最重的是「软件成熟度」 ② 手写 kernel 是生态差距,不是架构差距 照抄 P-3 的句式,因为它把话说全了:占优 + 类别 + 时效两者会随时间收敛,别混成 一件事讲 —— 架构差距不会自己变小,生态差距会。 ③ TPU 那个前提,这门课从没用数据兑现过 我们自己欠的 §0 立的是一个前提(「早就知道你要跑什么」);第 5 节兑现的只是它的反面。正面那一 半全课一次都没兑现过。把它当已证结论回收,就是「用命名冒充证明」的同族错误 落点那句话本身不值钱 —— 值钱的是它下面那三条限定,那才是这门课挣来的
图 P-24 中间那个提问框是全课的骨架,右格四句是唯一能带出门、换成别的两块硬件照样用的东西。底带那三条限定,正文下面逐条展开。
⭐ 只带一样东西回去的话,带「算子融合」。 上面那张图讲的是原则;融合是这门课里唯一一个从头贯到尾的具体实例, 而它恰好把那条原则的三层都走了一遍 —— 
为什么要融:不改数学就能往屋脊线右边挪,只有这一招(§1 · §3.6)
能融多大:工作台多大 + 刀多宽,硬件两个数一起卡死(§3.2b · §3.4)
谁动手融:不改数学的编译器替你做,改数学的永远归人(§5.3)

物理、硬件、控制权,三层各卡一道 ——  这就是为什么研究硬件,最后总会落到融合上。

讲完了。它没有告诉你该买哪个 —— 它想让你下次看到一份 「A 比 B 快 N 倍」的材料时,知道该问哪四句话,以及翻到第几页去找它的分母和分子—— 6.2 那两个都叫「利用率」的数,分母一样,差的全在分子。

这一版是 L200 · 精讲版(主线 109 分钟)—— 完整版 L300 多讲了什么(想往下挖再点开)
这一版是 L200 · 精讲版(主线 109 分钟) 完整版 L300(两小时,72 张图)多出来的是四组深挖:
·芯片封装全景与十行完整对照
·寄存器堆到位线的五张图
·四种通信原语与域内结构
·推理引擎移植账单与 AOT
以及「这一课的数我是怎么核的」整节自曝 ——  八笔撤回的台账、同一个漏法犯两次、二十条这门课答不了的。
—— 去看 L300 完整版 →
两版对照着翻时:哪些节号一致、哪些不一致(只在对照 L300 时需要)
§0–§5 与 §9 两版大节号一致,可以按大节对照着翻。
⚠️ 两处不一致:L300 的 §6+§7 在本版合成了第 6 节 (实测在本版 6.3),§8 整节没收
另外图里出现的小节号(§1.4、§2.6、§4.1、§5.5、§7 这一类)都是 L300 的编号 —— 那些图是从 L300 原样搬过来的,图上的 §7 讲实测时读作本版 6.3。 看到小节号对不上,去 L300 里翻。