第 3 节
拆开看:同一份复杂度,两边放在了不同的地方
第 2 节数的是决策点 ,这一节数的是粒度 。
同样一块矩阵乘的活,两边一条指令吃下的块大小差 128 倍 ——
这个数字一旦记住,后面「为什么这个优化在那边不管用」大半都能自己推出来。
旁白:下面这批图的两条读法约定(编号与出处,看不懂图时再回来)
① 图上的「§N」是《显微镜》里的节号,本课自己的节一律写「第 N 节 」。
② 右下角没有出处的十四张是本课自己画的,不是漏了 。
3.1 先看整颗,再谈内部
两颗芯片都是双 die 封装 ,这是先讲全景的理由 ——
如果直接跳进核内部,会漏掉一个只在封装层面才看得见的差别。先看 B200:
一颗 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 一颗 B200 的全景。 两颗 die 用 NV-HBI 缝成「一个 GPU」,对软件完全透明 —— 代价是那道缝还在(L2 跨 die 从约 21 掉到 16.8 TB/s),而这件事不出现在任何 API 里 。 —— 出自《GPU 显微镜 》
再看 v7。物理构造几乎一样,对软件的呈现完全相反。
一颗 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 链路快 6× 。但请注意它没有 把两个 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 一颗 TPU v7 的全景,同样两颗 die。 但它如实暴露成两个 device —— 缝在明处。这两张图并排看,是第 2 节那个决定在封装这一层的样子 :一边把复杂度藏起来,一边把它交给你。 —— 出自《TPU 显微镜 》
这一对图是全课的缩影,值得在这儿多停一分钟
图上已经把「一边缝在暗处、一边缝在明处」说清楚了。要补的是它意味着什么 :
藏得好的时候你省事,藏漏了的时候你连查都不知道从哪查起。
B200 那道缝不出现在任何 API 里,所以它只会表现为「这个 kernel 莫名其妙慢了」;
v7 那道缝写在脸上,麻烦但可查。
后面每一层都会再遇到这个对子一次 ——
核内部、层级表、访存路径,全是同一个选择在不同尺度上的复现。
整门课如果只留一件事,就是这一件。
3.2 把一个核拆开
往里一层。不要从参数表开始,从「有什么」和「没有什么」开始。
先看 GPU 这边留下了哪些部件 ——
一个 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 一个 SM 拆开。 warp 调度器、256 KB 寄存器堆、228 KB L1/共享内存、记分板 ——这些部件加起来只干一件事:在运行时动态地藏住延迟和分支。 —— 出自《GPU 显微镜 》
再看 TPU 这边 —— 这张图的信息量全在右边那一栏。
把一个 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 一个 TensorCore 拆开,重点在右边那一栏。 上面那五样在这里一样都没有 。而它们不是五个独立的取舍,是同一个决定的五个后果:一旦你决定不在运行时换任务 ,这五样同时失去存在理由。 —— 出自《TPU 显微镜 》
接回第 2 节:这不是「砍配置」,是同一个决定的连锁反应
右边那五样看着像五个独立的取舍,其实是一个:要不要在运行时换任务跑 。
答「要」,就必须同时驻留几十份上下文、必须有东西挑下一个、必须知道谁的数据到了 ——
五样缺一不可。答「不要」,五样同时 失去存在理由。
而第 2 节主路那 4 个红框,就是答「要」之后的必然产物。
「有没有 cache」和「有没有 warp 调度器」不是两件事,是同一个决定的两个面。
还有第 1 节欠下的那笔账 —— 图里那个「跨 lane 单元」。
先把画面立起来:一个向量寄存器是 8 个 sublane × 128 条 lane,
像一张 8 行 128 列的座位表;每条 lane 是一整套自己的 ALU,列不是画出来的。
于是同一批数,上下走和横着走根本不是一回事 ——
同一张 8 × 128 的座位表 —— 上下走三步就完,横着走要另找一个单元
纵向 · 沿 sublane 轴(8)
横向 · 跨 lane(128)
神经网络最常见的两个归约,恰好都在横向
一个向量寄存器就是这么一张表:8 行(sublane)× 128 列(lane)
128 条 lane →
0
1
2
3
4
5
6
7
8
9
10
11
12
⋯
126
127
8 个
sublane
0
1
2
3
4
5
6
7
ALU
每一列底下是一整套自己的 ALU(v4 论文:每 lane 16 个)
⭐ 列不是画出来的,是真的硬件。 同一列上下那 8 个数,落在同一套硬件 里;隔壁那一列,是另一套硬件 。
绿色这一列
沿它上下 求和 —— 数就在自己这套硬件里,不用出门。
红色这一行
沿它横着 求和 —— 要跨 128 套互不相通的硬件。
于是「上下」和「左右」根本不是一回事。 下面两格分别走一遍。
纵向 · 沿 sublane 轴(8 个)
硬件自带一条 shuffle 能沿这个轴滚一格,公开资料说约一个周期
原始
错 4 位加一次
错 2 位加一次
错 1 位加一次
= Σ
三步
8 → 4 → 2 → 1,三步,全程在寄存器里就地完成 —— 数一次都没有离开自己那套硬件。这就是「便宜」的全部含义。
横向 · 跨 lane(128 条)
没有这条 shuffle —— 数据要横着走,只能交出去
…… 128 条
XLU 跨 lane 单元
转置 · 跨 lane 归约 · shuffle
VMEM
往往还要绕这一趟 —— 不是在寄存器里就地完成
公开资料对它的形容:「慢,而且贵」
一个标量核管着:一个 VPU(几千个 ALU )· 多个 MXU · 2 个 XLU · 多个 DMA 引擎
需求侧几千,供给侧个位数 —— 这就是那间传达室有多窄
「2 个」出自公开资料,但那句话没标代次 —— 只当「不止一个」用
⭐ 麻烦的是:神经网络最常做的那两个归约,方向恰好都是横的
张量的最后一维默认铺在 lane 方向 上。而 softmax 沿最后一维求和,RMSNorm 也沿最后一维求和 —— 两个最常见的算子,都撞在上面那条最贵的路上。
这就是为什么在 TPU 上,attention 里 softmax 那一段经常比你按 FLOP 算出来的贵得多 :贵的不是算,是把数横着挪。
⭐ 那为什么 8 那个方向便宜、128 那个方向就得专门做个单元?—— 差的是一个平方
要让一组数任意互换位置 ,需要的开关数按 N² 涨。sublane 方向 8 见方 = 64 ;lane 方向 128 见方 = 16,384 —— 差 256 倍。
所以 8 那个网络小到可以摊进每一条 lane 里 ,一个周期就转完;128 那个只能全核共享一两个 ,还得排队。⚠️ N² 是数字设计通则,不是 TPU 的公开规格 —— 这里用它解释数量级。
⚠️ 一个说法要收回:贵不是因为「没有连线」
这张图早先的版本写着「列与列之间没有直连通路」——那是想当然,已经删掉。公开资料的说法是:跨 lane 至少要过一趟 VMEM/XLU/SMEM ——贵在「出一趟寄存器堆」这个往返,不在缺一根线。
下一张图把这个往返画出来。XLU 的内部电路仍然没有出处 ——公开资料只说它是个独立单元、而且慢,没说它里面长什么样。
图 P-26 整张图就一件事:同一个寄存器、同一批数,换个方向,代价完全不同 。绿的那一列是纵向 —— 8 → 4 → 2 → 1,三步,全程没离开自己那套 ALU;红的那一行是横向 —— 128 条 lane 各是一套硬件,数要横着走只能交给 XLU ,而且往往还得绕一趟 VMEM。黄带才是这张图真正要落的地方 :softmax 和 RMSNorm 都沿最后一维求和,而最后一维默认就铺在 lane 方向上 —— 最常做的那两个归约,恰好都撞在最贵的方向 。⚠️ 底带那条不能跳过,它记的是一次收回 :「贵是因为列与列之间没有连线」——这个说法是错的,已经撤掉 。公开资料的说法是跨 lane 至少要过一趟 VMEM/XLU/SMEM ,所以贵在「出一趟寄存器堆再回来」这个往返,不在缺一根线 。(这一句是后面三张图共同的地基,别看漏。)
讲到这儿一定会有人问:这些部件之间到底怎么连?
答案跟「各自一套寄存器」和「共用一条总线」都不一样
—— 而它顺带补正了上一张欠的那句:横着走贵,贵在哪一趟。
核里只有一套 vreg —— VPU、MXU、XLU 都从这里取,各开一个门
vreg 堆(唯一的一份)
VMEM 进出
VPU
MXU
XLU
VMEM
片上便签本
所有数据进核的第一站。vreg 只
能从这里装填 ,算完也只能写回
这里。
→
每周期 装 3 个 vreg
←
每周期 写回 1 个
(v5p,公开资料)
vreg 堆 · 全核唯一一份
…… 共 64 个
一个 vreg = 8 × 128
1024 个 32 位数 = 4 KiB
64 × 8 × 128 × 4 B = 256 KiB ,这就是一个核全部的
向量寄存器。
没有「VPU 专用」和「MXU 专用」之分 —— 六十四个是一
个池子,编译器随便挑。
(v5p 是 64 个;v4 只有 32 个)
VPU · 逐元素
每个 (lane, sublane) 位置上有 4 个互相独立的 ALU —— 8 × 128 × 4 = 4096 个 ,书里那个数就是这么来的。
一条指令吃的是两个完整 vreg ,写出第三个。每周期发 4 条 ,而且 4 条可以各干各的 (vadd、vsub 同时跑)。
MXU · 矩阵乘
书里这句是原话:vreg「hold data for the VPU and MXU 」—— 同一批寄存器,两个用户 。喂进去的 LHS 正好是 8 × 128。
XLU · 跨 lane
横着走唯一的门。书里说要跨 lane,至少得过一趟 VMEM/XLU/SMEM —— 出了寄存器堆再回来 。
⭐ 于是「横着走为什么贵」有了一个更准的说法
不是「列与列之间没有连线」 —— 那是想当然。真正的差别是要不要出这个盒子 :纵向那三步 shuffle 在 ALU 里就地完成,数没离开 vreg;横向要跨 lane,就得把整个 vreg 交出去、绕一趟、再收回来 。
贵的是这趟往返,不是那一次加法。
⭐ 换算成 GPU 的词:书里给了一组对照
VPU 里的一个 ALU ≈ 一个 CUDA core ;VPU 的一条 lane ≈ 一个 warp scheduler (就是那组通常 32 个 CUDA core 一起做 SIMD 的单位)。两边差的不是「有没有这些东西」,是谁在运行时挑活 —— 这正是 3.2 开头那张图右边一栏说的事。
⚠️ 这张图讲的是结构,不是 v7 的规格表
上面每个数(64 个 vreg、每位置 4 个 ALU、每周期 4 条指令、VMEM 每周期 3 读 1 写)都出自公开资料,而书里标的是 v5p —— 它自己就说了 v4 只有 32 个 vreg,所以这些数会跨代变 。v7 没有同口径的公开数,别把这张图当 v7 的规格表用。
图 P-27 上一张图留了个问题:这些部件到底怎么连。答案是中间那个盒子只有一个 —— VPU、MXU、XLU 取的是同一批 vreg ,各开一个门 —— 不是各有各的寄存器 (「那到底怎么连的」这个问题下一张图专门答)。于是「横着走贵」有了更准的说法:贵在要把整个 vreg 交出去、绕一趟、再收回来 ,不是缺一根线。蓝带那组换算是这门课最该记的一句 :一个 ALU ≈ 一个 CUDA core,一条 lane ≈ 一个 warp scheduler —— 两边差的从来不是有没有这些部件,是谁在运行时挑活 。⚠️ 图里的数书里标的是 v5p (v4 只有 32 个 vreg),别当 v7 规格表用。
上一张开完门,下一个问题就是门后面的线。直觉的那个答案只对一半
—— 要紧的恰恰是错的那半。
数据到底怎么搬 —— 128 是每条通路的宽度,不是一条总线的车道数
① 常见的错模型:共享总线
② 实际:端口
lane 内
跨 lane
攒够才算
❌ 很自然、但不对的那张图:一条总线,大家挂上面
「128 条车道」
vreg 0
vreg 1
…共 64
VPU
MXU
这张图会推出两个结论:同一周期只能有一个人说话 ,而且芯片上得有个仲裁器在现场协调谁开谁关 。
两个结论都不成立。 寄存器堆从来不是这么接的 —— 它开的是端口 ,不是挂总线。
这张图之所以看着对,是因为「128」这个数确实到处都在。但它在每一处都是「宽度」,不是「份额」 —— 没有谁
需要从这 128 里分一块走。
✅ 实际:一个寄存器堆,开出有限条端口
vreg 堆
64 个
读口
→ VPU
读口
→ VPU
写口
← 结果回填
装填口
← 便签本
每一条端口本身就是 128 lane 宽 —— 128 不是要分给大家用的车道,它是每条通路的宽度 。真正有限的是端口有几条 。(端口具体是什么
线、为什么加不起 —— P-30 拆开画。)
🔢 那到底有几条?公开资料没直说 —— 但给了能推的两个数
① 每周期发 4 条 向量指令,每条吃 2 个 vreg、写 1 个 → 读 8、写 4。 ② 每周期从便签本装 3 个 vreg、写回 1 个 → 再加写 3、读 1。 合计约 9 读、7 写。
⚠️ 这是推的,不是原话 —— 两个乘数都出自公开资料,加法是我做的。而且真实硬件常靠分体 省端口,具体怎么分公开资料没说。
VPU · 逐元素
就地
横向?
不需要,lane 内自己算自己
延迟
约 2 周期
队列
不用
最短的一条路:从 vreg 出来,进本 lane 的 ALU,回
vreg。全程没离开那一竖条。逐元素算子便宜,真正的原因
就是它一步都没有出去。
超越函数单元
竖着,但深
横向?
不需要,也是 lane 内
延迟
几十周期 ,流水线深
队列
要 ← 因为慢
exp、rsqrt 这类。它跟 VPU 一样是竖着接的,但不能占着
发射位干等 :推进去,先干别的,好了再捞。
XLU · 跨 lane
横着,且共享
横向?
要 ,这是唯一横着走的门
延迟
几十周期 + 排队
队列
要 ← 因为共享
全核只有一两个,谁都得从这儿过。贵的是「出一趟再回来」
这个往返,不是那一次加法。
MXU · 矩阵乘
喂不满,要攒
横向?
阵列自带,不占 XLU
延迟
攒够了才乘一次
队列
要 ← 进料和出料都要
一次运算是 8×128 乘 128×128 。左边那个正好是一个 vreg,
可权重那一侧有 128 行,一个 vreg 只给得出 8 行 —— 差
16 倍 ,只能分好多趟送。
⭐ 那「撞车」呢?—— 它不存在,因为它在编译期就被消灭了
TPU 是 VLIW :一个指令包里,每类操作有几个固定的位置 ,编译器往里填。填不下就多排一个周期。芯片上没有一个运行时的仲裁器在现场商量谁先走 —— 冲突不是被解决的,是被提前排掉、根本没机会发生 。
GPU 正相反:靠运行时的记分板和 warp 调度器现场决定。 这就是这门课一直在回的那条分水岭 —— 谁在运行时挑活。
图 P-28 上一张图说「各开一个门」,紧接着最自然的追问是:那不就是 128 条车道大家串在一起、时分复用吗?一半对,一半错,而错的那一半最要紧 :争用确实有,但争的不是一条共享总线,是端口和发射位 —— 128 是每条通路的宽度,不是要分给大家的份额 。四张卡把四个单元的接法摊开对比,机关在「要不要队列」那一栏:三个都要,理由两两不同 (超越函数是因为慢,XLU 是因为共享,MXU 是因为一口喂不满)。落点那条最值得记:撞车不是被解决的,是被编译期提前排掉、根本没机会发生 —— GPU 靠运行时记分板现场决定,这正是全课那条分水岭。⚠️ 端口那个「约 9 读 7 写」是推的不是原话 ,图上写了推导链。
还有一层没讲:真正光刻在硅片上的走线。
顺着上一张图最较真的那个追问往下走,正好能把前面所有说法收口 。
版图上它是碎的 —— 一个 vreg 不用扯 32,768 根线出去,因为那些线从来不汇合
vreg 存储(切片)
本 lane 的 ALU
256 根短线,几微米
真正要扯出去的只有这两条
🔢 先纠一个单位:一根走线传的是一个 bit,不是一个数
一个 vreg = 8 sublane × 128 lane × 32 bit = 32,768 bit → 要一次整个读出来,就是 32,768 根线 ,不是 128 根。
「那也太荒唐了」—— 对,所以它不是一整块。 下面这张图就是它真实的样子。
…64 行
ALU
×32
lane 0
…64 行
ALU
×32
lane 1
…64 行
ALU
×32
lane 2
…64 行
ALU
×32
lane 3
…64 行
ALU
×32
lane 4
…64 行
ALU
×32
lane 5
…64 行
ALU
×32
lane 6
…64 行
ALU
×32
lane 7
…64 行
ALU
×32
lane 8
…
…64 行
ALU
×32
lane 127
每条 lane 底下 8 × 4 = 32 个 ALU → 128 × 32 = 4,096,这就是公开资料那个数
同一个 vreg 的 128 个切片 —— 它们并排,但彼此不相连
256 根
每一条切片只要 8 × 32 = 256 根线 ,而且存储就贴在 自己那条 lane 的 ALU 上 —— 走几微米。
128 份加起来确实是 32,768 根,但它们从来不汇合 。所以「128 宽」是 128 份并行的硬件 ,不是一条粗 128
倍的线。
「顶多一百多根」这个直觉是对的 —— 那是每条 lane 的局部数,被当成了全局数。
① vreg ↔ 其他 63 个 vreg
完全不连
它们之间没有任何直接通路。 要把 vreg 3 挪到 vreg 7,唯一的办法是读出来、过
一遍 ALU、再写回去 。
它们共用位线 (就是把数据送出去的那组竖线,下一张图拆开画 ):一条切片里 64
行共用一组线,靠地址 挑一行。但这不是轮转排队 —— 你要哪一行就是哪一行,当周
期就到 ,不存在「等 64 个周期才轮到我」。共用的代价是一条端口一个周期只能送
一行 :想同时送 8 行,就得有 8 条端口 —— 真正的预算是端口数,不是 64。
② vreg ↔ VPU
贴着,几微米
根本不用「扯出去」。vreg 的 lane 5 和 ALU 的 lane 5 在芯片上是挨着的 ,
中间就是那 256 根短线。
这就是逐元素算子便宜的物理原因 :它走的是全芯片最短的一条路,而且 128 条同
时在走。
③ vreg ↔ MXU / XLU
真的要扯出去
MXU 是个独立的块 :一次运算吃 8×128 乘 128×128 。左边正好一个 vreg,可权重
那侧有 128 行,一个 vreg 只给 8 行 —— 差 16 倍 ,喂不满;XLU 更是必须同
时够到全部 128 条 lane 。
所以只有这两条是真正的长路。 它们因此各自配了进料口、结果队列,而且全核只有
一两个 —— 长而宽的路,芯片不会多铺。
⚠️ 「按 lane 切片」这件事是推出来的,不是查到的版图
推导链有两条。一 :4,096 个 ALU 不可能靠一个集中的寄存器堆去喂,布线在物理上过不去。二 :如果寄存器堆是集中的,那个交叉开关本来就在那儿了,跨 lane 就不该特别贵 —— 而它特别贵,还专门做了一个独立单元。「XLU 存在」这件事本身,就是版图按 lane
切的证据。
没查到的:具体的 bank 怎么划、位线怎么排、真实 floorplan —— 公开资料没有。 上面讲的是数量级和拓扑 ,不是版图图纸。
图 P-29 P-28 说「每条端口本身就是 128 宽」,最较真的追问紧接着就来:一个 vreg 才 4 KB,它怎么可能扯出去那么多根线? 这个质疑完全正确 —— 而且答案不是「线真有那么多」,是「它们根本不需要汇合」 。先纠单位:一根走线传一个 bit ,所以一个 vreg 是 8 × 128 × 32 = 32,768 根 ,不是 128 根。荒唐 → 所以它不是一整块:整条数据通路按 lane 切成 128 份,存储就贴在自己那条 lane 的 ALU 旁边 ,每份只要 256 根线、走几微米 。于是三个问题一次答完:64 个 vreg 之间完全不连 (只共用位线,而且是按地址选通、当周期就到 ,不是轮转排队 —— 共用的代价是「一条端口一周期只送一行」,不是「等 64 个周期」);到 VPU 是贴着的 ;只有 MXU 和 XLU 才真的要扯长线 —— 这也正是它们要队列、要共享的原因。⚠️ 红带那条必须念:「按 lane 切片」是推的,不是查到的版图 ,具体 bank 划分与 floorplan 公开资料没有。
「位线」用了好几次却一直没画。不画就会误解成「大家轮流用一条总线」,
而那个比喻会推出一个跟公开资料对不上的结论 ——
对不上的是比喻,不是硬件。
位线是一张田字格 —— 横的选行,竖的送数据,一个端口就是一整套线
字线:横着,一行一根,选中谁
位线:竖着,一位一根,送数据
存储单元
被选中的那一行
一条 lane 切片里的寄存器堆:64 行 × 256 位的格子
位线 · 竖着走,一位一根 → 共 256 根
vreg 0
vreg 1
vreg 2
vreg 3
vreg 4
vreg 5
vreg 6
vreg 63
← 选中
…… 中间省略,实际 64 行 × 256 列
地址译码器
6 位地址
→ 选 1 行
读出电路 → 这一行的 256 bit 一起出去
横的叫字线,一行一根,管「选中谁」。竖的叫位线,一位一根,管「把数据送出去」。
给一个 6 位地址,译码器点亮一根字线 ,那一行的 256 个单元同时接上位线 —— 当周期就到,没有排队这回事。
所以「64 个共用位线」的真面目是「按地址 64 挑 1」 ,不是「轮流用 64 个周期」 。
那共用到底代价在哪? 就在这张图上:这一整套线,一个周期只能送一行出去 。想同时送 8 行 —— 只能再铺 7 套 。
这就是下一格要说的「端口」。
那「端口」到底是什么?—— 一整套线,不是一个比喻
① 地址译码器
把 6 位地址变成「点亮第几根字线」
② 一组字线
64 根,横着穿过所有单元
③ 一组位线
256 根,竖着穿过所有单元
④ 读出/写入电路
把位线上的电平变成能用的数
凑齐这四件,才算一个端口。
放大看同一个存储单元
字线1
字线2
字线3
位线1
位线2
位线3
1 bit
多端口 = 在同一批存储单元上,并排铺好几套线。
同一个单元被好几根字线 横着穿过、被好几根位线 竖着穿过。于是它可以同
时被不同的端口读到 —— 这就是「一个周期读 8 个 vreg」的物理实现。
⭐ 那「多开几个端口不就行了」?—— 又一个平方,而且这次是面积
每加一个端口,每个单元横向多一根字线、纵向多一根位线 —— 单元在两个方向上同时变大,面积按端口数的平方涨 。端口翻倍,寄存器堆的面积大约变成四倍。
所以端口是全芯片最稀缺的预算之一,不是想加就加。 这也把 P-28 那个「约 9 读 7 写」的口径钉住了 :真要铺 16 套线,面积会离谱到不可能 —— 那是「同时访问数的上限」,不是「真的铺了 16 套」。
真实设计一定会分体 :把 64 行拆成几组,各组自己一套端口,同时访问落在不同组里就不用真的加线。⚠️ 平方关系与分体都是数字设计通则;TPU 具体用什么单元、几个端口、怎么分组,公开资料没有。
✅ 对账:这张格子图能不能跟公开数字对上?
一条 lane 切片 64 行 × 256 位 = 16,384 个单元 ;全核 × 128 条 lane = 2,097,152 个单元 ;换成字节 ÷ 8 = 262,144 B = 256 KiB 。
而公开资料写的是「每核约 256 kB 的 vreg」—— 正好对上。 一个孤立的数不能信,但两条独立路径撞出同一个数,整套模型就立住了。
图 P-30 「位线」这个词前面用了好几次,但一直没画。它其实就是一张田字格 :横着的是字线,一行一根,管「选中谁」;竖着的是位线,一位一根,管「把数据送出去」 。给一个 6 位地址,译码器点亮一根字线,那一行的 256 个单元同时接上位线 —— 当周期就到,没有排队这回事 。所以「64 个共用位线」的真面目是按地址 64 挑 1 ,不是轮流用 64 个周期 。这张图顺带把「端口」落到实处:一个端口 = 译码器 + 一组字线 + 一组位线 + 读出电路 ,多端口就是在同一批单元上并排铺好几套线 —— 于是「多开几个端口」代价是面积按端口数的平方涨 。⭐ 最值得看的是绿带那个对账:64 × 256 × 128 ÷ 8 = 256 KiB,正好等于公开资料说的「每核约 256 kB vreg」 —— 两条独立路径撞出同一个数,前面整套模型才算立住。
五张图走完了,串起来其实只有一句话
从方向 (P-26)到一套 vreg 三个门 (P-27)到端口不是总线 (P-28)
到版图上按 lane 切碎 (P-29)再到位线是一张田字格 (P-30)——
每一层都在回答同一个问题的更下一层:数据从哪儿到哪儿,代价是多少。
而五层的答案指向同一个结论:TPU 把「谁什么时候走哪条路」全部搬到了编译期 。
端口有几条是固定的,发射位有几个是固定的,跨 lane 只有那一两个 XLU ——
正因为全都是固定的,编译器才排得出来;也正因为编译器排得出来,芯片上才不需要仲裁器。
这就是这门课从第 1 节起一直在回的那条分水岭 :
GPU 把这些决定留到运行时,所以要 warp 调度器、要记分板、要几十份常驻上下文;
TPU 把它们提前定死,于是那五样东西同时 失去存在理由 ——
省下来的面积,就变成了更多的 MXU 和更大的 VMEM。
拆完部件,下一个必然要问的是片上那块地 —
3.6 那趟 FlashAttention 的全部收益,就来自「中间结果留在片上」。
这个问题只有两半:多大、多快 ,下面两小节各答一半。
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 向量寄存器官方未公开,未计入
表里的数与本节前面各图同源(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 MiB;
64×2+1×2+1×2×2 = 134 MiB。
同一张表,三种口径,三个方向相反的结论
① 比总量 —— GPU 赢,231 对 134,多出 73%。
这一条是对的,别嘴硬。
② 比「软件当数据暂存用的那一层」(只数表里「软件显式管的暂存」那一行 )
—— TPU 赢,130 对 37,约 3.5 倍。
本课反复出现的那句「省下的面积变成了更大的 VMEM」说的是这个口径 ——
但一直没把口径写出来,所以按 ① 读完全合理。
⚠️ 这一条排除了「矩阵操作数暂存」那一行 ——
按「谁说了算」那一列读,TMEM 37 和累加器 4 也算软件管的,全算进来是
134 对 74、1.8 倍 。3.6 引的是前一个数。
报这类比值,必须先说清数了哪几行。
③ 比「一个计算单元能当成连续工作台用的」—— TPU 赢 289 倍。
64 MiB ÷ 227 KiB ≈ 289
—— 这就是前面图里那个 289 倍的出处 。
GPU 那 37 MiB 不是一整块,是 148 个互相看不见的 256 KiB 小岛
—— 而这 256 KiB 里 L1 还占着一部分,一个线程块能显式管到的最多 227 KiB 。
⚠️ 那 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.1 那张图上主路 4 个运行时决策点里,有两个(L2 命中、换出谁)说的就是它。
⭐ 所以这三种口径不是并列的,得看你在问什么:
问「这颗芯片一共有多少 SRAM」用 ①;
问「我能把中间结果放在哪儿不落 HBM 」—— 只有 ③ 算数。
而后面这个问题,正是算子融合每一次都要回答的那个 ——
3.6 并排走 FlashAttention 时,这个 289 倍会直接决定两边把块切多大。
3.2c 那速度呢 —— L2 很大,但它并不快
3.2b 回答的是「有多大、归谁管」。还差一半:跑多快。
前面几张图各自标过一两个带宽数,但从没并排放在一起除过 ——
除完会撞出一个几乎所有人都猜错的结果。
这一站 带宽 相对 HBM 数从哪来
HBM3e(片外) 8.0 TB/s 1× 官方
L2 本分区 21 TB/s 2.6× 第三方实测
L2 跨到对面 die 16.8 TB/s 2.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/s。
128 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 那一行只给量级,不给数
VMEM 的带宽属于未公开规格 —— 官方规格页只给容量,不给带宽。
本课能说的到此为止:它比 HBM 高约一个数量级 ,
而 GPU 的共享内存是 4.3 倍 。
—— 所以能确定的结论只有一句:各跟各的 HBM 比,TPU 这一层甩开得更多。
这一句不依赖具体数值。不要往这里填一个精确数字 ,也不要写成某个整数倍
—— HBM 带宽是公开的,写出倍数等于把那个未公开的数说出来。
⭐ 反直觉的那一条:L2 大,但只比 HBM 快 2.6 倍
大多数人默认「cache 总该快一个数量级吧」。B200 的 L2 不是。
—— 所以 L2 的价值不在「它本身快」,而在「省掉一趟 HBM 往返」 。
命中了,你拿到的是 2.6 倍;没命中,你什么都没省还多绕了一圈。
这就是为什么「让它落在 L2 里就好了」不是一个能依赖的策略 ——
收益倍数本来就不大,而且你还没法保证它在。
§2.1 那 4 个决策点里的两个,押的就是这 2.6 倍 —— 而它只有 2.6 倍。
为什么慢:L2 是全片共享的。
共享内存 ≈ 4.3 倍、VMEM 约一个数量级,都比 L2 的 2.6 倍高 ——
因为它们就贴在计算单元旁边 ,一个 SM 只读自己那块;
L2 的数据得穿过片上互连才到得了 SM。
(这一条是架构推理,不是实测归因 —— 省掉 tag 与替换逻辑也省面积和功耗,
但带宽差距的大头在距离。)
—— 所以真正快的那两块,恰好都是软件说了算的那一类。
那 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 那张账表:乘加总量 GPU 反而多 16%),变成的是更少的电路 ——
代价是要求你(或者编译器)提前把话说死 。
—— 落到选型上就一句话:形状规整、可预测的活,TPU 这套换算得过来;
形状不规则、长度动态变的活,GPU 那笔保险费就花得值。
第 5 节整节都在算这笔保险费到底多少钱。
⭐ 「省下的是复杂度」这句有官方出处 —— TPU 初代论文(ISCA 2017)全文摘录,以及它十年后哪几句不再成立(引文 + 边界)
出处:Jouppi, Young, Patil, Patterson 等,In-Datacenter Performance
Analysis of a Tensor Processing Unit ,ISCA 2017,arXiv:1704.04760。
以下引文逐字,中文是转述不是翻译。
一、省掉了哪些东西 —— 论文一句话列完(第 8 页)
As compared to CPUs and GPUs, 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.
⭐ 注意它的措辞:这些特性「消耗晶体管和能量,改善的是平均情况、
而不是 99 分位」。省掉它们的理由不是省钱,是这类负载根本用不上。
二、省下来变成了什么 —— 摘要里直说
The lack of such features helps explain why, despite having
myriad MACs and a big memory, the TPU is relatively small and low
power .
结论段又说了一遍:
…the omission of general-purpose features that enabled a small
and low power die despite the larger datapath and memory…
三、⭐ 最硬的一个数:控制逻辑在 die 上只占 2% (Figure 2 floor plan)
The light (blue) data buffers are 37% of the die, the
(yellow) compute is 30% , the (green) I/O is 10% , and the
(red) control is just 2% . Control is much larger (and much more
difficult to design) in a CPU or GPU.
—— 「简单」在版图上的样子就是这两个点。
四、「稳定」那一条,官方讲的是确定性 ,不是可靠性
The TPU’s deterministic execution model is a better match to the
99th-percentile response-time requirement of our NN applications than are
the time-varying optimizations of CPUs and GPUs (caches, out-of-order
execution, multithreading, multiprocessing, prefetching, …) that help
average throughput more than guaranteed latency.
⚠️ 这说的是尾延迟可预测,不是良率、不是故障率。
良率/可靠性这门课没有找到任何可引的出处,所以不说这一条。
五、那一代的量化结果(v1 对 K80 / Haswell,2015 年的机器)
矩阵单元 256×256 共 65,536 个 8-bit MAC ,峰值 92 TOPS ;
片上 28 MiB 软件管理 的内存
比 K80 多 25 倍的 MAC (65,536 个 8-bit 对 2,496 个 32-bit)、
3.5 倍的片上内存 (28 对 8 MiB),而功耗不到 K80 一半
推理快 15–30 倍 ,TOPS/Watt 高 30–80 倍 ;
换上 GDDR5 后可到 GPU 的 约 70 倍
TPU die 不到 Haswell die 的一半大
功耗:每 die 增量 40 W 、含主机分摊 118 W 。
⚠️ 但能耗比例性很差 —— 10% 负载时仍用 88% 的电
(论文自陈:短工期没来得及做省电特性 )
⛔⛔ 这篇论文引到今天,三条必须带的边界
① 它比的是 TPU v1(2015)对 K80(2014)。那一代「省下来」确实换成了算力
—— 25 倍 MAC 就是证据。但到 v7 对 B200 反过来了 :
§3.7 那张账表数出来乘加总量 606,208 对 524,288,GPU 反而多 16% 。
同一个机制(minimalism),十年之间结果不一样了。
所以今天只能说「省下的是复杂度」,不能再说「省下的变成了算力」。
② ⛔ 那「面积到底省了多少」?—— 这个问题在公开信息层面答不了 。
NVIDIA 公布 B200 是 2,080 亿晶体管、两颗 reticle 极限 die(各约 800 mm²)、
TSMC 4NP ;而 Google 从不公布 TPU 的 die 尺寸和晶体管数
(v7 的制程是第三方供应链报道的 N3P,官方未确认)。
一边公布一边不公布,「谁的 die 更小」就没有可比口径。
⭐ 顺带一个值得记的观察:B200 那两颗 die 已经顶在光刻极限上
—— 它不能再大了。「把面积用到物理上限」和「用多少算多少」是两种不同的做法,
但要证实后者比前者省,仍然需要那个不公开的数。
③ ⭐ 能说的是「省在哪几项」,而这个可以逐项点名 ——
本课自己的两张表就够:
· L2 整整一层 TPU 没有 :B200 侧 126 MB(≈120 MiB) ,TPU 侧 0
· 线程上下文的寄存器堆 TPU 没有 :B200 侧 37 MiB ,TPU 侧 0
· —— 合计 157 MiB 的片上 SRAM,花在 TPU 压根不存在的两样东西上。
SRAM 是要占面积的。 (§3.2b 那张表)
· 控制接口的份数少 148 倍 (148 是倍数不是差值,592 − 4 = 588 套):同样多的算力,B200 切成
592 个 Tensor Core ,v7 只切成 4 个 MXU ,每个大 128 倍。
每个 Tensor Core 都要自己的操作数通路、发射接口、累加器接线;
一个 256×256 的阵列只要一套。 (§3.7 那张账表)
—— 所以准确的说法是:省下来的东西能一项一项点出来,
但「整颗 die 谁大」这门课给不出答案,因为有一半的数不公开。
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 并行层级:哪几层是运行时才定的
上一节说的「运行时换任务」,具体在哪一层发生?把两边的层级摆开对照。
GPU 这边一共六层 ,比课本上常说的四层多两层 ——
先看清六层是什么、各自钉在哪块硅上。
线程层级 ↔ 硬件归属 —— 每一层抽象都精确对应一条硬件边界,不是纯软件约定
warp:SIMT 的真正宽度
线程块:共享内存的作用域
cluster:分布式共享内存
grid:只剩 L2/HBM
软件抽象层
钉在哪块硅上
这一层内部能共享什么 · 怎么同步
线程
thread
1 个
一个处理块里的 1 条 lane
只有自己的寄存器 —— 别的线程看不见
同步:无
warp
warp
32 个线程
一个处理块 (sub-core)的 32 条 lane,一条指令一拍走完
寄存器可以互相洗牌(shfl )—— 这是最快的通信
同步:天然同步;分支分歧只在这一层内发生
warp 组
warpgroup
4 个 warp = 128 线程
warp 0
warp 1
warp 2
warp 3
一个 SM 的四个处理块各出一个 warp
Hopper 的 wgmma 、Blackwell 的 TMEM 都以它为单位
同步:TMEM 按 warp 号切成四份:warp k 只能碰 lane 32k–32k+31
线程块
CTA / block
≤ 1,024 线程 = ≤ 32 warp
整块钉死在一个 SM 上 ,一旦落下就不迁走
共享内存 (≤227 KiB)—— 这是 CUDA 里最重要的一层作用域
同步:__syncthreads() ;一个 SM 最多同时驻留 32 块
cluster
thread block cluster
≤ 8 块(H100 起可 opt-in 到 16)
8 块(可 16)
同一个 GPC 内的若干 SM
分布式共享内存 :能直接读写同 cluster 里别的块的共享内存
同步:cluster 级 barrier;Hopper 引入,Blackwell 沿用
grid
grid
整个 kernel 的全部线程块
…
整颗 GPU (B200 是跨两个 die 的 148 个 SM)
只剩 L2 和 HBM —— 已经没有片上快捷通道了
同步:只能靠原子操作 / 协作组 / 分 kernel
同一张表,TPU 那边长什么样 —— 层数少得多,而且少掉的那几层,正是 GPU 用来「藏延迟」的那几层
GPU 的这一层
TPU v7 上对应什么
差在哪
线程 / warp
没有对应物。 TPU 是显式向量机:一条向量指令直接吃一整个 8×128 的向量寄存器,没有「32 个线程」
这层皮
GPU 靠「很多 warp 轮流上」来藏访存延迟;TPU 没有这个机制,延迟必须在编译期 排流水藏掉
warp 组 warpgroup
没有对应物。 TPU 的 VLIW bundle 一拍 9 个发射槽同时发,哪条指令进哪个槽,编译期就钉死了(9
槽是 v2/v3 的公开数字,v7 未公布)
GPU 在运行时 挑指令,TPU 在编译期 排指令 —— 这是两边最根本的分工差别
线程块 + 共享内存
一个 TensorCore + 它私有的 64 MB VMEM (Google 开发者社区 Ironwood 调优文;一颗 chip 两
个核 = 128 MB)
容量差 289 倍 (227 KiB vs 64 MiB)—— 但这是上限比上限 :227 是一个线程块 的上限(同一个 SM
上 4 个 Tensor Core 抢它),64 也要跟权重预取 分
cluster
没有对应物。 TPU 一个 chip 只有 2 个 TensorCore,本来也不需要「一组核共享暂存」这层
GPU 要发明 cluster,恰恰是因为它有 148 个 SM 要协调;核少反而省掉一层抽象
grid
整颗 chip 的 2 个 TensorCore,再往外就是 ICI 3D torus 上的别的芯片
GPU 的 grid 在一颗 芯片内部就要协调 148 个 SM;TPU 的协调主要发生在芯片之间
图 G-3 GPU 的四层:thread → warp → block → grid。 注意 block 和 warp 调度器都是运行时 概念 —— 有几个 block、落在哪个 SM,启动才知道。这两层就是 GPU 藏延迟的全部本钱。 —— 出自《GPU 显微镜 》
六层摆完了,紧接着一定会有人问:那为什么偏偏是六层?
—— 下面这张只答这一个问题,一行一个「被什么逼出来的」。
那六层为什么是六层 —— 每一层都是被一个具体的麻烦逼出来的
包含关系与「谁定的」看那张折叠全表,每层钉在哪块硅上看《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 那张对照表上的两个空格,就是这一列不存在时的样子。
六层里只有 grid 和 thread 是边界,其余四层每一层都对应一个具体的硬件麻烦:取指译码太贵,于是 32 个线程共用一条指令流;Tensor Core 大到一个 warp 喂不饱,于是发指令的单位抬到 4 个 warp;共享暂存需要一条作用域边界,于是有了 CTA;一个 SM 的 227 KiB 不够用,于是允许跨 SM 组簇。它们合起来就是「靠运行时适应」这条路线的组织成本 —— 而 TPU 不做那个决定,这一整列同时失去存在的理由。
三个名字的来历 —— warp / CTA / warpgroup
(是词源不是机制,讲课时可跳过)
warp 这个词来自织布 —— 织布机上纵向绷紧、被一起拉动的那一排线叫经线,
英文就是 warp(NVIDIA 自己在早期架构论文里点过这个出处)。取的就是
「一排并排绷着、一起动」 的意象。中文标准译法是线程束 ,
「束」这个字抓到了重点:它们是被捆在一起的,不是各走各的。
那为什么是 32? 这是个权衡:越宽,取指译码摊得越薄,
但分支分歧的惩罚越重、访存合并的粒度越粗 。
AMD 早年用 64(叫 wavefront),新架构也退回了 32。
CTA 全称 Cooperative Thread Array ,
它就是 CUDA 里的 thread block —— 同一个东西的两个名字:
CUDA 那一层叫 block,到了 PTX 和硬件那一层叫 CTA。看到两个词别当成两层。
warpgroup 除了发 wgmma,还带来一种写法叫
warp 分工 (warp specialization):让一个 warpgroup 专门发 TMA 拷贝、
只负责把数据从 HBM 拉进 shared memory,另外几个专门发矩阵指令 ——
搬的和算的分开,流水线自然叠起来 。
FlashAttention-3 和 CUTLASS 3.x 的 Hopper kernel 都建在这套上。
再往里一层。「一个 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 起是整个 warpgroup)
一起发一条指令去用的,「某个线程的 Tensor Core」这个东西根本不存在。
它真正独占的只有寄存器 ,还有它所在那个块分到的共享内存。
这一份从块落到 SM 上就划走,到整个块跑完才还,中间一秒都不让 。
⭐ 所以上面图注里那两句其实是同一件事的两面 :
正因为寄存器是独占且从不搬动的,换 warp 才零成本 ;
也正因为它独占,最先卡住占用率的才是它 ,不是「核够不够」。
—— 「一个线程要多少资源」这个问题,唯一有意义的答法就是
「它要几个寄存器」。
寄存器到底怎么数 —— 单位、上限、以及为什么是 8 的倍数
(回查用 · 讲的时候跳过)
一个寄存器就是 32 位、4 个字节。 编译器报的
「每线程用了多少个寄存器」数的就是这个。
这条能自己验:65,536 × 4 B = 262,144 B = 256 KiB,
正好是图里那个寄存器堆的大小。
每线程有上限:现在的卡都是 255 个 (这个数历史上变过,早期只有 63)。
出处是编程指南里那张按计算能力排的规格表 ——
也正是上面那张图里「每线程 255 个(上限)」那一行的来处。
⚠️ 顺手记一个对不上的地方:255 还是 256
同一家的两份材料给了两个数。 编程指南那张表写 255 ;
而 CUDA 自带的 cuda_occupancy.h 里,
cudaOccRegAllocationMaxPerThread 从 Volta 那一档起写的是 256 。
我没能确认哪个是硬件真值。 一个说得通的解释是
能分配 256 个、能用的只有 255 个 (最后那号被占作恒零寄存器),
—— 但这只是解释,不是查到的出处,别当结论引。
用的时候按 255 算。
这跟附录 A 那个 GiB/GB 是同一类事:「我查了官方文档」并不等于「不会错」,
你还得知道每一份是按哪个口径写的。
不是一个一个分的,是按 warp 打包。 同一个头文件里,
分配粒度写死是 256 个寄存器一包 ,而且 Kepler 到 Blackwell 一路没变过 :
先算 每线程寄存器数 × 32,再往上取整到 256 的倍数。
—— 换算到每个线程,就是「向上取整到 8 的倍数」 (256 ÷ 32 = 8)。
所以你把某个变量从寄存器里挤掉一个,占用率经常纹丝不动;
要动,得跨过 8 这道坎。
⭐ 顺带一个对得上的检查:住满时每线程 32 个,
正好是 8 的倍数,一个字节都没浪费在取整上。
64 位的东西占两格 —— double、以及指针。
kernel 里多留几个指针,占用率掉得比你预期快。
最后回到最底下那一格。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」这个动作。
知道了六层各自为什么存在,本节标题那问题才问得出口:哪几层是「运行时才定的」?
这个问题不能直接答 —— 得先把它拆成两个。
那六层,到底什么时候 定下来 —— 划分全在开跑之前,运行时只剩两个决定
开跑之前就定死
启动那一下定一次
每个时钟周期都在定
🔍 先把「什么时候定的」拆成两个问题 —— 混着问一定得出错结论
问题一:这一组是怎么划出来的? (谁跟谁算一组) 问题二:它什么时候落到硬件上、谁挑它执行? (这一组什么时候真的开跑)
混着问,会得出「GPU 反正都是运行时」这种印象。拆开看完全不是 —— 问题一在下表里六行全是绿的 :要么是硬件写死的规则,要么是启动前主机给定的参数 —— 没有一行是芯片在运行时自己决定的 。
📋 六层 × 两个问题 (底色=时机)
这一层
① 这一组是怎么划出来的
② 什么时候落到硬件上 · 谁挑它执行
线程
thread
硬件写死的规则 :线程号到 lane 的对应是算出来的,不由谁分配
不单独调度 —— 它跟着自己所在的 warp 走,没有自己的命运
warp
warp
硬件写死的规则 :连号的 32 个线程打成一包,改不了
每个时钟周期都在挑 :调度器从驻留的几十个 warp 里,挑一个数据已经到位 的发射出去
warp 组
warpgroup
硬件写死的规则 :连号的 4 个 warp,Hopper 起给矩阵指令用
不单独调度 —— 跟着组成它的那几个 warp
线程块
block / CTA
启动参数给定 :一块多少线程由主机传进来,跑起来就不再变
启动那一下,硬件分发器挑一个 SM 放进去 —— 程序员指定不了 ,落下之后也不迁走
cluster
thread block cluster
启动参数给定 :簇里几块由主机传进来,之后不变
启动时,硬件在同一个 GPC 里 挑出那几个 SM
grid
grid
启动参数给定 :一共几块由主机传进来,之后不变
启动时铺开。块与块之间的先后顺序,规范里明说不作保证
决定一 · 这个块放哪个 SM
启动时,一次
整个 kernel 启动的时候发生一次,之后这块就钉在那儿不动了。
代价很小:一个工作分发器,加上每个 SM 的一本资源账 (还剩多少寄存器、多少共享内存)。频率低,硬件不必为它常备什么。
顺带解释一件事 :CUDA 里没有「把这块放到 3 号 SM」这种写法 —— 不是漏了,是这层抽象故意不交给你 。
决定二 · 下一拍发哪个 warp
每个周期,永不停
只要核在跑,这个决定每个时钟周期都要做一次 ,而且要在一拍之内做完。
代价是全部: 要记分板 盯着谁的数据到了,要几十个 warp 槽 让人排队,要所有上下文常驻在寄存器里 不能换出 —— 不然「立刻切
一个」就无从谈起。
⭐ TPU 的答案:两个决定都不做 —— 于是为它们服务的部件同时失去存在理由
决定一没有 :哪条指令在哪个周期发、用哪个部件,编译期就写死在指令包里 ,没有「放到哪儿」这个问题。 决定二没有 :卡住就是真的空转 ,没有别的活可以顶上来。
所以下一张表里 TPU 那一栏的两个红格子,就是这两个决定。 省下来的面积变成了更多 MXU 和更大的 VMEM —— 代价是编译器排错了,运行时没有人会来救场 。
⚠️ 哪些是查到的,哪些是我归纳的
查到的 :块到 SM 的分配由硬件负责、程序员指定不了、块间执行顺序不作保证;调度器每周期从驻留 warp 里挑一个能发的 —— 都是 CUDA 编程模型的公开说法。
我归纳的 :「运行时只剩两个决定」这句话是我从上表读出来的结论 ,不是谁的原话。它成立的前提是把「划分」和「调度」拆开看 —— 换个拆法可能会数出不一样的数目。
图 P-31 3.3 的标题问「哪几层是运行时才定的」,这张图专门来答。关键是先把问题拆成两个 :① 这一组怎么划出来的 ;② 它什么时候落到硬件上、谁挑它执行 。混着问会得出「GPU 反正都是运行时」,拆开看完全不是 —— 第①栏六行全绿 ,要么是硬件写死的规则,要么是启动前主机给定的参数,没有一行是芯片在运行时自己决定的 。真正留到运行时的只有两个决定,而且性质完全不同 :「这块放哪个 SM」启动时发生一次 ,代价只是一个分发器加一本资源账;「下一拍发哪个 warp」每个时钟周期都要做一次 ,代价才是那一整套 —— 记分板、几十个 warp 槽、全部上下文常驻 。⭐ 这就是 3.2 和 3.3 是同一件事的接缝:上一节说 TPU 核里少掉的那批部件,全部是为第二个决定服务的 ,决定没了它们同时失去存在理由。⚠️ 底带那条:「运行时只剩两个决定」是我从表里归纳的,不是原话 。
知道了运行时只剩那两个决定,再看 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 TPU 这边刻意按问题对齐,不按名词对齐。 名词对名词能排出一张漂亮的翻译表,按问题排才看得见有两行 TPU 那一栏是空的 。空的那两行正是 GPU 的 block 和 warp 调度器。 —— 出自《TPU 显微镜 》
这张表最容易被误用的方式:拿它当翻译词典
图上标出的两个空格,正是上一节 TPU 核里少掉的那批部件 —— 同一件事的第二个视角。
而它最容易引出的一个问题是:「那 TPU 上的 warp 到底是什么?」
这个问题没有答案 —— 它没有对应物。
那一层解决的问题(运行时挑谁跑)在 TPU 上根本不存在,
硬给它找一个对应物,等于给自己造一个不存在的心智模型 ——
然后你会用它去解释 profile,然后每一次都解释错。
跨平台学习翻车,八成翻在这儿,不是翻在记错参数。
3.3b 「运行时适应」在硬件上是三套电路 —— 把账单打开
回到那两个留到运行时的决定 —— 「这块放哪个 SM」和「下一拍发哪个 warp」 。
到这里为止它们还只是编程模型层面的说法。往硬件里再走一步,这笔账落地成三套具体的电路,
各自回答一个很窄的问题:谁现在能发指令、谁跟谁怎么对齐、
数据怎么在不占用线程的前提下就位。
第一个问题这门课其实欠了一笔账 :「记分板」这个词前面已经点过好几次名 ——
拆 SM 的时候、算「下一拍发哪个 warp」代价的时候,
却从没说过它是什么。先把这笔还上。
⭐ 记分板:调度器凭什么知道哪个 warp「就绪」
这个名字不是 NVIDIA 起的,是从 1964 年的 CDC 6600 借来的 ——
那台机器第一次做到指令不按顺序完成,于是需要一张集中的表,
记着哪个运算单元忙着、哪个寄存器正等人往里写,那个部件的正式名字就叫 Scoreboard 。
回到 GPU。调度器每周期要从驻留的 warp 里挑一个就绪的发指令。「就绪」是怎么判定的?
NVIDIA 先按一条线把指令劈成两半 —— 编译器知不知道它要跑多久。
定长的那一半根本不用硬件管。
浮点乘加这类几拍出结果是固定的,编译器直接在指令里写一个「停几拍」的数,硬件照做。
—— 这就是 SASS 里每条指令前面挂着一串控制位的原因。
变长的那一半才是记分板管的。
访存、特殊函数、矩阵指令,跑多久编译期不知道。机制是:每个 warp 六个计数器
(SB0–SB5,各能数到 63);生产者指令发出时加一、写回时减一 ;
消费者指令身上带一个六位掩码,写明它要等哪几号归零。
⭐ 所以它不是「哪个寄存器脏了」的一张大表,而是六个很小的计数器,
加上编译器提前埋在指令里的标记 。它只回答一个问题 —— 这个 warp 现在能不能发指令。
而这一个问题正是延迟隐藏的地基:没有它,就没有「换一个 warp 上来」这个动作。
📌 顺手带走一条判据 :Nsight 里的 long / short scoreboard,
分界线是等的那个东西出没出 SM —— 共享内存、特殊函数、矩阵指令算 short,
全局内存加载算 long。访存瓶颈的 kernel,long scoreboard 一定是最大那一项。
⚠️ 出处:六个计数器与控制位的格式出自第三方微基准反推
(Analyzing Modern NVIDIA GPU cores , 2025),不是 NVIDIA 官方文档 ;
long / short 的分界出自 Nsight 自己的口径。
那为什么叫「记分」—— 以及这块板和 CDC 那块已经不是一回事
(词源与辨析 · 讲的时候可跳)
先说清楚哪句有据、哪句是我推的。
有据的是:这个词出自 CDC 6600,那个部件的正式名字就是 Scoreboard。
而设计者本人为什么挑这个字,我没有找到出处 —— 下面这段是推测,别当史料引。
英文 keep score 的本义不只是「记比分」,是把当下的状态记着 。
球场边那块牌子的用处,是让你抬一次头就知道现在是什么局面。
这张表干的正是这件事,所以中文译成「记分板」其实译窄了,叫「状态板」更贴 。
—— 不过到了 GPU 这一代反倒名副其实:它是真的在数数,
发出去加一、写回来减一。老那块牌子只是挂个「忙」的旗子。
更要紧的是别把两块板当成同一个东西。
CDC 那块是一张集中的大表 ,管全部寄存器的读写冲突,
连「后写的别把先写的盖了」这类反向冲突也归它仲裁,依赖判断全在运行时做 。
NVIDIA 这块只有每个 warp 六个计数器 ,只回答「我等的那件事回来了没有」;
定长指令的依赖压根不经过它 —— 那部分是编译器提前写进控制位排掉的。
—— 名字继承下来了,机制小了一大圈。
而缩掉的那一圈,恰好又是「编译期能定的就别留到运行时」这条主线的一个小注脚
—— 连 GPU 自己也在往这个方向挪。
第三个问题值一张图 —— 带着「哪一列在变、哪几列没变」去看它 。
一块数据怎么进到共享内存 —— 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 资源:
block 里的 warp 陆续到达,硬件数人头,数满了全放行。三个容易忽略的点 ——
① 它按 warp 到达,不按线程 ,因为一个 warp 本来就一起走;
② 它同时兼一个内存栅栏 ,barrier 之前写进共享内存的东西,之后别人一定读得到,
所以你不用另写 fence;
③ 在 barrier 上等着的 warp 仍占着它的槽 —— 不干活,也不腾位子。
所以同步密集的 kernel,占用率那本账不会因为「大家都在等」而变好看。
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 拍 。
所以 DSMEM 不是「把几个 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 × 128 16,384 ÷ 256 64
128 × 256 32,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 倍
再往里一层,到指令。两边都有专门的矩阵乘单元,要问的只有一句:
一条指令一次吃进去的矩阵有多大。 先看 GPU,记住它的收缩维。
先把词说清楚:收缩维就是矩阵乘里被「加掉」的那一维 ——
A[M,K] × B[K,N] 里的 K 。
M 和 N 决定输出块有多大,K 决定一次乘加链有多长 ——
也就是「一条指令一次能吃多深」。
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 Tensor Core 一条指令吃多大一块。 GPU 的收缩维是 16 —— 记住这个数,下一张图要用它。 —— 出自《GPU 显微镜 》
TPU 这边的收缩边是 256 ,正好是上面那个数的 16 倍。
图上第三张卡把 head_dim = 128 那个例子算完了。
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_dim 。head_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 = 128:GPU 上是 16 的 8 倍,切八条指令不浪费;TPU 上只喂满 256 的一半,而空着的那一半没有别的活能顶上来 。 —— 出自《TPU 显微镜 》
⭐ 这一对图直接回答了第 2 节结尾那条反直觉
第 2 节讲 head_dim = 128 在 TPU 上浪费 1/2 时,只说了「MXU 是 256×256」。
并排看这两张图,才知道这句话在 GPU 上根本不成立 ——
GPU 的收缩维是 16,128 是它的整整 8 倍,切八条指令,一点不浪费 。
所以「head_dim 要对齐硬件」这条建议是有前提的 :
同一个模型配置,两边的浪费根本不在一个位置。
在 GPU 上调它收益接近于零,在 TPU 上是实打实的。
这也是为什么跨平台照搬调优经验特别容易翻车。
上面那条结论值得单独摆开看一次 —— 整门课里唯一一个「只改模型配置、
参数量和 FLOP 一个都不动,就换来两位数 MFU」的例子 。
关键在 head_dim 在两个矩阵乘里落在不同的维上:
QKᵀ 是收缩维、堵 MXU 那 256 行,PV 是输出维、堵那 256 列。
head_dim = 128 这笔账 —— 同一个数字,打 TPU 不打 GPU
3.4 最能直接用的一条
在动的行/列
空转:没有第二个任务能填进来
GPU:整除,一点不浪费
TPU v7 · MXU 是 256 × 256
head_dim = 128 在两个矩阵乘里各堵一次 —— 而且堵的不是同一条边
① QKᵀ
head_dim 是收缩维
50%
喂进去 128 行,下面 128 行没有数据 ,整趟空转。
② PV
head_dim 是输出维
50%
输出只要 128 列,右边 128 列没有产出 ,一样空转。
灰的那一半没有办法拿去干别的 —— TPU 没有「换一个跑」这一层(§3 一路在说的那件事,在这儿又结了一次账)。上限
50% 是纯几何推的:128 ÷ 256。
GPU · 收缩维 16、输出维步长 8
同一个 head_dim = 128,两处都整除 —— 改它的收益接近于零
① QKᵀ
K = 128 = 16 × 8
16
16
16
16
16
16
16
16
八条指令连发,累加器一直待在 TMEM 里不落地 。喂满率
100%。
② PV
N = 128,合法宽度
N = 128
wgmma 的 N 从 8 到 256 可选,tcgen05 单 SM 步长 8、
配对步长 16 —— 128 三种都落在合法值里。
但别读成「GPU 不挑食」。 它挑的是 16 的倍数 —— head_dim = 24 在 GPU 上同样只有 75% (一条满 K=16 +
一条只喂 8)。这一条是从整除关系推的。 两边都挑,只是那条边一个 16、一个 256。
所以「head_dim 要对齐硬件」这条建议是有前提的。 在 GPU 上调它收益接近于零,在 TPU 上是实打实的 —— 同一个模型
配置,两边的浪费根本不在一个位置。
📐 实测佐证:参数量和 FLOP 一个都没变,只换了切法
把 Qwen3-30B 的注意力从 32 头 × 128 改成 16 头 × 256 —— 总的 head 维度不变,参数量不变,FLOP 不变 ,变的只有「每一头撞不撞 MXU 那条 256 边」。
MFU 在 8K / 16K / 32K 三个序列长度上分别提升 21% / 32% / 46% 。序列越长,注意力占比越大,这条收益越明显 —— 趋势本身就是这个机制的一个旁证。
但别把这三个数搬到别的模型上。 它们跟层数、序列长度、batch、有没有开 FlashAttention 都相关。能搬的是那句话:先量 head_dim 对不对得上收缩边,再去调别的。
⭐ 落点:模型配置和硬件收缩边,要一起选
结论不是「TPU 不适合注意力」。 是这门课一路在说的那件事换个说法:静态的硬件把选择权交回给了你 —— 也就是说,选错了没人替你兜。
GPU 就算真撞上了(比如上面那个 head_dim = 24),空出来的发射槽还能被别的 warp 顶上,你在 profile 上未必看得见;TPU 上没有第二个任务,空着就是真空着、真报在 MFU 上。 这不是谁更好,是「谁替你收拾烂摊子」的差别 —— 而那份收拾是要付晶体管的。
⚠️ 出处分层
纯几何 :上限 50% = 128 ÷ 256,不需要任何内部信息。 查到的 :MXU 256×256 出自公开工程博客;wgmma/tcgen05 的合法 N 出自 PTX ISA。
实测 :21% / 32% / 46% 与图 T-4 卡片 C 同源,是一个模型上的一组数,不是规律 。「GPU 上收益接近于零」是从整除关系推的,没有配对实测。
图 P-36 同一个 head_dim = 128,打 TPU 不打 GPU。 它在 QKᵀ 是收缩维、在 PV 是输出维,各堵 MXU 的一条边 ,上限 50% 是纯几何推的。但别读成「GPU 不挑食」 —— 它挑的是 16 的倍数,只是那条边窄得多。
最后收个口。课里一共出现过三个倍数,都对
—— 但只有一个是这里算出来的。
同一对硬件,三把尺子 —— 16 倍、128 倍、512 倍,量的是三件不同的事
3.4 的自查
GPU 侧
TPU v7 侧
本课反复说的那个「128 倍」在这一行
尺子一 收缩维 K
一条指令一次能吃多深 。它决定 head_dim
撞不撞墙。
GPU Tensor Core K = 16
16
TPU MXU 的收缩边 = 256
256
16 ×
256 ÷ 16
这一节真正算出来的就是这个数。 head_dim = 128 打 TPU 不打 GPU,
全从这一行来。
尺子二 单个单元多大
一个 Tensor Core/MXU 每个周期 能做多少
次乘加。量的是硬件,不是指令。
GPU 一个 Tensor Core 1,024 乘加/周期
1,024
TPU 一个 MXU 131,072 乘加/周期(256×256×2)
131,072
128 ×
131,072 ÷ 1,024
本课反复说的 128 倍是这一行。 它出自 3.7 的账表,不是 3.4 算出来
的。
尺子三 一条指令的输出块
M × N 有多少个位置。这把尺子最容易被当
成前两把 —— 而它随指令变,一个数都钉不
住 。
TPU MXU 256 × 256 = 65,536
tcgen05.mma 128 × 256 32,768 个位置 → 2 倍
wgmma 64 × 256 16,384 个位置 → 4 倍
mma.sync 16 × 8 128 个位置 → 512 倍
512 × ~ 2 ×
同一对硬件,三个答案
取决于你拿哪条指令来比。 所以这把尺子适合讲道理,不适合报数字。
* 尺子二的 GPU 条与尺子三的 mma.sync 条真实长度不足 3 px,已钳到 6 px 才画得出来 —— 这两根不按比例 ,看数字。
⚠️ 三个数都是对的 —— 错的是随手抓一个当「那个 128 倍」
它们量的东西根本不同 :尺子一量指令能吃多深 ,尺子二量硬件一个周期能做多少 ,尺子三量一条指令产出多大一块 。把任意两把混起来,都会算出一个不存在的结论。
这门课自己踩过一次 :3.4 的标题一度写着「这里出那个 128 倍」,而这一节从头到尾只产出 16 倍。已改。留在这里当例子 —— 这类错读起来完全通顺,只有把三把尺子摆在一起才看得见。
⭐ 落点:报比值之前,先说清楚你在量什么
「GPU 和 TPU 差多少倍」这个问题,没有单一答案 —— 它取决于你量的是哪一维。 同一对硬件,换把尺子就换个数,而三个数都能拿出出处。
能带走的一条习惯 :看到一个倍数,先问它的分子分母各是什么单位。 这条习惯比记住 16、128、512 这三个数有用得多 —— 数字会换代,问法不会。
⚠️ 出处分层
查到的 :K = 16 与三条指令的 M×N 出自 PTX ISA;MXU 256×256、262,144 FLOP/周期/MXU 出自公开工程博客。
口径要照搬、不要升级 :GPU 单核 1,024 乘加/周期在 3.7 的账表里标的是「官方 + 推导」 ,这里沿用同一个标注。它是 128 倍那一行的分母,分母的成色决定整行的成色。
图 P-35 三个比值全都对,量的却是三件不同的事。 收缩维差 16 倍、单个单元每周期吞吐差 128 倍、一条指令的输出块最多差 512 倍。本课反复说的那个 128 倍在第二行 —— 它出自 3.7 的账表,不是这一节算出来的。 (3.4 的标题一度写成「这里出那个 128 倍」,这张图就是那次自查的产物。)
旁白:这张图是本课自查出来的一个错,留着当例子(跟硬件无关,可跳过)
3.4 这一节的标题原本写的是「这里出那个 128 倍」——
可整节从头到尾只产出 16 倍 ,128 在 3.7 的账表里,
是「单个单元每周期多少乘加」那一行(131,072 ÷ 1,024)。
那是硬件单元的吞吐密度比,不是指令粒度比。
更值得记的是它怎么被发现的 :不是有人去查出处,
是照着从头讲了一遍,发现标题承诺的那个数一次都没讲到 。
「讲一遍,看每个承诺兑现没有」是这类错唯一有效的检查方式 ——
它们读起来完全通顺,静态看是看不出来的。
把上面这些话用一个真尺寸走完一遍 ——
Q 投影 M=128K、K=7168、N=1536,阵列 256×256。
一块矩阵乘是怎么在 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 这个门槛都够不着 。
3.5 各自多出来的那一块
两边在主力矩阵乘之外都各自多准备了一样东西,而且方向正好相反 :
GPU 往「更细的缩放粒度 」走 —— 它要解决的是「怎么让 FP4 这种极窄的类型真的能用」。
块量化 —— 「多细的一撮数共享一个缩放因子」,Blackwell 把它做进了硬件
PTX ISA 9.3
整张量一个缩放因子
MX 标准:32 个一组
NVFP4:16 个一组
warp 级指令(消费级 die 才有)
同样 64 个数,三种分法
整个张量一个缩放因子
1 组
scale
最粗。一个离群值就把整条的动态范围拉坏 —— 这是 FP8 训练早期最常见的翻车原因。
MXFP4 每 32 个元素一个
2 组
scale
scale
开放 MX 标准。缩放因子类型 .ue8m0 (8 位指数,无尾数)。
NVFP4 每 16 个元素一个
4 组
scale
scale
scale
scale
NVIDIA 自己的格式,粒度是 MX 的两倍细 。缩放因子 .ue4m3 ,外面再套一层 FP32 的张量级缩放 —— 两级缩放。
关键在于:这套能力挂在哪条指令上 —— 两条通路,但不在同一颗 die 上
warp 级 32 个线程
sm_120a
要求的目标架构 sm_120a → B200 用不了
mma.sync.aligned.m16n8k32.block_scale
mma.sync.aligned.m16n8k64.block_scale
· .kind::mxf8f6f4(1X)· .kind::mxf4 / mxf4nvf4(配 k64 才有 2X/4X)
· 操作数走寄存器 ,一个 warp 就能发
· PTX 8.7 引入,只在消费级 Blackwell 上 (RTX 50 / RTX PRO)
· 这不是 Ampere 遗产 —— fp8/fp6/fp4 这套形式是 Blackwell 才有的。
双 SM 级 256 个线程
sm_100a
要求的目标架构 sm_100a → B200 就这一条
tcgen05.mma.cta_group::2.kind::mxf4nvf4.block_scale
(缩放因子本身也放在 TMEM 里:[scale-A-tmem]、[scale-B-tmem])
· .scale_vec::1X / 2X / 4X · .block16 / .block32(这两个是 tcgen05 的)
· 缩放因子类型 .ue8m0 (MX)/ .ue4m3 (NVFP4)
· 操作数与累加器都在 TMEM
· 这条是粗快车道:吞吐最高,但要两个 SM 配合
图 G-5 块量化:多细的一撮数共享一个缩放因子。 Blackwell 把它做进了硬件,但两条通路不在同一颗 die 上 —— 所以「B200 支持 FP4」这句话要看你说的是哪一条。 —— 出自《GPU 显微镜 》
TPU 往「更不规则的访存」走。
SparseCore —— 第二种核,以及它在生产里到底在干什么
TensorCore 侧
SparseCore 侧
存在的数据通路
公开资料没有列出
两颗核之间怎么传数 —— 注意它是单向的
一颗 device 里有 1 个 TensorCore 和 2 个 SparseCore 。它们不共享地址空间、也没有缓存一致性 —— 跨核只有 DMA 一
条路,而 DMA 的落点可以直接是对方的私有 SRAM,不必绕 HBM。
ICI —— 出芯片,去邻居(见 §8)
HBM 96 GiB / device —— 两颗核共用同一个控制器
TensorCore ×1
VMEM 64 MiB
最小一块 tile (8,128)
2 个 MXU + 1 个 VPU。矩阵乘只在
这里发生。
SparseCore ×2
私有 SRAM 512 KiB
最小一块 (8,)
16 个向量子核 + 1 个标量子核。没
有 MXU 。
HBM → VMEM
HBM → SC 私有 SRAM
VMEM → ICI
SC → ICI
① 能:SC → TC
直写 VMEM
② ?
公开未列
各条通路的存在 可以从公开的 Pallas SparseCore 接口和它的内存空间约束看出来,但完整的通道清单、以及反方向到底存
不存在,公开资料没有列出 —— 本文只画能站住的部分。
「细 128 倍」指的是 tile 形状,不是 DMA 更快
TensorCore 的最小一块
(8, 128)
= 4,096 B
SparseCore 的最小一块
(8,) = 32 B
同一张图上按比例画就是左边的 1/128
32 B 是 HBM 通道宽度,两颗核完全一样。 SparseCore 并没有更快的搬运器,它只是允许你按 32 B 为单位去要 ;
TensorCore 一开口就是一个 4 KB 的 tile。散落在词表里的几百行,用左边那种块去取,取回来的绝大部分都会被扔
掉。
两种核逐项对照
问的是同一件事
TensorCore
SparseCore
每颗 chip 几个
2 个 TensorCore
4 个 SparseCore
每个 device 几个
1
2
一条向量指令多宽
8 × 128 = 1,024 格
16 条 lane
私有 SRAM
VMEM 64 MiB + SMEM 1 MiB
512 KiB / 子核
DMA 最小粒度
32 B(通道宽度)
32 B(同上)
两边一样
最小可寻址的一块
tile (8, 128) = 4,096 B
(8,) = 32 B
差 128 倍
有没有矩阵乘单元
2 个 MXU,256×256
没有
bf16 峰值
≈1,155 TFLOP/s / device
整颗 chip 4 个合计约 MXU 的 1%
量级差两位
它是一颗真的核,不是一台搬运机
公开的 pallas.tpu_sc 里能直接看到它的指令面:cumsum 、sort_key_val 、fetch_and_add 、addupdate_scatter 、
load_gather —— 全是不规则访存 + 归约 这一类活。
但整份接口里没有任何矩阵乘原语。 所以它不是「小一号的 TensorCore」,是另一种形状的核。
最反直觉的一条:我们的生产任务里,它一次表都没查
要不要把 embedding 卸载到 SparseCore,看的是同一批里「重复取同一行」的程度 —— 重复得越厉害,专用通路省下的越多。
推荐系统那边少量热行被反复命中,重复度很高,这是 SparseCore 的主场。而我们这次的语言模型取行重复度不够高 (不是没有重复 :十
几万 token 里不同的词只有几千个,绝大部分命中最高频那一两千行,只是跟搜广推差着量级)—— 落在最不划算的一端 ,编译器于是根本
没把它派过去。
那生产里的 embedding 到底怎么跑的?—— 它根本不是查表
MaxText 有一个开关 use_iota_embed 。打开时,查表被写成「把 token id 展成 one-hot,再和整张词表做一次矩阵乘」 —— 一条 dot ,跑在 MXU 上。关掉才是真的 gather 。上游默认是关的,但仓库里 34 份配置显式打开、0 份显式关闭 ,包括那几份给 GPU 用
的配置。
代价算得清楚:Hunyuan3 那个尺寸下,这条 matmul 是 28.38 TFLOP/device,占一步的 0.62% ,约 24.6 ms;同样的事用 gather 只要约 1.0 ms —— matmul 慢约 24 倍 。所以不要说「反正 MXU 闲着,用算力换带宽很划算」,算术直接否掉了:它不省时间,它花
时间。成立的唯一理由是分母够大 ,而真实动机(最可能是反向传播里 scatter-add 在分片下难做)我没查实,不写成结论 。
图 T-5 TPU 这边的第二种核,它没有矩阵乘单元。 但要小心:「专门查 embedding 表的核」这句话会让你看自己的 profile 时彻底看错 —— 生产任务里它可能一次表都没查过。 —— 出自《TPU 显微镜 》
这两块「多出来的」有一个共同点
它们都不是给通用计算用的 ,而且都容易被一句宣传话带偏:
· 「B200 支持 FP4」—— 但两条通路不在同一颗 die 上 ,
你说的是 warp 级那条还是双 SM 那条,能不能用完全不同;
· 「TPU 有个专门查 embedding 表的核」—— 但它只对「同一批行被反复查」的负载划算,
语言模型落在最不划算的那一端 ,编译器根本没往那儿派。
⛔ 注意别把这条读成「语言模型用不上这颗核」 ——
用不上的只是查表 那条用途。它在大模型训练里真正跑的是
集合通信卸载 :把 All-Gather / Reduce-Scatter 从 TensorCore 接过去,
让计算和通信并行 (见 Google Cloud《Training large models on
Ironwood TPUs》,以及 MaxText 公开 XLA flag 库里的 SparseCore All Gather)。
这个名字本身就是上面那句宣传语的又一个受害者。
共同的教训是同一条:看 trace,不要看宣传页。
这两个坑被问到的频率极高,而且都是「说法没错、但用它解释自己的 profile 就会全错」。
⭐ 想知道块量化在硬件上到底怎么跑的
—— NVFP4 与 MXFP4 差在哪、HBM 里放的是什么、量化是谁做的、
Blackwell 究竟加了哪三样、以及训练侧那个 16×16 为什么存在
—— 整套机制连同出处台账都在
专题八 · 精度与量化 。
这一讲只用它做「宣传口径 vs 细则门槛」的例子。
那「处理不规则访存」到底是怎么处理的?—— 把这颗核拆开看一眼 。
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 那块 。
⭐ 它扛延迟靠的不是每次更快,是十六路各追各的地址、同时欠着很多次取数 。
知道了它不是什么,接着该知道它是什么 —— 把盖子掀开看一眼。
这颗核在这门课里的地位跟 MXU 同级,理由是同一条:
它不是某个部件的加强版,是主核上被砍掉的那套能力的独立承载者。
把 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 全部出自公开材料(MaxText 的
benchmarks/xla_flags_library.py、OpenXLA 文档与公开的 Ironwood 调优配方),
列出来是为了让「卸载」这两个字有个能上手的抓手 :
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 Core TPU 的 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.md 与
benchmarks/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 那半句成立,GPU 那半句不成立 :块量化不在 Tensor Core 外面,就在里面。
摆正之后,那两个坑会露出一个共同的形状。
两块「多出来的」不在同一层 —— 而两道门槛都不在宣传页上
3.5 的自查
在指令里(同一条数据通路)
是独立的核
门槛:宣传页不会写的那一句
GPU · 块量化
它换的是指令,不是硬件模块
一个 Tensor Core(同一套硅)
mma.sync / tcgen05.mma
普通版:只有 A、B、累加器
…….block_scale
带块量化的同一条指令 ,多两个缩放操作数
缩放因子
tcgen05 这条上,缩放因子跟操作数
一起放在 TMEM 。
所以它不是「挂在主计算单元之外」的东西。 同一套硅,换一条指令变体就有了 —— 差别在指令集,不在框图上多一个方块。
TPU · SparseCore
这才是「主计算单元之外」的那种多出来
TensorCore
有 MXU
矩阵乘主力。本课一路拆下来的都是它。
SparseCore
没有 MXU
为不规则访存 准备的:DMA 粒度 64 字节(SIMD 宽
16),不是靠 (8,128) 那套单位。
HBM · 共用控制器,通道宽度也一样 —— 差的是一次搬多小,不是能搬多快
两个方块,两颗核。 它们之间连线的方向公开资料没有列出,所以这张图不画那根箭头 —— 这里只主张一件事:它是独立的核,
不是一条指令。
两块多出来的能力,各自都有一道门槛 —— 而两道门槛都不写在宣传页上
GPU 那道门槛:你手上是哪颗 die
宣传口径:「B200 支持 FP4」
块量化有两条通路,不在同一颗 die 上。 warp 级的 mma.sync…block_scale 要 sm_120a (消费级 Blackwell,RTX 50 /
RTX PRO);tcgen05…block_scale 要 sm_100a 。
B200 只有后面那一条。 所以这句宣传语没说错,错的是听的人默认「两条都有」。先问是哪一条,再问能不能用。
TPU 那道门槛:这批数据的取行重复度够不够高
宣传口径:「TPU 有个专门查 embedding 表的核」
它只对「同一批行被反复查」的负载划算。 推荐系统那边少量热行被反复命中,重复度很高,那是它的主场。
而语言模型的取行重复度不够高 (不是没有重复 :十几万 token 里不同的词只有几千个,绝大部分命中最高频那一两千行;只是跟
搜广推差着量级)—— 落在最不划算的一端,编译器根本没往那儿派,生产任务里它一次表都没查过。
⭐ 落点:两句宣传语都没说错 —— 错的是把「支持」读成「我的负载用得上」
这两个坑是同一个形状 :能力确实存在,但通往它的路上有一道只在文档细则里的条件 。宣传页只负责说「有」,不负责说「你够不够格」。
能带走的一条动作 :看到一个「支持 X」,先去找那道门槛在哪一行。 找不到就当它不适用于你 —— 然后去看 trace。这两条都不是本课特有的技巧,是所有硬件宣传语的通用读法。
⚠️ 出处分层
查到的 :sm_120a 跟 sm_100a 的区别出自 PTX ISA 的 Target ISA Notes;一批 131,072 个 token 与词表 129,280 行,是本课这次训练任务的实际配置。
不主张的 :两颗核之间连线的方向,公开资料没有列出 ,所以这张图不画那根箭头。「块量化在数据通路里」是从「它是 MMA 指令的一个变体、缩放因子与操作数同住 TMEM」推的。
图 P-37 两块「多出来的」不在同一层。 块量化就在 Tensor Core 的数据通路里 ,是同一条 MMA 指令的一个变体;SparseCore 才是物理上独立的核。下半张是这张图真正的增量 :两个坑其实是同一个形状 —— 能力都真的存在,但各自有一道只写在细则里的门槛 (哪颗 die/取行重复度)。
旁白:这一节的自查 —— 对称是修辞,不是证据(方法论,可跳过)
「两边各挂一块、方向相反」是个很顺口的句式 —— 顺口正是它的危险之处 。
为了凑齐对称,它把一个指令变体 说成了独立部件 ,
而这门课从第 1 节起就在强调「部件在哪一层」是判断力的地基。
收回那半句之后反而多赚了一条:
两个坑摆到一起,形状是一样的 —— 能力都真的存在,
但通往它的路上各有一道只写在细则里的门槛 。
GPU 那道是你手上是哪颗 die ,TPU 那道是这批数据的取行重复度够不够高 。
看到「支持 X」,先去找那道门槛在哪一行;找不到,就当它不适用于你。
3.6 高潮:FlashAttention 在两条路上各走一遍
前面那几小节拆的都是部件,这一小节拆的是路 —— 拿一个真东西来走。
FlashAttention 没换算法、没减 FLOPs、没用新指令,只改了数据在路上的走法 。
先看它要解决什么。
先看这一节要解决的到底是什么 —— 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 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 的分块重复读,不是零。
把这张图的三个数记住,后面全靠它们
序列 128K、head_dim 128、bf16,注意力矩阵 S 一个人就是 32 GiB ;
朴素写法要写 S、读 S、写 P、读 P 四趟,128 GiB 的 HBM 往返 ;
而真正的输入输出 Q/K/V/O 加起来只有 128 MiB 。
中间产物比输入输出大 256 倍,而且全在路上。
FlashAttention 靠 online softmax 边算边更新,让 S 永远不落 HBM,
把这 128 GiB 整项删掉。它一次乘法都没省 —— 省的全是搬运。
这就是为什么这一节值得讲:真实瓶颈大多数时候不在算,在路上。
要看清它在两边分别怎么走,先各看一眼这条路本身。GPU 侧:
一个数走完全程 —— 从 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 B200 侧五站,中间两站是缓存。 命中不命中要跑起来才知道 —— 这就是 2.1 主路那 4 个红框的物理来源。 —— 出自《GPU 显微镜 》
TPU 侧 —— 注意中间那一站的名字,一字之差,性质完全不同 :
一个数走完全程 —— 中间那一站是「暂存」,不是「缓存」
片外: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 v7 侧这条路(到累加器为止),中间那站不是缓存,是暂存。 每一站只问一个问题:这一步是谁决定的 。从头到尾没有一站的答案是「硬件自己看着办」—— 这张图是 2.1 里「TPU 侧 0 个红框」的逐站展开。 —— 出自《TPU 显微镜 》
现在把两边并起来,让 FlashAttention 同时在两条路上往前推:
一行是同一站,左边是什么、右边是什么。这是本节的主图,横着读。
同一个 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 这一节的主图:同一个 FlashAttention,两边一站一站并排走完。 横着读,每一行是同一站。六行里只有两行是结构性的 —— 第 ② 行(GPU 有一整层硬件缓存,TPU 没有)和第 ⑥ 行(切换由谁安排)。中间那条黄带顺手统一了本课此前不一致的「TPU 到底几站」:两处数的是两条不同的路,所以不该报总数。
这张表的读法:逐行问「这一步是谁决定的」
问完会得到一个很干净的结果 ——
GPU 侧有两站答「硬件,运行时才知道」,TPU 侧一站都没有。
而这正是第 2 节里「GPU 4 个红框、TPU 零个」的来历:
那 4 个红框不是随手圈的,就是沿着这条路径一站一站数出来的。
到这里第 2 节那张图才算真的讲完了。
旁白:顺带收掉的一处口径不一致 —— TPU 到底几站(图上已说明,可跳过)
本课此前一处写 TPU「五站」、另一处写「四站」 。
两个都对 —— 一处沿 MXU 那条支线数(HBM/VMEM/向量寄存器/MXU/累加器),
一处沿 VPU 那条主路数(HBM/VMEM/向量寄存器/VPU)。
数的是两条不同的路,所以本来就不该报一个总数。
改成按功能对齐 —— 问「谁在这一层、谁没有这一层」——之后,这个矛盾自己就没了。
走到这里还剩最后一个问题,而它恰好是整门课的那条主线 :
第 ③ 行说「tile 住在片上暂存里」—— 那么,是谁把它搬进去的?
同一个算法,两种性质的工作 —— 一边亲手搬,一边只声明形状
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
要不要占 L1 :cp.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 访问都要过它,没有哪条指令能跳过。真正能选的只有 L1 :cp.async 的 .ca 走 L1+L2,.cg 只走 L2。
绕开的不是缓存本身,是对缓存的依赖。 复用被显式安排进共享内存之后,命中率高不高就不再决定性能了 —— 这才是那句话想说的意思。
⭐ 落点:第 5 节那条主线,在一个具体 kernel 上长这样
GPU 上,FlashAttention 是一段和硬件的猜测协商的代码 :硬件准备了缓存和 warp 调度器来兜住不确定性,而这个 kernel 不需要那种兜底 ,于是自己接管了搬运。TPU 上没有可绕开的东西 ,于是只剩下声明形状。
代价也对称 :GPU 的手写 kernel 换一代硬件要重调;TPU 的声明写错了,编译器排出来就是真的停住,没有第二个任务顶班。 同一个算法,两种工程性质 —— 这就是运行时与编译期的分工,落在一个 kernel 上。
⚠️ 出处分层
查到的 :cp.async 的 .ca / .cg 缓存行为出自 PTX ISA;BlockSpec / index_map / grid 出自 Pallas 文档;Splash Attention 的块级掩码出自其公开实现。
沿用本课已有标注 :共享内存 ≤ 227 KiB / 线程块出自图 G-6。「FlashAttention 把复用显式放进共享内存」是公开实现的通行做法,不是某一份文档里的原话。
图 P-40 同一个算法,两种性质的工作。 数学、FLOPs、结果三样完全相同 —— 差的只有「谁来安排搬运」 。 GPU 侧 6 条要 kernel 作者亲手写 :块多大、发 cp.async/TMA、双缓冲、warp 分工、对齐、要不要占 L1。 TPU 侧只有 2 条声明 :BlockSpec 与 grid。右栏比左栏短,这件事本身就是结论。 红带按住了最容易讲过头的那句:L2 绕不开,绕开的是对缓存的依赖。
⭐ 同一个算法,在两边是两种性质的工程工作
数学一样、FLOPs 一样、结果数值等价,差的只有「谁来安排搬运」 。
GPU 上是一段和硬件的猜测协商的代码。
硬件准备了缓存和 warp 调度器来兜住「不知道下一步要什么」,
可 FlashAttention 的访问模式完全可预测 ,那套兜底对它没有价值 ——
于是它自己接管了搬运:块开多大、什么时候发 cp.async/TMA、
双缓冲怎么错开、哪一组 warp 专管搬、哪几组专管算 、屏障放在哪,
全写在 kernel 源码里。
TPU 上没有可绕开的东西 ,因为本来就没有那一层。
于是只剩下声明形状:BlockSpec 说清每块多大、第 i 块取哪一段,
剩下的 DMA 时序由编译器在编译期排死。
代价也是对称的 :GPU 那份手写 kernel 换一代硬件要重调;
TPU 那份声明写错了,编译器排出来就是真的停住,没有第二个任务顶班 。
这就是第 5 节那条主线 —— 运行时决定,还是编译期决定 ——
第一次落在一个具体 kernel 上的样子。
⚠️ 顺手按住一句最容易讲过头的话
「FlashAttention 绕开了缓存」这句话只对一半。
L2 绕不开 —— 所有 HBM 访问都要过它,没有哪条指令能跳过;
真正能选的只有 L1 (cp.async 的 .ca 走 L1+L2,
.cg 只走 L2)。
绕开的不是缓存本身,是对缓存的依赖 ——
复用被显式安排进共享内存之后,命中率高不高就不再决定性能了。
这才是那句话想说的意思。
3.7 压轴:把这一节的账合起来
前面每一小节看的都是局部。最后这一张把整节的账合成一张图 ——
回答本节开篇那句:总量差不多,那差的到底是什么。
同一把尺子上 —— 两个方块面积等比,总量几乎一样 ,切分粒度差 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 总量几乎一样,切法完全不同。 两个方块面积按每周期乘加次数等比,内部按真实单元数切。两个比值别混 :份数差 148 倍,单个单元多大差 128 倍 —— 3.4 说的是后者。 —— 出自《GPU 显微镜 》
⭐ 图上两个比值必须分开看,否则会算出第三个数
图上同时给了两个比值,它们完全不是一回事 :
份数 592 ÷ 4 = 148 倍 ;单个单元多大 131,072 ÷ 1,024 = 128 倍 。
本课反复说的那个「128 倍」指的是后者 ——
它出自这张账表的「单个单元多大」那一行,不是 3.4 算出来的 。
3.4 交付的是收缩维差 16 倍 ,两者量的不是同一件事(P-35 已经把三把尺子并排摆过了)。
如果只说「差 128 倍」而屏幕上摆着 592 和 4,
592 ÷ 4 = 148 —— 一个跟这门课反复说的 128 毫无关系的数。
顺带一提,这张图在原文档里是压轴,
但它压的是那份文档 的轴 —— 在本课它只收第 3 节这一节的口。
第 4 节还有另一个 128 倍,位置完全不同,两者无关。
⭐ 这张图最值得记的是右边那个 148 ÷ 128
它不是约等于,是精确等于 :148 ÷ 128 = 1.156,
而 606,208 ÷ 524,288 = 1.156。
—— 「份数多 148 倍」和「每份小 128 倍」这两个数几乎抵消掉了,
这才是两边峰值差不多的真正来源。
剩下那 1.16 倍,看你拿哪一颗 GPU 来比 —— 两个方向的答案是反的 :
· 锚在 HGX 板上的 B200 (1.83 GHz):606,208 × 2 FLOP × 1.83 GHz = 2,219
≈ 官方 2,250,而 TPU 524,288 × 2 FLOP × 2.2 GHz = 2,307
—— TPU 用高出两成的时钟把那 1.16 倍补了回来 ,就是图上右下那格的 1.03 倍。
· 锚在 NVL72 里那颗 GB200 (第 1 节开场用的就是它,2,500):
反推时钟 2,500 ÷ (606,208 × 2) ≈ 2.06 GHz,
TPU 只高 6.8% —— 压不过 1.156,于是变回第 1 节那个「GPU 高 8%」。
⚠️ 抵消本身是真的 (份数和单元大小两边完全一样),
但「谁在上面」取决于你比的是哪一颗 ——
图上那格 1.03 倍带着一个 ˟,指的就是这件事。
⭐ 所以这一节所有的倍数,说的全是「份的大小」,不是「算力大小」。
两边的硅在同一个量级上,只是一个切成 592 小份,一个切成 4 大份。