10 > 数值格式与 Tensor Core
GPU 处理的每个数,都用固定数量的位来存储。这一课讲这些位是怎样分配的,从 FP64 一直讲到 4 位的 NVFP4,逐个看今天在用的格式,再讲 Tensor Core。它们就在普通的 CUDA 核心旁边,把更少的位变成快得多的速度。
浮点数是怎样存储的
浮点数的存法就像以 2 为底的科学计数法。它由三部分组成。
- 符号位占 1 位,0 表示正数,1 表示负数。
- 指数表示这个数有多大,也就是 2 的几次方。它带一个偏置存储,这样负的次方不需要自己的符号位也能存下。
- 尾数是这个数的各位数字,决定它有多精确。正规数总是以 "1." 开头,所以这个 1 不存。它叫隐藏位。
value = (−1)^sign × 2^(exponent − bias) × 1.mantissa
算一个例子,把 6.5 存成 FP32,它有 1 个符号位、8 个指数位、23 个尾数位,偏置是 127。
- 6.5 的二进制是 110.1,也就是 1.101 × 2^2。
- 符号是正数,所以是 0。
- 指数是 2 + 127 = 129,二进制是 10000001。
- 尾数是 "1." 后面的数字,也就是 101 后面跟 20 个 0。
- 存下的位是
0 10000001 10100000000000000000000。验算一下,2^(129 − 127) × 1.625 = 4 × 1.625 = 6.5。
6.5 能精确存下,是因为 0.5 是 2 的幂。大多数十进制数都不行。
[!NOTE] 0.1 没有精确的二进制形式,就像 1/3 没有精确的十进制形式一样。FP32 存的是它能表示的最近的值 0.100000001490116……每种格式都会舍入,尾数位越少,舍入误差越大。
范围与精度
一种格式的位要分给两件事。
- 指数位越多,范围越大,也就是数能有多大、多小。
- 尾数位越多,精度越高,也就是相邻两个数挨得多近。
FP16 和 BF16 都用 16 位,但分法不同。FP16 有 5 个指数位和 10 个尾数位。它的最大值是 65,504,所以 70,000 会变成无穷大。BF16 有 8 个指数位,范围和 FP32 一样(最大约 3.4 × 10^38),但只有 7 个尾数位。所以 1 + 1/512 = 1.001953125 在 FP16 里是精确的,而 BF16 会把它舍入成 1,因为它在 1 附近的步长是 1/128。
各种格式
| 格式 | 位数(符号 / 指数 / 尾数) | 最大值 | 主要用途 |
|---|---|---|---|
| FP64 | 1 / 11 / 52 | ≈ 1.8 × 10^308 | 科学计算、仿真 |
| FP32 | 1 / 8 / 23 | ≈ 3.4 × 10^38 | CUDA 核心上的一般 GPU 计算 |
| TF32 | 1 / 8 / 10 | ≈ 3.4 × 10^38 | Tensor Core 上的 FP32 矩阵运算 |
| FP16 | 1 / 5 / 10 | 65,504 | 推理、较早的训练 |
| BF16 | 1 / 8 / 7 | ≈ 3.4 × 10^38 | 训练 |
| FP8 E4M3 | 1 / 4 / 3 | 448 | 推理、训练的前向传播 |
| FP8 E5M2 | 1 / 5 / 2 | 57,344 | 训练中的梯度 |
| FP6 E2M3 / E3M2 | 1 / 2 / 3 或 1 / 3 / 2 | 7.5 或 28 | 配合块缩放做推理 |
| FP4 E2M1 | 1 / 2 / 1 | 6 | 配合块缩放做推理 |
| INT8 | 8 位整数 | 127(从 −128 起) | 量化推理 |
E4M3 和 E5M2 这些名字只是在数位数,4 个指数位和 3 个尾数位,或者 5 个和 2 个。TF32 就是把尾数截到 10 位的 FP32。它仍然放在 32 位寄存器里,但 Tensor Core 只用其中 19 位。INT8 完全没有指数,它是 256 个等间距的整数,再乘以按每个张量或每个通道选定的缩放因子。
选一种格式,输入一个值。格子显示符号位、指数位和尾数位,表格显示实际存下的值和舍入误差有多大。
下面是各种格式的用处。
- 科学计算和仿真(天气、化学、物理)需要 FP64,因为微小的误差会在几百万步里不断放大。
- 训练 AI 模型主要用 BF16,越来越多地用 FP8,权重的主副本和求和仍保留 FP32。训练需要范围,因为梯度可能非常小。
- 推理,也就是运行训练好的模型,是最小格式的天下,比如 FP8、INT8,现在还有 FP4。训练好的模型比训练过程更能容忍舍入。
块缩放与 NVFP4
FP4 E2M1 自己只能表示 15 个值,即 0 以及 ±0.5、±1、±1.5、±2、±3、±4、±6。单独用远远不够。NVFP4 用共享缩放因子解决这个问题。每 16 个值组成一块,共用一个 FP8 E4M3 数,每个值存成"缩放因子 × 它的 4 位元素"。缩放因子的选法是让块里最大的值落在 6 附近,也就是 FP4 的上限。另外还有一个 FP32 缩放因子覆盖整个张量。算一个例子,如果一块里最大的值是 0.1,缩放因子就是 0.1 / 6 ≈ 0.0167,E4M3 把它存成 0.017578125,于是 0.1 存成 0.017578125 × 6 = 0.10546875。普通 FP4 会把 0.1 舍入成 0。代价是每 16 个值多 8 位,即 4 + 8 / 16 = 每个值 4.5 位。MXFP4 是 OCP 的开放格式,它改用 32 个值一块,缩放因子是 2 的幂。
算一算 700 亿参数的模型
模型权重占用的内存 = 参数个数 × 每个参数的字节数。下面是 700 亿个参数的情况。
- FP16 或 BF16 每个参数 2 字节,70 × 10^9 × 2 = 140 GB。
- FP8 每个参数 1 字节,70 × 10^9 × 1 = 70 GB。
- FP4 每个参数半个字节,70 × 10^9 × 0.5 = 35 GB。加上 NVFP4 的缩放因子是 70 × 10^9 × 4.5 / 8 ≈ 39.4 GB。
L40S 有 48 GB。用 FP16,权重至少要 3 块 L40S(140 / 48 ≈ 2.9)。用 FP8 要 2 块(70 / 48 ≈ 1.5)。用 NVFP4,权重一块就放得下。但 L40S 是 Ada Lovelace 架构,它有 FP8 Tensor Core,没有 FP4 的。4 位权重要先转换成更宽的格式才能参与计算。Blackwell GPU,比如 B200 或 RTX 5090,可以直接做 FP4 乘法。
[!WARNING] 这些数字只是权重。运行模型还需要内存放激活值和 KV 缓存,也就是之前各个 token 的键和值,它会随批大小和提示长度增长。
Tensor Core 是什么
CUDA 核心对单个数做一次 FMA,也就是 a × b + c。Tensor Core 则对小块矩阵做矩阵乘累加。
D = A × B + C
A 和 B 是窄格式(比如 FP16 或 FP8)的小矩阵。C 和 D 通常保持更宽的格式,常常是 FP32,这样求和不丢精度。最早的 Tensor Core 出现在 Volta 上,每个每时钟做一次 4 × 4 × 4 的矩阵乘累加,即 4 × 4 × 4 = 64 次 FMA,而一个 CUDA 核心只做 1 次。在 CUDA 里,由一个线程束(见第 08 课)一起发出这条指令。一条形状为 m16n8k16 的 FP16 mma 指令把 16 × 16 的块乘以 16 × 8 的块,整个线程束共 16 × 8 × 16 = 2,048 次乘加。
窄格式正是 Tensor Core 扩展得这么好的原因。位数更少的乘法器更小,搬运的数据也更少,同一块芯片就能放下更多。在 L40S 上,从 FP16 换到 FP8,峰值翻倍,下面的数据手册表格就能看到。
哪个架构加入了哪种格式
每一代 Tensor Core 都加入了新格式。CC 一栏的计算能力数字来自第 05 课。
| 架构 | CC | Tensor Core 代数 | 新格式 |
|---|---|---|---|
| Volta(2017) | 7.0 | 第 1 代 | FP16 |
| Turing(2018) | 7.5 | 第 2 代 | INT8、INT4 |
| Ampere(2020) | 8.0、8.6 | 第 3 代 | TF32、BF16、FP64、2:4 稀疏 |
| Hopper(2022) | 9.0 | 第 4 代 | FP8 |
| Ada Lovelace(2022) | 8.9 | 第 4 代 | FP8 |
| Blackwell(2024) | 10.x、11.0、12.x | 第 5 代 | FP6、FP4、NVFP4、MXFP8/6/4 |
2:4 稀疏的意思是每 4 个权重里至少有 2 个是 0。Tensor Core 跳过这些 0,所以对按这种方式剪枝过的权重,它用一半时间完成同样的工作。
L40S 上的 Tensor Core 与 CUDA 核心
NVIDIA 的 L40S 数据手册给出了这些峰值。稠密是正常情况,稀疏需要 2:4 稀疏的权重。
| 单元和格式 | 稠密 | 带稀疏 |
|---|---|---|
| CUDA 核心,FP32 | 91.6 TFLOPS | 无 |
| Tensor Core,TF32 | 183 TFLOPS | 366 TFLOPS |
| Tensor Core,FP16 / BF16 | 362 TFLOPS | 733 TFLOPS |
| Tensor Core,FP8 | 733 TFLOPS | 1,466 TFLOPS |
| Tensor Core,INT8 | 733 TOPS | 1,466 TOPS |
Tensor Core 上的稠密 FP16 是 CUDA 核心 FP32 峰值的 362 / 91.6 ≈ 4 倍,稠密 FP8 是 733 / 91.6 ≈ 8 倍。TFLOPS 和 TOPS 都是峰值,其中 TOPS 计的是整数运算。
[!WARNING] 数据手册常常先写稀疏的数字,只用一个小星号标出。H100 SXM 标的 FP8 是 3,958 TFLOPS,这是带稀疏的,稠密是 1,979。永远拿稠密和稠密比。
在第 09 课的屋顶线模型里,这些都是更高的计算屋顶。带宽 864 GB/s 时,FP8 屋顶把脊点推到 733,000 / 864 ≈ 848 FLOP 每字节。位数少在显存这一侧也有好处。一个 FP8 值占 1 字节而不是 4 字节,同一个矩阵要搬运的字节数少 4 倍。
这对 CUDA 意味着什么
- 普通 C++ 的
float和double运算跑在普通的 FP32 和 FP64 单元上,不在 Tensor Core 上。窄格式有自己的类型,分别是cuda_fp16.h里的__half,cuda_bf16.h里的__nv_bfloat16,cuda_fp8.h里的__nv_fp8_e4m3,cuda_fp4.h里的__nv_fp4_e2m1。 - 核函数可以通过
mma.h里的 WMMA API、PTX 的mma指令,或者 cuBLAS、CUTLASS 这类替你完成的库来使用 Tensor Core。 - 这种格式必须在你的 GPU 的计算能力上存在。在 L40S 上,用
-arch=sm_89编译才能得到 FP8 Tensor Core 指令,FP4 指令则需要 Blackwell 目标。 - 累加用的格式永远要比乘法用的更宽。用 FP16 去累加成千上万个 FP16 乘积,很快就会丢掉有效数字。
小结
浮点数由符号、决定范围的指数和决定精度的尾数组成。位数越少,内存越省、搬运越少、Tensor Core 吞吐越高,但舍入误差越大。FP64 服务科学计算,BF16 和 FP8 服务训练,FP8、INT8 和 NVFP4 服务推理,块缩放让 4 位也能用。Tensor Core 在小块上计算 D = A × B + C,从 Volta 起每一代都加入了更窄的格式。
术语表
- GPU(Graphics Processing Unit,图形处理器):这些课讲的处理器,由许多并行工作的核心组成。
- CUDA(Compute Unified Device Architecture,统一计算设备架构):NVIDIA 用来编写在其 GPU 上运行的程序的平台。
- 浮点数(floating-point number):存成符号、指数和尾数的数,就像以 2 为底的科学计数法。
- 符号位(sign):表示一个数是正(0)还是负(1)的那一位。
- 指数(exponent):存放 2 的次方的那些位,因此决定格式的范围。
- 偏置(bias):加在指数上的固定数,让负的次方不用符号也能存下,FP32 是 127,FP16 是 15。
- 尾数(mantissa):存放数字各位的那些位,因此决定精度。
- 隐藏位(hidden bit):正规数开头的那个 1,它不被存储。
- 范围(range):一种格式里的数能有多大、多小。
- 精度(precision):一种格式里相邻两个数挨得多近,由尾数位决定。
- FP64 / FP32 / FP16(64-, 32- and 16-bit floating point):64 位、32 位和 16 位浮点格式。
- BF16(bfloat16,brain floating point,脑浮点):拥有 FP32 的范围、7 个尾数位的 16 位格式。
- TF32(TensorFloat-32):尾数为 10 位的 FP32,从 Ampere 起 Tensor Core 用它做 FP32 矩阵运算。
- FP8 / E4M3 / E5M2:8 位浮点。E4M3 有 4 个指数位和 3 个尾数位,E5M2 有 5 个和 2 个。
- FP6 / FP4(6-bit / 4-bit floating point):6 位和 4 位浮点,在 Blackwell 上配合块缩放使用。
- NVFP4(NVIDIA 4-bit floating point,NVIDIA 4 位浮点):FP4 E2M1 值,每 16 个一块共用一个 FP8 E4M3 缩放因子,每个张量再有一个 FP32 缩放因子。
- MXFP4(microscaling FP4,微缩放 FP4):OCP(Open Compute Project,开放计算项目)的开放 4 位格式,32 个值一块,缩放因子是 2 的幂。
- OCP(Open Compute Project,开放计算项目):发布开放硬件标准的行业组织,MX 格式也出自这里。
- 块缩放 / 缩放因子(block scaling / scale factor):每块值共用的一个数,块里每个值都要乘以它。
- INT8(8-bit integer,8 位整数):从 −128 到 127 的整数,推理时配合缩放因子使用。
- AI(artificial intelligence,人工智能):从数据中学习的软件,比如语言模型。
- 训练(training):用数据教 AI 模型,靠梯度调整它的权重。
- 推理(inference):运行训练好的模型来得到答案。
- 参数 / 权重(parameter / weights):模型学到的数,个数乘以每个数的字节数就是它们占的内存。
- KV 缓存(key-value cache,键值缓存):语言模型在推理时保存在内存里的之前各个 token 的键和值。
- Tensor Core:用一条指令在小矩阵块上计算 D = A × B + C 的单元。
- CUDA 核心(CUDA core):普通的 GPU 核心,每时钟做一次 FP32 FMA。
- FMA(fused multiply-add,融合乘加):计算 a × b + c 的一条指令。
- 线程束(warp):一起发出指令的 32 个线程,Tensor Core 指令由整个线程束发出。
- 2:4 稀疏(2:4 sparsity):每 4 个权重里至少有 2 个 0,Tensor Core 可以跳过它们,吞吐最多提高到 2 倍。
- 稠密 / 稀疏(dense / sparse):不带稀疏的峰值,或者需要 2:4 稀疏权重的翻倍峰值。
- FLOP(floating-point operation,浮点运算):对浮点数做一次加、减、乘或除。
- TFLOPS / TOPS(tera floating-point operations per second / tera operations per second):每秒万亿次浮点运算,或每秒万亿次整数运算。
- 计算能力(compute capability,CC):NVIDIA 用来表示 GPU 硬件支持哪些功能的版本号。
- 脊点(ridge point):峰值 FLOPS 除以显存带宽,算术强度低于它的核函数受显存限制。
- WMMA(Warp Matrix Multiply-Accumulate,线程束矩阵乘累加):
mma.h里的 CUDA C++ API,用来在核函数里使用 Tensor Core。 - PTX(Parallel Thread Execution,并行线程执行):CUDA 代码编译成的 NVIDIA 底层中间语言。