跳转至

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 是结构代表,不表示只有它包含下一级。

SYSTEM + CHIP ARCHITECTURE

A100 · Host 调度、计算与存储互连

7 GPC · 108 SM
HOST SYSTEM Host CPU + System Memory 配置 Grid / Block · 发起 Kernel · 准备输入数据
PCIe 4.0 x16 命令 + Host 数据
PEER SYSTEM Peer GPU / NVSwitch GPU 间数据交换与扩展互联
EXTERNAL I/O Host + Peer Ports 两个入口汇入芯片内部控制面与数据面
控制路径Kernel Launch / Command
Front End + Work Distribution 命令处理 · 将 Thread Block 分发到启用的 GPC / SM
调度到 GPC控制路径
External I/O ↔ CrossbarHost / Peer 双向数据路径
Compute hierarchy · private L1/Shared per SM GPC → TPC → SM
GPC 0展开一个代表
Texture Processing Cluster TPC
TPC 0展开
SM 0Execution + LD/ST192 KB Unified L1 / Texture + SharedGlobal→L1 事务 32 B · SMEM 32 Banks × 32-bit/bank/clk
SM 1Execution + LD/ST192 KB Unified L1 / Texture + SharedGlobal→L1 事务 32 B · SMEM 32 Banks × 32-bit/bank/clk
×2 SM / TPC · 每个 SM 独享一份 L1/Shared
×7 / 8•••TPC / GPC
TPC NSM ×2L1/Shared ×2
×7•••GPC / A100
GPC 6TPC ×7 / 8SM ×2 / TPCL1/Shared / SM
Load / Store
Cache Fill / Evict
Atomic
ON-CHIP DATA FABRIC Partitioned
Crossbar
  • GPC / SM Ports
  • L2 Bank Ports
  • External I/O Ports
分区访问
所有 SM 共享
Outside GPCs · shared by all SMs共享 L2 + Memory I/O
SHARED CACHE · 40 MB TOTALL2 Cache聚合读吞吐 5120 B/clk · 40,960 bit/clk
Partition 020 MB · 40 Slices Partition 120 MB · 40 Slices
8 × 512 KB L2 Slice / Controller (跨时钟)
DRAM INTERFACEMemory Controller ×10512-bit / Controller · 5120-bit Total
2 Controllers
/ Stack
CUDA ADDRESS SPACE Global Memory 主要物理载体
HBM ×5 Stack

HBM2 / HBM2e
40 / 80 GB

  • 外框嵌套:包含关系
  • ×N数量 + 摘要卡:同类单元重复
  • 紫色虚线:控制/调度
  • ↔青色双向箭头:数据通路
逻辑架构图,不按 Die 面积、位置或布线比例绘制。Host 与 Peer 位于加速器外;GPC/TPC/SM、每 SM 的 L1/Shared、Crossbar、L2 与内存控制器位于 GA100 Die 内;HBM 位于同一加速器封装内、Die 外。Global Memory 是 CUDA 地址空间,图中以其主要物理载体 HBM 表示,并不是额外的一块片上存储。

与 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/共享内存。四张分区卡片明确表示物理重复关系,不把面积比例当成真实版图。

SM MICROARCHITECTURE

一个 A100 SM 的内部组织

4 个处理分区
L1 Instruction Cache SM 级共享取指资源
Processing Partition 0L0 I-Cache · Scheduler · Dispatch
  • Register ×16K
  • FP32 ×16 · INT32 ×16
  • Tensor Core ×1
  • LD/ST ×8 · SFU ×4
Processing Partition 1L0 I-Cache · Scheduler · Dispatch
  • Register ×16K
  • FP32 ×16 · INT32 ×16
  • Tensor Core ×1
  • LD/ST ×8 · SFU ×4
Processing Partition 2L0 I-Cache · Scheduler · Dispatch
  • Register ×16K
  • FP32 ×16 · INT32 ×16
  • Tensor Core ×1
  • LD/ST ×8 · SFU ×4
Processing Partition 3L0 I-Cache · Scheduler · Dispatch
  • Register ×16K
  • FP32 ×16 · INT32 ×16
  • Tensor Core ×1
  • LD/ST ×8 · SFU ×4
192 KB Unified Data Cache L1 Data Cache + Shared Memory · SM 内共享
四个分区合计 64K × 32-bit Register、64 FP32、64 INT32 与 4 个 Tensor Core;上下横条表示 SM 级共享资源。

四个分区合计得到每 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”。其功能路径可以分成四步:

  1. Warp Scheduler 选择一条就绪的 FP32 Warp 指令,例如 SASS FADD、FMUL 或 FFMA。
  2. Dispatch 将同一操作码和 Active Mask 发给所在分区;各活跃 Lane 从寄存器文件切片读取自己的源操作数。
  3. 每分区 16 个 FP32 Lane 执行逐 Lane 运算,四个分区在峰值吞吐口径下合计产生 64 个 FP32 结果/clk/SM。若是 FFMA,一个结果包含一次乘和一次加,因此性能表按 FLOP 计数时是 128 FLOP/clk/SM。
  4. 结果写回各 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。两者是不同关系,不用同一组箭头混画。

PROGRAM HIERARCHY

Grid → Block → Warp → Thread

Warp = 32 Threads
一次 Kernel LaunchGrid
Thread Block 0展开
Warp 0
T0T1T2•••T31
×⌈B/32⌉•••Warp / Block
Warp WThread ×32
× gridDim•••Block / Grid
Block NWarp ×W
B 是一个 Block 的线程数;最后一个 Warp 可以只有部分活跃 Thread,但硬件调度单位仍是 Warp。

上图回答“谁包含谁”;下面回答“运行时谁被交给谁”。这两组层次不是四级一一对齐:Grid → Block → Warp → Thread 是软件组织,GPC → TPC → SM → 执行流水线 是硬件组织,真正稳定的调度契约落在 Block → SM。

SOFTWARE ↔ HARDWARE

CUDA 对象在 A100 上的实际落点

关键边界:Block → 1 SM
CUDA 软件对象运行时关系A100 硬件落点
一次 Kernel LaunchGrid许多彼此独立的 Block
以 Block 为任务分发Grid 本身不塞进某个 GPC 或 SM
整个目标设备A100 · 108 SM
GPC ×7›TPC ×7/8›SM ×2
资源分配 / 驻留单位Thread Block线程、寄存器、Shared Memory
完整交给一个 SM不能把同一 Block 拆给多个 SM
一个被选中的 SMResident Blocks
Block ABlock F资源允许时可同时常驻
指令调度 / 发射单位Warp32 Threads + Active Mask
选择 Ready Warp只在所属 Block 驻留的 SM 内推进
SM 内四个处理分区Warp Scheduler / Dispatch ×4从常驻 Warp 池选择可发射者
程序员看到的最小上下文Thread自己的 ID、寄存器与控制状态
成为当前指令的一个 Lane不是永久占有某一个 Core
由指令类型选择流水线
FP32INT32LD/STTensorSFU
软件对象与硬件对象的对应关系。GPC/TPC 是组织 SM 的物理层次,不是与 Grid/Block 对称的 CUDA 调度层;图中的箭头表示运行时落点,不表示永久绑定。

把这张图读成四句话即可:

  1. Grid 面向整个 CUDA 设备。 A100 的工作分发逻辑把 Grid 中尚未执行的 Block 分派到可用 SM;基础 CUDA 启动语法不能指定 GPC、TPC 或 SM,具体分发次序也不是程序语义的一部分。
  2. GPC/TPC 是硬件分组,不是软件层次的“配对项”。 A100 中 GPC → TPC → SM 最终提供 108 个 SM;普通 Grid 可以使用跨多个 GPC 的 SM,但并不存在 1 Grid = 1 GPC 或 1 Block = 1 TPC 的关系。
  3. Block 是不可拆分的驻留单位。 一个 Block 的全部线程都由同一个 SM 执行;因此它的所有 Warp 也留在该 SM 内。反过来,只要线程数、寄存器、Shared Memory 等资源允许,一个 SM 可同时常驻多个 Block。
  4. 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 RESIDENCY

待执行 Block → 资源准入 → 多个 SM 并行驻留

示例:256 Threads / Block
Grid 的待执行部分Pending Blocks
B432B433B434•••B9999
不保证 Block 的实际先后次序
按每个 SM 的可用资源准入
ThreadsWarpsRegistersShared
当前驻留集合(概念示例)A100 · 108 SM
SM 0B17 · B205 · B9 · B3014 Blocks · 32 Warps
SM 1B88 · B4 · B221 · B3904 Blocks · 32 Warps
•••
SM 107B71 · B6 · B330 · B1284 Blocks · 32 Warps
概念示例,不代表 NVIDIA 公开了具体 Block 分发算法。假设每 Block 256 Thread(8 Warp),且寄存器与 Shared Memory 用量只允许每 SM 常驻 4 个 Block,则一个 SM 的常驻池为 32 Warp;108 个 SM 当前最多容纳 432 个这样的 Block,剩余 Block 等待后续波次。MIG、并发 Kernel 或其他资源限制会改变可用 SM 和常驻量。

A100(Compute Capability 8.0)每个 SM 的硬上限是 2048 Thread、64 Warp、32 Block;但真实常驻 Block 数取各项约束中最紧的一项。可把它粗略理解为:

\[ N_{\text{resident blocks/SM}} \leq \min\left( \left\lfloor\frac{2048}{T_{\text{block}}}\right\rfloor, \left\lfloor\frac{64}{W_{\text{block}}}\right\rfloor, 32, N_{\text{register}}, N_{\text{shared}} \right),\qquad W_{\text{block}}=\left\lceil\frac{T_{\text{block}}}{32}\right\rceil \]

其中 \(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 线程那样保存、恢复上下文。

INSIDE ONE SM

常驻 Warp 池 → Scheduler → 指令对应的执行流水线

4 个处理分区 / SM
Resident ≠ Executing常驻 Warp 池
Warp A0Ready · 下一条 FP32
Warp A1等待 Global Memory
Warp F3Ready · 下一条 LD/ST
Warp F4…等待依赖 / Barrier
选择
Eligible Warp
四个分区各一套Warp Scheduler
+ Dispatch
InstructionActive Mask1111…1011
一次发射一个 Warp 的共同指令
按指令类型
选择管线
每个处理分区Execution Pipelines
FP32 ×16浮点运算INT32 ×16整数 / 地址LD/ST ×8访存Tensor ×1矩阵运算SFU ×4特殊函数
一条 Warp 指令带有最多 32 个活跃 Lane;执行管线数量表示硬件吞吐资源,不表示 Thread 的永久归属。图展开一个处理分区,整个 A100 SM 有四套这样的 Scheduler/Dispatch 与执行资源。

最容易混淆的一点: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 风格线性层中,逻辑运算仍是:

\[ \mathbf{x}_{[1,K]}\,\mathbf{W}^{\mathsf T}_{[K,N]} \longrightarrow \mathbf{y}_{[1,N]}, \qquad M=1,\;K=N=4096 \]

从数学形状看,它是 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 在运行时由上一算子或单独的量化步骤产生:

\[ q^x_k=\operatorname{clip}\!\left(\operatorname{round}_{\text{even}}(x_k/s_x),-128,127\right), \qquad q^W_{o,k}=\operatorname{clip}\!\left(\operatorname{round}_{\text{even}}(W_{o,k}/s^W_o),-128,127\right) \]

真正送进 Tensor Core 的两个乘数都是 INT8;乘积累加到 INT32,完整点积结束后才在 Epilogue 中反量化:

\[ z_o=\sum_{k=0}^{4095}q^x_k q^W_{o,k}\quad(\mathrm{INT32}), \qquad y_o\approx s_x s^W_o z_o\quad(\mathrm{FP16/FP32}) \]

即使按最保守的 \(|q|\le128\) 估算,\(|z_o|\le4096\times128^2=67{,}108{,}864<2^{31}\),本例不会溢出 INT32。若线性层带 Bias,可在反量化后的浮点 Epilogue 中相加;若下一层仍接收 INT8,也可把 Epilogue 改为 Requantize,而不必写出 FP16。

显存中的 qweight[N,K] 按输出通道行主序存放;数学上使用的是它的逻辑转置 [K,N],运行时不需要真的转置整张权重矩阵。

ONE-TOKEN · W8A8 LINEAR

INT8 输入与权重进入 Tensor Core,INT32 结果再反量化

M = 1 · K = N = 4096
Quantized ActivationqA [1,4096] · INT8per-token Scale sₓ · FP32
Quantized WeightqWᵀ [4096,4096] · INT8per-output Scale sᵂ[4096] · FP32
Tensor Core + EpilogueINT32 z → DQ → FP16 yy[o] = sₓ · sᵂ[o] · z[o]
放大一个输出 Tile Block b
qA[0,:]×qW[16b:16b+16,:]ᵀ→y[16b:16b+16]
Grid 含 256 个 Block;每 Block 计算 16 个输出,4 个 Warp 分担 K 维
逻辑上仍是 4096 个 `[1,4096] · [4096]` 点积;Kernel 把相邻 16 个输出组成一个 N Tile。INT8 乘加期间不逐元素反量化,只有 INT32 点积完成后才乘两个 Scale。

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 部分和相加。

#include <cuda_runtime.h>
#include <cuda_fp16.h>
#include <mma.h>

namespace wmma = nvcuda::wmma;
using int8 = signed char;

constexpr int K = 4096;
constexpr int N = 4096;
constexpr int WMMA_M = 16;
constexpr int WMMA_N = 16;
constexpr int WMMA_K = 16;
constexpr int WARPS_PER_BLOCK = 4;
constexpr int THREADS = WARPS_PER_BLOCK * 32;
constexpr int K_PER_WARP = K / WARPS_PER_BLOCK;

__global__ void w8a8_tc_gemv_kernel(
    const int8* __restrict__ qact,       // [K], INT8
    const int8* __restrict__ qweight,    // [N, K], row-major INT8
    float act_scale,                     // per-token s_x
    const float* __restrict__ w_scale,   // [N], per-output s_W
    half* __restrict__ y) {              // [N], FP16

    const int tid = threadIdx.x;
    const int warp = tid >> 5;
    const int lane = tid & 31;
    const int out_base = blockIdx.x * WMMA_N;
    const int k_begin = warp * K_PER_WARP;
    const int k_end = k_begin + K_PER_WARP;

    // 每个 Warp 各有对齐的 A/B Tile 与 INT32 部分结果。
    __shared__ __align__(32) int8 a_tile[WARPS_PER_BLOCK][WMMA_M * WMMA_K];
    __shared__ __align__(32) int8 b_tile[WARPS_PER_BLOCK][WMMA_K * WMMA_N];
    __shared__ __align__(32) int c_tile[WARPS_PER_BLOCK][WMMA_M * WMMA_N];

    wmma::fragment<wmma::matrix_a, WMMA_M, WMMA_N, WMMA_K,
                   int8, wmma::row_major> a_frag;
    wmma::fragment<wmma::matrix_b, WMMA_M, WMMA_N, WMMA_K,
                   int8, wmma::col_major> b_frag;
    wmma::fragment<wmma::accumulator, WMMA_M, WMMA_N, WMMA_K,
                   int> acc_frag;
    wmma::fill_fragment(acc_frag, 0);

    // 每个 Warp 沿自己的 K 区间循环 1024 / 16 = 64 次。
    for (int k0 = k_begin; k0 < k_end; k0 += WMMA_K) {
        for (int i = lane; i < WMMA_M * WMMA_K; i += 32) {
            // 逻辑 M=1:第 0 行有效,其余 15 行补 0。
            a_tile[warp][i] = (i < WMMA_K) ? qact[k0 + i] : int8{0};

            // B 逻辑形状为 [K,N];qweight[N,K] 行主序恰好是其列主序存储。
            const int n = i / WMMA_K;
            const int kk = i % WMMA_K;
            b_tile[warp][i] = qweight[(out_base + n) * K + k0 + kk];
        }
        __syncwarp();

        wmma::load_matrix_sync(a_frag, a_tile[warp], WMMA_K);
        wmma::load_matrix_sync(b_frag, b_tile[warp], WMMA_K);
        wmma::mma_sync(acc_frag, a_frag, b_frag, acc_frag); // s8 × s8 → s32
        __syncwarp(); // 确保 Shared Tile 不会在仍被读取时覆写
    }

    wmma::store_matrix_sync(
        c_tile[warp], acc_frag, WMMA_N, wmma::mem_row_major);
    __syncthreads();

    // A Tile 只有第 0 行有效;前 16 个 Thread 合并四个 K Slice 并反量化。
    if (tid < WMMA_N) {
        int z = 0;
        #pragma unroll
        for (int w = 0; w < WARPS_PER_BLOCK; ++w) {
            z += c_tile[w][tid];
        }
        const int o = out_base + tid;
        y[o] = __float2half(static_cast<float>(z) * act_scale * w_scale[o]);
    }
}

void launch_w8a8_tc_gemv(
    const int8* qact,
    const int8* qweight,
    float act_scale,
    const float* w_scale,
    half* y,
    cudaStream_t stream) {
    // qact 与 act_scale 已由上游 Quantize / Requantize 步骤产生。
    w8a8_tc_gemv_kernel<<<N / WMMA_N, THREADS, 0, stream>>>(
        qact, qweight, act_scale, w_scale, y);
}

这段代码与编程模型的对应关系是:

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
CUDA COMPILATION TRAJECTORY

WMMA 从 CUDA C++ 降到 PTX,再成为 A100 的 IMMA

Target · sm_80
① Sourcew8a8_tc_gemv.cu
Host Launch CodeCUDA WMMA Device Code
nvcc 分离
② Two PathsHost / Device
Host Path合成标准 C++
保留 Kernel Launch Stub
Device Path识别 WMMA
生成 Warp 级矩阵操作
分别编译
③ Artifacts虚拟与目标 ISA
Host ObjectHost Compiler
PTX
wmma.mma · s8→s32
→ ptxas →Cubin
sm_80 · IMMA
PTX / Cubin → Fatbinary → 嵌入 Host Object
Link + Load
④ RuntimeExecutable / Library
CUDA Driver优先选择兼容 Cubin
只有 PTX 时 JIT
A100 Tensor Core执行 INT8 IMMA
CUDA C++ 的 `wmma::mma_sync` 表达 Warp 级矩阵语义;PTX 可见 `wmma.mma.sync...s32.s8.s8.s32`,`ptxas` 再把它展开为一个或多个适合 A100 的 IMMA-family SASS 指令。Host 编译与 Fatbinary 装载路径不变。

要抓住三个边界:

  1. WMMA 是 CUDA C++ 的 Warp 级矩阵 API。 Fragment 是分布在 32 个 Lane 寄存器中的协作对象,不是某一个 Thread 的普通数组。
  2. PTX 是虚拟 ISA。 本例应能看到 wmma.load、wmma.mma.sync...s32.s8.s8.s32、wmma.store 等语义;A100 不直接执行 PTX 文本。
  3. 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 仍可能带来一次性准备开销。

W8A8 LAUNCH → GRID COMPLETION

一次 INT8 Tensor Core Launch 的运行时执行时间线

256 Blocks · 4 Warps / Block
1
Upstream Quantize准备 `qA[1,4096]` 与 `sₓ`Weight 已离线量化,`qW` 与 `sᵂ` 随权重常驻显存;Activation 由当前 token 动态量化或由前一 Epilogue 直接写出 INT8。
Q / Requantize
2
Host + Driver选择 `sm_80` 代码并发起异步 Launch<<<256, 128, 0, stream>>> 产生 256 个相互独立的 Block。
Software
3
Work DistributionBlock b 获得某个 SM 的资源空位CUDA 代码不指定 GPC、TPC 或 SM;完整 Block 被准入一个 SM,并分配寄存器与约 6 KiB Shared Memory。
GPC → TPC → SM
4
Selected SM4 个 Warp 分担同一 N Tile 的四段 KWarp 0–3 分别覆盖 0–1023、1024–2047、2048–3071、3072–4095。
Split-K
5
64 K-Tiles / Warp合并 Global Load → Shared Tile → Fragment每轮搬入 16 个 K 元素;A Tile 只有第 0 行有效,B Tile 覆盖连续 16 个输出通道。
Repeat ×64
6
Warp-Level Matrix Operation32 个 Lane 共同发射 INT8 WMMATensor Core 对 `m16n16k16` 做 `s8 × s8 → s32`;INT32 Fragment 在 K 循环中持续累加。
IMMA
7
Block Epilogue四份 INT32 K Slice 求和并反量化Barrier 后,Thread 0–15 计算 `y[o] = float(z[o]) × sₓ × sᵂ[o]`,再写回 FP16。
IADD + DQ + STG
8
Grid Completion最后一个 Block 完成所有 256 个 N Tile 完成后,Stream 中的后续工作才能消费完整的 y[1,4096]。
Observable
时间线按逻辑事件绘制,不按时钟比例。步骤 3–7 会在多个 SM 上重叠;一个 Block 和它的四个 Warp 始终留在同一个 SM,整个 Grid 才会跨越多个 GPC/SM。

下面冻结一个 Warp 的一次 K-Tile 迭代,再接上 Block Epilogue:

ONE RESIDENT BLOCK · ONE WMMA ITERATION

Lane 协作搬 Tile、Tensor Core 累加、Block 级 Split-K 合并

128 Threads · 4 Warps · 16 Outputs
① Lane Cooperation分工搬运 Tile
L0
part
L1
part
L2
part
•••L31
part
Lane 不与输出元素固定绑定
② LD/ST + SharedINT8 A / B Tile
qA · 16 BqW · 256 BShared · aligned
A 的 15 个 Padding Row 写 0
③ Fragment + Tensor PipeWMMA · INT8 m16n16k16 · s8 × s8acc_frag += A × B · s32 32 Lane 共同拥有 Fragment
④ Block Epilogue4 K Slices → DQ
INT32 ×4→× sₓsᵂ→FP16
Thread 0–15 写 16 个 y[o]
每个 Warp 重复 64 次 Tile 搬运与 WMMA。Shared Memory 解决对齐与矩阵布局,Fragment 留在寄存器中;四个 Warp 沿 K 维产生部分和,只有 Block Barrier 之后才能安全合并。

代码中的关键构造可落到如下硬件层次:

代码构造 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 乘加。一次 m16n16k16 WMMA 覆盖 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_80 Cubin → 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. 校验问题

读完后应能独立回答:

  1. 完整 GA100 的 128 SM 与 A100 的 108 SM 为什么不能混写?
  2. 为什么不能把 Grid → Block → Warp → Thread 与 GPC → TPC → SM → CUDA Core 逐级一一对应?
  3. 一个大 Grid 为什么会分多个波次执行?Block、Warp 与 SM 的驻留关系分别是什么?
  4. 为什么 32 Threads / Warp 不代表每个 Thread 永久对应一个 CUDA Core?
  5. 192 KB Unified L1/Shared 为什么不代表 Block 可申请 192 KB Shared Memory?
  6. Local Memory 为什么可能比 Shared Memory 慢很多?
  7. 分支发散、访存不合并和低 Occupancy 分别损失了什么资源?
  8. 80 GB SXM 的 HBM 带宽、NVLink 带宽与 PCIe 带宽各描述哪条路径?
  9. 对称 W8A8 中,为什么 INT8 × INT8 可以先完整累加到 INT32,再统一乘 sₓ × sᵂ[o] 反量化?
  10. 教学 Kernel 中,Block、Warp 与 Lane 分别负责 N Tile、K Slice 和哪一类协作?
  11. 为什么不能把 WMMA Fragment 的某个输出元素永久对应到某个 Thread?
  12. wmma::mma_sync、PTX wmma.mma...s32.s8.s8.s32 与 A100 IMMA-family SASS 分别处在哪一层?
  13. 为什么 batch=1 即使使用 INT8 Tensor Core,仍会同时面临 M Tile 利用率低和 HBM 权重带宽瓶颈?

参考资料