NVIDIA A100:从芯片到 SIMT¶
A100 是理解现代数据中心 GPU 的合适起点:它把大规模 SIMT 计算、第三代 Tensor Core、统一 L1/共享内存、40 MB L2、HBM 和 NVLink 放在同一套层次结构中。本页依次画出 整体架构、SM 微架构、访存架构与 SIMT 编程模型,最后用一个 batch=1 的 W8A8 Llama 线性层把 Kernel、编译产物与 Tensor Core 执行串起来。
图的口径
本页区分 完整 GA100 Die 与实际 A100 产品。完整 GA100 设计为 8 GPC、128 SM;A100 产品启用 7 GPC、108 SM。图中数量来自 NVIDIA 架构白皮书,40/80 GB 与带宽差异按具体 PCIe/SXM 产品区分。
1. 整体架构¶
下图把包含、调度、片上互连与完整访存层次放在同一张系统图中:外框表示包含,数量标签与末端摘要卡表示同类单元重复,紫色虚线表示 Host 发起 Kernel 后的控制/调度路径,青色双向箭头表示数据通路。每个 SM 内嵌自己的 Unified L1/Texture Cache + Shared Memory;L2、Crossbar 和内存控制器位于 GPC 之外,由所有启用的 SM 共享。展开的 GPC 0 / TPC 0 是结构代表,不表示只有它包含下一级。
A100 · Host 调度、计算与存储互连
Cache Fill / Evict
Atomic
所有 SM 共享
/ Stack
HBM2 / HBM2e
40 / 80 GB
- 外框嵌套:包含关系
- ×N数量 + 摘要卡:同类单元重复
- 紫色虚线:控制/调度
- ↔青色双向箭头:数据通路
与 NVIDIA 官方架构的校对口径
NVIDIA 白皮书给出的 A100 产品实现为 7 GPC、每 GPC 7 或 8 TPC、每 TPC 2 SM,共 108 SM;每个 SM 有 192 KB Unified L1 Data Cache / Shared Memory。40 MB L2 位于 GPC 之外,由所有 GPC/SM 共享,并分成两个分区;每个分区含 40 个 512 KB Slice,10 个内存控制器各关联 8 个 Slice。图中的 Partitioned Crossbar → L2 → Memory Controller → HBM 因而是一条连续的数据路径,而不是独立摆放的“共享存储”附图。参见 NVIDIA A100 架构白皮书 与 NVIDIA Ampere 架构解析。
如何比较图中的 L2、HBM 与 L1/Shared 数字
L2 与 HBM:时钟域、接口和数据来源都不同。 5120 B/L2 clk 是 80 个片上 L2 Slice 面向 SM/Crossbar 的聚合读吞吐;10 × 512-bit = 640 B/HBM Transfer 是十个内存控制器面向片外 HBM 的物理接口宽度。HBM 使用独立 DRAM 时钟并以 DDR 每时钟传输两次,所以不能把两者当成同一个 clk 比较。以 A100 40 GB 白皮书峰值口径估算,L2 为 5120 B × 1.41 GHz ≈ 7.22 TB/s,HBM 为 640 B × 2 × 1.215 GHz ≈ 1.555 TB/s。L2 Hit 直接从多 Slice 并行读取片上 SRAM,同一 Cache Line 还能被多个 SM/多次访问复用,不必经过内存控制器;只有 L2 Miss 的填充、Evict/Writeback 等流量受 HBM 带宽约束。
L1/Shared:一个是事务粒度,一个是理想并行吞吐。 32 B Transaction 表示 Warp 的 Global Memory 请求经 Coalescer 后按 32 B 单元访问 L1/下级层次;它是“一笔事务多大”,不是 L1 每周期只能传 32 B。32 Banks × 32-bit/Bank/clk = 128 B/clk 表示 Shared Memory 无 Bank Conflict 时的理想 Bank 聚合吞吐。典型的 32 Thread 连续读取 32 个 float 共 128 B:在 Global/L1 路径上通常拆成 4 × 32 B 事务;若数据已在 Shared Memory,则可由 32 个 Bank 各提供 4 B,并行服务这 128 B。L1 与 Shared 虽共享 192 KB 物理块,但不能据此把 128 B/clk 当作已公开的 L1 物理总线位宽。
1.1 从完整 Die 到 A100 产品¶
| 层次 | 完整 GA100 设计 | A100 产品实现 |
|---|---|---|
| GPC | 8 | 7 |
| TPC | 8/GPC | 7 或 8/GPC |
| SM | 2/TPC,128/Die | 108 |
| FP32 CUDA Core | 64/SM,8192/Die | 6912 |
| 第三代 Tensor Core | 4/SM,512/Die | 432 |
| HBM 控制器/Stack | 12 个 512-bit / 6 | 10 个 512-bit / 5 |
这里最重要的不是背诵 108,而是看清可扩展层次:GPC 包含多个 TPC,TPC 包含两个 SM,SM 是执行 CUDA Thread Block 的核心计算单元。
2. SM 微架构¶
一个 A100 SM 可以理解为四个对称处理分区:每个分区都有自己的 L0 指令缓存、Warp Scheduler/Dispatch、寄存器文件切片和执行单元;四个分区共同连接 SM 级 L1 指令缓存与统一 L1/共享内存。四张分区卡片明确表示物理重复关系,不把面积比例当成真实版图。
一个 A100 SM 的内部组织
- Register ×16K
- FP32 ×16 · INT32 ×16
- Tensor Core ×1
- LD/ST ×8 · SFU ×4
- Register ×16K
- FP32 ×16 · INT32 ×16
- Tensor Core ×1
- LD/ST ×8 · SFU ×4
- Register ×16K
- FP32 ×16 · INT32 ×16
- Tensor Core ×1
- LD/ST ×8 · SFU ×4
- Register ×16K
- FP32 ×16 · INT32 ×16
- Tensor Core ×1
- LD/ST ×8 · SFU ×4
四个分区合计得到每 SM 的 64 个 FP32 Core、64 个 INT32 Core 与 4 个 Tensor Core。Tensor Core 执行矩阵乘加;普通 CUDA Core、整数单元、Load/Store 与特殊函数单元共同覆盖地址计算、标量运算和其他指令。
| SM 资源/上限 | A100(Compute Capability 8.0) |
|---|---|
| FP32 CUDA Core | 64/SM |
| 第三代 Tensor Core | 4/SM |
| 32-bit Register | 64K/SM |
| Unified L1/Shared 物理资源 | 192 KB/SM |
| 可配置 Shared Memory 上限 | 164 KB/SM;单 Block 最多 163 KB |
| 最大常驻 Warp / Thread | 64 Warp / 2048 Thread |
| 最大常驻 Block | 32/SM |
192 KB 不等于都能分给 Shared Memory
白皮书中的 192 KB 是统一 L1 Data Cache 与 Shared Memory 的物理资源总量;CUDA 暴露的 A100 Shared Memory 上限为每 SM 164 KB、每 Block 163 KB。寄存器和 Shared Memory 的单 Block 用量还会共同限制 occupancy。
公开资料能说明到哪一层
NVIDIA 公布了 SM 框图、单元数量、指令语义和峰值吞吐,但没有公开 CUDA Core、Tensor Core、LD/ST 或 SFU 的晶体管级电路、完整流水级与端口连接。下面的图是由官方资料支持的功能级微架构,不是版图或 RTL 复原;更细的延迟与指令映射只能借助微基准测试推断。可对照 A100 架构白皮书、CUDA Programming Guide 的 Compute Capability 8.x,以及两项公开研究:Ampere 指令与访存微基准和 Tensor Core 指令级拆解。
2.1 从 Warp 指令到五类执行管线¶
每个处理分区的 Warp Scheduler 从已就绪 Warp 中选择一条指令;Dispatch 传递的是同一条 Warp 指令及其 Active Mask,各 Lane 的操作数和结果仍分布在寄存器文件中。下图用紫色虚线表示发射/控制,用实线表示操作数或数据流,避免把“谁控制谁”和“数据往哪里走”画成同一种箭头。
flowchart TB
WS["Warp Scheduler<br/>选择 Ready Warp"] -. "发射 + Active Mask" .-> DP["Dispatch"]
DP -.-> PIPE["Execution Pipes / Partition<br/>FP32 ×16 · INT32 ×16 · Tensor ×1<br/>LD/ST ×8 · SFU ×4"]
RF["Register File Slice<br/>16K × 32-bit"] <--> PIPE
PIPE <-->|"仅 LD/ST 数据路径"| L1["Unified L1 / Shared"]
L1 <--> L2["Shared L2"]
linkStyle 0,1 stroke:#7c4dff,stroke-width:2px,stroke-dasharray:5 4
linkStyle 2,3,4 stroke:#008ea3,stroke-width:1.5px 四个分区合起来就是图中的 64 FP32 + 64 INT32 + 4 Tensor Core + 32 LD/ST + 16 SFU。单元数量描述的是并行资源,不等于每条指令的固定延迟;吞吐还会受数据相关、发射冲突、Active Mask、Cache Miss 和 Bank Conflict 影响。
2.2 CUDA Core:Warp 共享控制的标量向量流水线¶
这里的 CUDA Core 特指白皮书计数口径中的 FP32 算术 Lane,不是一个能独立取指的“小 CPU”。其功能路径可以分成四步:
- Warp Scheduler 选择一条就绪的 FP32 Warp 指令,例如 SASS
FADD、FMUL或FFMA。 - Dispatch 将同一操作码和 Active Mask 发给所在分区;各活跃 Lane 从寄存器文件切片读取自己的源操作数。
- 每分区 16 个 FP32 Lane 执行逐 Lane 运算,四个分区在峰值吞吐口径下合计产生 64 个 FP32 结果/clk/SM。若是
FFMA,一个结果包含一次乘和一次加,因此性能表按 FLOP 计数时是 128 FLOP/clk/SM。 - 结果写回各 Thread 的架构寄存器;Scoreboard 在依赖满足前阻止后续相关指令发射。吞吐数字不能直接当成单条指令延迟。
A100 还在每分区配置 16 个独立 INT32 Lane。地址更新、循环计数等整数运算可与 FP32 运算并行,这正是白皮书所说的“FP32 与 INT32 同时执行”;但是否真的重叠取决于编译出的指令序列、依赖关系和 Scheduler 当时可发射的 Warp。Thread 只是状态与 Lane 语义,执行期间会先后使用 FP32、INT32、LD/ST、SFU 等不同管线,不能永久绑定到某一个 CUDA Core。
2.3 Tensor Core:Warp 级矩阵指令和分布式 Fragment¶
Tensor Core 与 CUDA Core 共用 Scheduler、寄存器和存储层次,但消费的是 Warp 级 MMA 指令。一个典型高吞吐数据通路如下:
flowchart TB
GM["HBM / L2"] -->|"coalesced load 或 cp.async"| SMEM["Shared Memory Tile"]
SMEM -->|"ldmatrix / warp load"| FRAG["32 Lane 寄存器中的<br/>A/B Fragment"]
FRAG --> MMA["mma.sync / HMMA · IMMA · DMMA<br/>+ C Accumulator<br/>+ 可选 2:4 稀疏元数据"]
MMA --> OUT["D / Accumulator Fragment"]
OUT --> NEXT["后续 MMA 或 store"] - 协作粒度是一个 Warp。 A、B、C/D 的 Fragment 分散在 32 个 Lane 的寄存器中;某个 Thread 不拥有完整矩阵。
mma.sync要求参与 Lane 共同执行,Scheduler 发射一次 Warp 级矩阵指令。 - API Tile、PTX 指令和物理 Tensor Core 不是同一层。 WMMA 常见 Tile 如
m16n16k16;Ampere PTX/SASS 可使用更细的m16n8k16等 MMA 形状;白皮书给出的物理吞吐示例则是每个 Tensor Core 每周期完成一个 8×4×8 的 FP16/FP32 混合精度矩阵乘加。不要把其中任一形状直接解释成电路阵列的永久二维尺寸。 - 每分区一个、每 SM 四个。 每个第三代 Tensor Core 可做 256 个 FP16 FMA/clk,四个合计 1024 FP16 FMA/clk/SM;同时支持 BF16、TF32、FP64、INT8、INT4 与 Binary 等模式,不同模式的输入、累加类型和吞吐不同。
- 结构化稀疏是额外模式。 2:4 稀疏 MMA 携带非零值与元数据,硬件跳过规定位置的零,在满足格式与软件支持时把有效 Tensor 运算吞吐提高到最多 2 倍;它不是对任意稀疏矩阵自动加速。
第三代 Tensor Core 的重要改进不只是“单 Core 更宽”:A100 让数据在 Warp 的 32 个 Thread 间共享(Volta 为 8 Thread 范围),并增大单条指令的 K 维工作量,从而减少寄存器读取和指令数。官方没有公开其 Operand Collector、乘法阵列与归约树的完整流水级,因此这里不画臆测的内部门级结构。
2.4 Load/Store 与 SFU:地址、合并访问和特殊函数¶
| 单元 | 功能级微架构 | 容易误解的点 |
|---|---|---|
| LD/ST(8/分区,32/SM) | 活跃 Lane 生成地址,Warp 请求由 Coalescer 合并;Compute Capability 8.0 的 Global Memory 访问以 32 B 事务为基本口径,经 Unified L1 后命中返回寄存器,未命中继续到 L2/HBM。Store、Atomic 和 Shared Memory 指令也占用相应数据路径。 | 32 个 LD/ST 单元 是地址/指令处理资源计数,不表示“32 条固定字节宽度的 L1 总线”;实际事务数由访问宽度、对齐与 Lane 地址分布决定。 |
cp.async 路径 | A100 可把 Global Memory 数据异步搬到 Shared Memory,并可选择绕过 L1;数据不必先落入中间寄存器,可与独立计算配合异步 Barrier 做流水。 | 它减少寄存器占用和往返流量,但不会绕过 L2/HBM,也不会自动消除未合并访问或 Shared Memory Bank Conflict。 |
| SFU(4/分区,16/SM) | 执行单精度特殊函数相关指令;在 SASS 中可见 MUFU(Multi-Function)家族,常覆盖 reciprocal、reciprocal square root、sin、cos、exp2、log2 等近似核心操作。 | CUDA 的高精度数学函数可能由 Range Reduction、MUFU 与校正指令组合实现,并非每个源码函数都等于一条 SFU 指令;应以反汇编与实测为准。 |
Shared Memory 与 L1 共用 192 KB 物理块,但编程语义不同:Shared Memory 是软件显式寻址的 32-Bank Scratchpad,每个 Bank 每周期 32 bit;L1 是硬件管理的 Cache/Coalescing Buffer。32 Banks × 32-bit/clk = 128 B/clk 只描述无 Bank Conflict 时的 Shared Memory Bank 聚合口径,不能改写成“A100 L1 的公开物理总线位宽”。
3. 访存架构¶
3.1 层次、作用域与物理位置¶
flowchart TB
T["Thread<br/>标量状态"] --> R["Register<br/>线程私有 · SM 内"]
R --> S["Shared Memory / L1 Data Cache<br/>Block 协作 / 硬件缓存 · SM 内"]
S --> L2["L2 Cache · 40 MB<br/>所有 SM 共享"]
L2 --> HBM["Global Memory · HBM<br/>设备级持久数据"]
HBM <-->|PCIe 4.0| HM["Host Memory"]
L2 <-->|NVLink 3.0| PM["Peer GPU Memory"]
LM["Local Memory<br/>线程私有的地址空间"] -. 寄存器溢出等访问最终落到显存并经过缓存 .-> L2 | CUDA 空间 | 主要作用域 | 典型物理落点 | 性能含义 |
|---|---|---|---|
| Register | Thread | SM 寄存器文件 | 最低延迟;过多会降低常驻 Warp,溢出形成 Local Memory 访问 |
| Shared Memory | Block | SM 内统一 L1/Shared 资源 | 软件管理的 Scratchpad;关注容量、同步和 Bank Conflict |
| Local Memory | Thread | 设备内存,经 L1/L2 缓存 | “Local”指作用域而非片上位置,常见于寄存器溢出或大局部数组 |
| Global Memory | Grid/应用 | HBM,经 L2/L1 | 容量大、延迟高;合并访问和片上复用决定有效带宽 |
| Constant/Texture 路径 | Grid | 设备内存 + 专用缓存路径 | 适合特定只读与访问局部性模式 |
3.2 A100 产品带宽不要混用¶
| 形态 | 显存 | 标称显存带宽 |
|---|---|---|
| A100 40 GB PCIe / SXM | 40 GB HBM2 | 1555 GB/s |
| A100 80 GB PCIe | 80 GB HBM2e | 1935 GB/s |
| A100 80 GB SXM | 80 GB HBM2e | 2039 GB/s |
NVLink 的 600 GB/s 是 12 条链路的双向聚合 GPU 互连能力,不应直接当作 CPU 主存带宽;PCIe Gen4 x16 的 64 GB/s 也是双向聚合标称值,单方向理论量级约为 32 GB/s。
3.3 一次 Tile 计算的数据路径¶
sequenceDiagram
participant H as HBM
participant L as L2 / L1
participant S as Shared Memory
participant R as Register
participant C as CUDA / Tensor Core
H->>L: 读取输入 Tile
L->>S: 合并并异步搬运
S->>R: Warp 读取操作数片段
R->>C: 发射标量或矩阵指令
C->>R: 累加局部结果
R->>S: 需要时交换/重排
S->>H: 经 Cache 写回输出 Tile A100 引入了从 Global Memory 到 Shared Memory 的硬件加速异步拷贝,可绕开中间寄存器占用,并允许软件把数据搬运与计算重叠。这正是“软件调度 × 存储层次 × 计算单元”协同的典型例子。
4. SIMT 编程模型¶
4.1 软件层次如何映射到硬件¶
先看软件对象的包含关系,再看 Block 如何被调度到 SM。两者是不同关系,不用同一组箭头混画。
Grid → Block → Warp → Thread
B 是一个 Block 的线程数;最后一个 Warp 可以只有部分活跃 Thread,但硬件调度单位仍是 Warp。上图回答“谁包含谁”;下面回答“运行时谁被交给谁”。这两组层次不是四级一一对齐:Grid → Block → Warp → Thread 是软件组织,GPC → TPC → SM → 执行流水线 是硬件组织,真正稳定的调度契约落在 Block → SM。
CUDA 对象在 A100 上的实际落点
把这张图读成四句话即可:
- Grid 面向整个 CUDA 设备。 A100 的工作分发逻辑把 Grid 中尚未执行的 Block 分派到可用 SM;基础 CUDA 启动语法不能指定 GPC、TPC 或 SM,具体分发次序也不是程序语义的一部分。
- GPC/TPC 是硬件分组,不是软件层次的“配对项”。 A100 中
GPC → TPC → SM最终提供 108 个 SM;普通 Grid 可以使用跨多个 GPC 的 SM,但并不存在1 Grid = 1 GPC或1 Block = 1 TPC的关系。 - Block 是不可拆分的驻留单位。 一个 Block 的全部线程都由同一个 SM 执行;因此它的所有 Warp 也留在该 SM 内。反过来,只要线程数、寄存器、Shared Memory 等资源允许,一个 SM 可同时常驻多个 Block。
- Warp 才是 SM 内的指令发射单位。 Scheduler 发射的是“某个 Warp 的下一条指令 + Active Mask”,不是逐个发射 32 个独立 Thread;Thread 保留各自的数据与控制状态,当前指令再决定使用哪类执行单元。
读新版 CUDA 文档时注意 A100 的边界
Compute Capability 9.0 起还可显式组织 Thread Block Cluster,并保证一个 Cluster 位于同一 GPC;A100 是 Compute Capability 8.0,不支持这一层。理解本页时,不要把 Hopper 之后的 Cluster → GPC 规则倒套到 A100。
4.2 Block 如何分批进入 SM¶
Grid 可以包含远多于物理 SM 容量的 Block。尚未获得 SM 资源的 Block 只是待执行工作;某个 SM 有资源空位时,完整接纳一个 Block,为它的全部线程分配寄存器、为整个 Block 分配 Shared Memory,并把它拆成常驻 Warp。Block 完成后资源释放,后续 Block 再补入,因此大 Grid 会以许多动态“波次”推进。
待执行 Block → 资源准入 → 多个 SM 并行驻留
A100(Compute Capability 8.0)每个 SM 的硬上限是 2048 Thread、64 Warp、32 Block;但真实常驻 Block 数取各项约束中最紧的一项。可把它粗略理解为:
其中 \(N_{\text{register}}\) 和 \(N_{\text{shared}}\) 分别表示寄存器容量、Shared Memory 容量能支持的并发 Block 数。这两项还受分配粒度影响,实际应以编译器资源报告、CUDA Occupancy API 或 Nsight Compute 为准。尤其要区分:常驻(Resident)只表示执行上下文和资源已经在 SM 上,并不表示这些 Warp 正在同一个周期同时做运算。
4.3 Warp 如何在 SM 内真正执行¶
一个 A100 SM 有四个处理分区,每个分区包含 Warp Scheduler/Dispatch、寄存器文件切片和一组执行流水线。常驻 Warp 会处于“可发射”或“等待数据/依赖/同步”等不同状态;每个调度器从自己负责的常驻 Warp 中选择一个下一条指令已就绪的 Warp 发射。Warp 的寄存器和控制状态一直保存在片上,因此从一个 Ready Warp 换到另一个不需要像 CPU 线程那样保存、恢复上下文。
常驻 Warp 池 → Scheduler → 指令对应的执行流水线
Eligible Warp
+ Dispatch
1111…1011选择管线
最容易混淆的一点:Thread ≠ CUDA Core
Thread 是逻辑执行上下文,CUDA Core 是 SM 中服务标量算术指令的物理流水线单元;A100 白皮书以 FP32 CUDA Core 计数。 一个 Thread 在 Kernel 生命周期里会先后参与地址计算、Load/Store、FP32、甚至 Tensor Core 指令,并不会从头到尾占着某一个 CUDA Core。反过来,一个 CUDA Core 也会随时间服务许多不同 Warp 的 Lane 操作。
对 A100 而言,一个 SM 有 64 个 FP32 CUDA Core,分成四个处理分区、每分区 16 个。Warp 仍有 32 个 Thread;NVIDIA 白皮书给出的 FP32 FFMA 示例是一个 Warp 的 32 个 Lane 运算在一个 SM 分区上用 2 个周期完成。可见“32 Thread 的 Warp”从来不意味着“固定配 32 个 Core”。其他指令的管线宽度、延迟和吞吐又各不相同,CUDA 程序不应依赖更细的 Lane→物理单元绑定。
以 y[i] = a * x[i] + y[i] 为例,进入 if 的一个 Warp 大致会经历下列硬件路径;这是理解 SIMT 比“一个线程对应一个 Core”更准确的方式:
| 源码阶段 | Warp 级动作 | A100 主要硬件路径 | 每个 Thread 仍然独立的部分 |
|---|---|---|---|
计算 i | 32 个 Lane 执行整数/地址指令 | INT32 Pipeline | 各自的 threadIdx、blockIdx 与 i |
读取 x[i]、y[i] | 发射 Warp Load;相邻地址尽量合并 | LD/ST → L1/L2 → HBM | 各自请求的地址和返回值 |
a * x + y | 发射 Warp FP32 FMA | FP32 CUDA Cores | 各自的操作数与结果寄存器 |
写回 y[i] | 发射 Warp Store;相邻地址尽量合并 | LD/ST → Cache / HBM | 各自的写地址与数据 |
当 Warp A1 因 HBM 访问等待数百周期时,Scheduler 可以发射已经 Ready 的 Warp F3。延迟隐藏就来自大量常驻 Warp 中持续找到可做的工作,而不是让一个 Warp 的 32 个 Thread 随意跑到其他 SM。高 Occupancy 会增加可选择的 Warp,但如果它们都在等待同一个瓶颈,也不保证更高性能。
4.4 分支发散如何发生¶
flowchart LR
PC["同一 Warp · 同一条指令"] --> P{"每个线程的条件"}
P -->|偶数 Lane| A["执行 Path A<br/>Mask: 1010…"]
P -->|奇数 Lane| B["执行 Path B<br/>Mask: 0101…"]
A --> R[Reconverge]
B --> R
R --> NEXT[继续执行共同路径] 当同一 Warp 内线程走不同分支时,硬件用 Active Mask 分别执行所需路径,未参与该路径的 Lane 被屏蔽,因此有效利用率下降。不同 Warp 则可以独立推进,Warp Scheduler 会在可运行 Warp 之间切换以隐藏访存或流水线延迟。
__global__ void saxpy(int n, float a, const float* x, float* y) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
y[i] = a * x[i] + y[i];
}
}
这个 kernel 中,相邻线程访问相邻元素,有利于合并 Global Memory 访问;边界处最后一个 Warp 可能只有部分 Lane 有效。实践中通常让 Block 线程数为 32 的倍数,但最终仍需结合寄存器、Shared Memory、指令延迟和实际 occupancy 判断。
不要用“Warp 永远严格锁步”写同步逻辑
Volta 及之后支持 Independent Thread Scheduling。SIMT 仍规定 Warp 级执行语义,但线程间通信和内存可见性必须使用正确的同步原语;不能依赖未写明的隐式锁步行为保证程序正确。
5. 贯通示例:Llama batch=1 W8A8 Tensor Core GEMM 从 CUDA 到 A100¶
这一章不再分别介绍名词,而是跟踪一次 W8A8 线性层从量化公式、CUDA C++、PTX/SASS,一直走到 A100 的 GPC、SM、Warp Scheduler、Tensor Core 和存储层次。示例刻意保持完整、可推演:它明确发出 INT8 WMMA 指令,但仍是教学 Kernel,不是替代 cuBLAS、CUTLASS、TensorRT-LLM 或专用量化库的生产实现。
5.1 先把 W8A8 的“整数计算、最后反量化”说清楚¶
在单 batch、单 token 的 Llama 风格线性层中,逻辑运算仍是:
从数学形状看,它是 M=1 的 GEMM,也就是 GEMV;本章称其为 Tensor Core GEMM,是因为实现仍把它组织成 Warp 级矩阵 Tile,而不是改回逐元素标量点积。
本章采用一种易于追踪的对称 W8A8 口径:Activation 使用当前 token 的 per-tensor scale \(s_x\),Weight 使用每个输出通道的 per-channel scale \(s^W_o\),Zero Point 均为 0。权重在离线阶段量化;Activation 在运行时由上一算子或单独的量化步骤产生:
真正送进 Tensor Core 的两个乘数都是 INT8;乘积累加到 INT32,完整点积结束后才在 Epilogue 中反量化:
即使按最保守的 \(|q|\le128\) 估算,\(|z_o|\le4096\times128^2=67{,}108{,}864<2^{31}\),本例不会溢出 INT32。若线性层带 Bias,可在反量化后的浮点 Epilogue 中相加;若下一层仍接收 INT8,也可把 Epilogue 改为 Requantize,而不必写出 FP16。
显存中的 qweight[N,K] 按输出通道行主序存放;数学上使用的是它的逻辑转置 [K,N],运行时不需要真的转置整张权重矩阵。
INT8 输入与权重进入 Tensor Core,INT32 结果再反量化
qA[0,:]×qW[16b:16b+16,:]ᵀ→y[16b:16b+16]Activation 量化属于端到端执行流的一部分
本章 Kernel 接收已经量化好的 qA 与 sₓ。真实推理中,qA 可能来自显式 Q/DQ 图中的 Quantize 节点,也可能由前一 Kernel 的 Epilogue 直接 Requantize 后写出;前者多一次量化 Launch,后者可省掉中间 FP16 写回。两种情况都不改变本章追踪的 INT8 × INT8 → INT32 → Dequantize 主数据路径。
5.2 教学 Kernel:4 个 Warp 用 Tensor Core 计算 16 个输出¶
WMMA 的 INT8 m16n16k16 运算以一个 Warp 为协作单位,计算 \(16\times16\) 输出 Tile。逻辑 Activation 只有一行,因此下面把它放到 A Tile 的第 0 行,其余 15 行补 0;每个 Block 的 4 个 Warp 分别处理 1024 个 K 元素,最后把四份 INT32 部分和相加。
这段代码与编程模型的对应关系是:
| CUDA 对象 | 本例取值 | 它负责什么 |
|---|---|---|
| Kernel / Grid | 1 次 Launch / 256 Blocks | 完整输出 y[0:4096] |
| Thread Block | 128 Threads = 4 Warps | 一个 16-output N Tile;完整驻留于一个 SM |
| Warp | 32 Threads / 一个 K Slice | 共同执行 64 次 m16n16k16 WMMA,形成一份 INT32 部分和 |
| Thread / Lane | Warp 的协作成员 | 搬运 Tile 的一部分,并持有 WMMA Fragment 的一部分寄存器 |
| Block Epilogue | Thread 0–15 | 合并 4 个 K Slice,乘 sₓ × sᵂ[o],写回 FP16 |
这里不能再把一个 Thread 解释成“计算一个输出元素”。WMMA Fragment 在 32 个 Lane 的寄存器间按硬件规定分布,其元素到 Lane 的具体映射不是 CUDA C++ API 的稳定接口;只有整个 Warp 的 32 个 Lane 都执行 load_matrix_sync、mma_sync 和 store_matrix_sync,一个矩阵 Tile 才完整。
5.3 从 CUDA C++ 编译成 A100 的 INT8 Tensor Core 指令¶
编译 A100(Compute Capability 8.0)版本时,可保留不同层级的产物,并检查最终机器码是否真的出现整数矩阵乘加:
# 常用构建:生成可链接的 Host Object,并内嵌 A100 Device Code
nvcc -O3 -std=c++17 -arch=sm_80 -lineinfo -c w8a8_tc_gemv.cu -o w8a8_tc_gemv.o
# 分别观察虚拟 ISA 与 A100 原生机器码
nvcc -O3 -std=c++17 -arch=compute_80 -ptx w8a8_tc_gemv.cu -o w8a8_tc_gemv.ptx
nvcc -O3 -std=c++17 -arch=sm_80 -cubin w8a8_tc_gemv.cu -o w8a8_tc_gemv.cubin
cuobjdump -sass w8a8_tc_gemv.cubin
WMMA 从 CUDA C++ 降到 PTX,再成为 A100 的 IMMA
保留 Kernel Launch Stub
生成 Warp 级矩阵操作
wmma.mma · s8→s32→ ptxas →Cubin
sm_80 · IMMA
只有 PTX 时 JIT
要抓住三个边界:
- WMMA 是 CUDA C++ 的 Warp 级矩阵 API。 Fragment 是分布在 32 个 Lane 寄存器中的协作对象,不是某一个 Thread 的普通数组。
- PTX 是虚拟 ISA。 本例应能看到
wmma.load、wmma.mma.sync...s32.s8.s8.s32、wmma.store等语义;A100 不直接执行 PTX 文本。 - SASS/Cubin 才是目标机器码。
ptxas为sm_80完成寄存器分配、指令选择和调度;最终用cuobjdump -sass检查 IMMA-family 指令。
源代码与指令族大致有如下对应。确切寄存器、指令数量和次序随 CUDA 版本与优化而变化,应以本机产物为准:
| CUDA 源操作 | 典型 PTX 指令族 | A100 SASS 中常见指令族 | 主要硬件路径 |
|---|---|---|---|
从 Global 搬 qA / qW 到 Shared | ld.global / st.shared | LDG / STS | LD/ST → L1/L2/HBM → Shared |
wmma::load_matrix_sync | wmma.load.a/b.sync | LDSM / Shared Load | Shared → 各 Lane 的 Fragment 寄存器 |
wmma::mma_sync | wmma.mma.sync...s32.s8.s8.s32 | 一个或多个 IMMA | 第三代 Tensor Core |
wmma::store_matrix_sync | wmma.store.d.sync...s32 | STS | Fragment → Shared Memory |
| 四个 K Slice 求和 | add.s32 | IADD3 等 | INT32 Pipeline |
| INT32 反量化并转 FP16 | cvt / mul.f32 / cvt.rn.f16 | I2F / FMUL / F2F | 转换 + FP32 Pipeline |
写回 y[o] | st.global | STG | LD/ST → Cache / HBM |
W8A8 不会让任意标量循环自动变成 Tensor Core Kernel
输入类型、Tile 形状、内存布局和 API 都要满足矩阵指令约束。本例显式调用 WMMA,所以编译目标是 INT8 Tensor Core;生产代码更常通过 cuBLASLt、CUTLASS、TensorRT 或专用 Kernel 生成同类 MMA。验证时既要在 SASS 中确认 IMMA,也要用 Nsight Compute 检查 Tensor Pipe 指标,不能只根据源码类型判断。
5.4 Launch 之后:一个 Block 在 A100 上经历什么¶
Host 侧的 <<<256, 128, 0, stream>>> 只描述 Grid、Block、动态 Shared Memory 和 Stream,不指定 GPC/TPC/SM。Kernel Launch 对 Host 通常异步;首次模块装载或 PTX JIT 仍可能带来一次性准备开销。
一次 INT8 Tensor Core Launch 的运行时执行时间线
<<<256, 128, 0, stream>>> 产生 256 个相互独立的 Block。y[1,4096]。下面冻结一个 Warp 的一次 K-Tile 迭代,再接上 Block Epilogue:
Lane 协作搬 Tile、Tensor Core 累加、Block 级 Split-K 合并
partL1
partL2
part•••L31
part
m16n16k16 · s8 × s8acc_frag += A × B · s32 32 Lane 共同拥有 Fragment 代码中的关键构造可落到如下硬件层次:
| 代码构造 | SIMT 含义 | A100 上的实际作用 |
|---|---|---|
blockIdx.x × 16 | Block 选择一个 N Tile | Work Distributor 可把不同输出 Tile 分给任意可用 SM |
warp × K_PER_WARP | Block 内 Warp 选择一个 K Slice | 四个 Warp 独立形成同一 16-output Tile 的部分和 |
lane 搬 A/B | Lane 选择不同 Tile 元素 | Global 请求合并,并把数据写入对齐的 Shared Tile |
a_frag / b_frag / acc_frag | Warp 共同拥有的分布式状态 | Fragment 分散在 32 个 Lane 的寄存器中 |
wmma::mma_sync | Warp 级共同矩阵指令 | Warp Scheduler 发射,第三代 Tensor Core 执行 INT8 IMMA |
c_tile[4] + __syncthreads() | Block 内交换与同步 | Shared Memory 汇合四个 Warp;所有 Warp 到达 Barrier 后再读 |
act_scale × w_scale[o] | Epilogue 反量化 | INT32 转 FP32、乘 Scale、转 FP16,经 LD/ST 写回 |
5.5 把规模数字代回 A100¶
- Grid 有
4096 / 16 = 256个 Block;每 Block 128 Thread,也就是 4 Warp。 - 每个 Warp 处理 1024 个 K 元素,共发出
1024 / 16 = 64次 Warp 级 WMMA;每 Block 256 次,整个 Grid 共 65,536 次wmma::mma_sync调用。 - 逻辑问题仍只有
4096 × 4096 = 16,777,216次有用 INT8 乘加。一次m16n16k16WMMA 覆盖 4096 次物理乘加;由于 batch=1 只使用 16 行中的第 0 行,本教学映射会做约 16 倍矩阵算术,其中 15 行是 Padding。这正是 M=1 的 Tensor Core Tile 利用率问题。 - A100 每 SM 上限为 2048 Thread / 64 Warp,因此仅从 Thread/Warp 上限看,最多可同时容纳
2048/128 = 16个本例 Block;约 6 KiB Shared Memory、Fragment 寄存器分配以及每 SM 的 Block 上限还会进一步约束实际常驻量,应查看nvcc -Xptxas=-v与 Nsight Compute。 - 256 个 Block 足以在理想条件下覆盖 108 个 SM,并留下尾部 Block;实际准入仍是空位即补入,不存在全芯片整齐同步的“波次栅栏”。
从数据量看,INT8 qweight[4096,4096] 约 16 MiB,qA[4096] 仅 4 KiB,FP32 per-output Weight Scale 为 16 KiB,FP16 输出为 8 KiB。每生成一个 token,绝大多数权重仍只使用一次;W8A8 把两个乘数都变成 INT8 并启用高吞吐 Tensor Core,却没有改变 batch=1 时权重流量占主导、数据复用很低的事实。因此它通常仍更接近带宽受限,而不是接近 A100 的峰值 INT8 TOPS。
M=1 使用 Tensor Core,不代表一定比专用 INT8 GEMV 更快
生产 Kernel 会采用预打包权重、向量化/异步搬运、多级 Shared Memory Pipeline,并常通过 Continuous Batching 把多个 token/request 合并成更大的 M,避免 15/16 的 Padding 浪费。若必须保持严格 M=1,库或推理引擎也可能选择 dp4a、更窄的 MMA Tile、Split-K 或专用 GEMV 路径。最终选择取决于形状、布局、融合范围和实测延迟;“输入是 W8A8”只说明具备 INT8 Tensor Core 路径,并不保证该路径对所有形状最优。
5.6 用一条因果链复述整个执行流¶
上游把当前 Activation 量化成
qA + sₓ→ Driver 准备sm_80Cubin → Host 发起<<<256,128>>>→ Block 按资源空位落到 108 个 SM → 每 Block 的 4 个 Warp 分担四段 K → Lane 协作把 INT8 A/B Tile 从 HBM/L2/L1 搬入 Shared →wmma.load形成分布式寄存器 Fragment → Warp Scheduler 发射 IMMA → Tensor Core 完成s8 × s8 → s32→ 四个 Warp 的 INT32 K Slice 经 Shared Memory 合并 → Epilogue 乘sₓ × sᵂ[o]反量化 → Thread 0–15 写 16 个 FP16 输出 → 全部 256 个 Block 完成后得到y[1,4096]。
如果这条链能够不看图复述出来,就已经把 W8A8 量化、CUDA SIMT、Tensor Core 编程、编译栈与 A100 硬件架构真正连在了一起。
6. 校验问题¶
读完后应能独立回答:
- 完整 GA100 的 128 SM 与 A100 的 108 SM 为什么不能混写?
- 为什么不能把
Grid → Block → Warp → Thread与GPC → TPC → SM → CUDA Core逐级一一对应? - 一个大 Grid 为什么会分多个波次执行?Block、Warp 与 SM 的驻留关系分别是什么?
- 为什么
32 Threads / Warp不代表每个 Thread 永久对应一个 CUDA Core? - 192 KB Unified L1/Shared 为什么不代表 Block 可申请 192 KB Shared Memory?
- Local Memory 为什么可能比 Shared Memory 慢很多?
- 分支发散、访存不合并和低 Occupancy 分别损失了什么资源?
- 80 GB SXM 的 HBM 带宽、NVLink 带宽与 PCIe 带宽各描述哪条路径?
- 对称 W8A8 中,为什么
INT8 × INT8可以先完整累加到 INT32,再统一乘sₓ × sᵂ[o]反量化? - 教学 Kernel 中,Block、Warp 与 Lane 分别负责 N Tile、K Slice 和哪一类协作?
- 为什么不能把 WMMA Fragment 的某个输出元素永久对应到某个 Thread?
wmma::mma_sync、PTXwmma.mma...s32.s8.s8.s32与 A100 IMMA-family SASS 分别处在哪一层?- 为什么 batch=1 即使使用 INT8 Tensor Core,仍会同时面临 M Tile 利用率低和 HBM 权重带宽瓶颈?
参考资料¶
- 参考页面:HiHuo《GPU 架构基础》
- NVIDIA A100 Tensor Core GPU Architecture Whitepaper
- NVIDIA A100 Tensor Core GPU Datasheet
- CUDA Programming Guide:Thread Blocks and Grids
- CUDA Programming Guide:Warps and SIMT
- CUDA Programming Guide:Hardware Multithreading
- CUDA Programming Guide:Writing SIMT Kernels
- CUDA Programming Guide:Coalesced Global Memory Access
- CUDA Programming Guide:Warp Shuffle Functions
- CUDA Programming Guide:Warp Matrix Functions / WMMA
- PTX ISA:Warp-level Matrix Multiply-Accumulate
- CUDA Programming Guide:NVCC
- NVIDIA CUDA Compiler Driver:Compilation Trajectory
- NVIDIA CUDA Binary Utilities:cuobjdump
- NVIDIA Developer Blog:Faster Parallel Reductions on Kepler
- NVIDIA CUDA 11 Features:A100 Multi-precision Tensor Cores
- NVIDIA Matrix Multiplication Background:Tensor Core Tile 与性能
- NVIDIA TensorRT:INT8 Quantization Schemes
- NVIDIA Ampere GPU Architecture Tuning Guide:Streaming Multiprocessor
- NVIDIA CUDA Best Practices Guide:Coalesced Access 与 Shared Memory Banks
- NVIDIA CUDA Binary Utilities:Ampere SASS Opcode(FFMA / HMMA / IMMA / MUFU 等)
- Demystifying the Nvidia Ampere Architecture through Microbenchmarking and Instruction-level Analysis
- Dissecting Tensor Cores via Microbenchmarks: Latency, Throughput and Numeric Behaviors