数值精度与表示范围:从 FP32 到 INT2 的数据类型速查
深度学习的训练与推理会用到很多种数值类型,从高精度的 FP32(float32)一路到低比特量化的 INT2。本文用最直白的语言回答三个问题:每种类型能表示多大的范围?能保留多少精度?这些最大值、最小值是怎么算出来的? 看懂它们,是理解模型量化、算子优化和避免数值溢出(比如 Softmax 溢出)的第一步。
1. 浮点数的表示原理
IEEE 754 浮点数用三个字段表示一个数:
这个公式就是计算机里的"科学计数法":我们熟悉的科学计数法是 1.23 × 10⁵——"有效数字 × 10 的幂次";浮点数只不过把底数从 10 换成了 2,把 1.23 换成了 1.f。其中 1.f 决定精度(有效数字),指数
- 符号位
(1 位):决定正负。 - 指数位
:决定范围——能表示的最大值和最小绝对值。 - 尾数位
:决定精度——十进制有效数字的个数。 - bias:指数偏移,使指数可取负值。
三个字段按位排列,各自对"范围"与"精度"的决定作用如下图所示:
两个关键结论:
- 指数位越多,范围越大。例如 FP32 与 FP16 各有 8 位与 5 位指数,前者最大到
量级,后者仅到 65504。 - 尾数位越多,精度越高。十进制有效数字约等于
——换句话说,尾数每多约 3.3 位,十进制有效数字就多 1 位(因为 )。
另外区分两种数值:
- 正规数(normal):完整精度,尾数隐含前导 1。
- 非正规数(subnormal):指数域全 0,精度略降,但能填补正规数最小值到 0 之间的空隙。
沿正半轴看,各类数值的分布与指数域的编码一一对应:
衡量精度最常用的指标是机器精度(machine epsilon)ε,即 1.0 与下一个可表示数之间的间隔:
它代表相对精度——任何实数的相对表示误差一般不超过
2. 最大最小值是怎么算出来的
这一节回答一个问题:表格里的最大值、最小值,到底是怎么从"位宽"一步步算出来的。 记不住公式没关系,先记住一句话:
浮点数 = 精度 × 规模:尾数决定"数字长什么样",指数决定"数字有多大"。最大值,就是把"精度拉满"和"规模拉满"相乘。
2.1 浮点数的最大值:65504、3.40e38 怎么来的
回到浮点数的公式:
它由两部分拼成:
- 1.f(尾数)——决定"数字本身"。尾数有几位,就能写出几位精度的数字,FP32 的 23 位尾数就是这样来的。
- 2^(e−bias)(指数)——决定"放大多少倍"。指数大,数就大;指数小,数就小。
求最大值的思路非常直白:尾数取最大(所有位全 1),指数取最大(能用的最大指数),两者相乘。
尾数全 1 是多大?二进制里 1.111…1 约等于 11111111)必须留给 ±∞ 和 NaN,不能用来表示普通数字,所以能用的最大指数恰好是 bias(FP32 是 127,FP16 是 15)。于是最大值公式就是:
一个个代入,结果和总览表完全一致:
| 类型 | 最大指数 | 最大尾数 | 最大值 |
|---|---|---|---|
| FP32 | 127 | ≈ 3.40 × 10³⁸ | |
| FP16 | 15 | 1.999 × 32768 = 65504 | |
| BF16 | 127 | ≈ 3.39 × 10³⁸ | |
| FP8 E5M2 | 15 | 1.75 × 32768 = 57344 | |
| FP8 E4M3 | 8 | 1.75(尾数全 1 留 NaN) | 1.75 × 2⁸ = 448 |
| FP4 E2M1 | 2 | 1.5 × 2² = 6 |
"精度拉满 × 规模拉满"用图表示就是:
不过有两个例外,因为硬件对"保留位"的处理不一样,不能直接套"尾数全 1":
- FP8 E4M3:这个格式没有 ±∞,所以指数域 1111 还能拿来用(相当于 E = 8);但尾数 111 留给了 NaN,最大尾数只能到
1.110= 1.75。于是最大值 = 1.75 × 2⁸ = 448。 - FP4 E2M1:bias = 1,指数域 11 相当于 E = 2,最大尾数
1.1= 1.5。于是最大值 = 1.5 × 2² = 6。
2.2 浮点数的最小正数:能小到什么程度?
求最小值和求最大值正好相反:指数取最小,尾数也取最小。
先把每种类型的"最小正数"都列出来,再看它们怎么来的:
| 类型 | 最小正正规数(指数域 = 1) | 最小正非正规数(指数域 = 0) | 非正规数还能再小 |
|---|---|---|---|
| FP32 | |||
| FP16 | |||
| BF16 | |||
| FP8 E4M3 | |||
| FP8 E5M2 | |||
| FP4 E2M1 |
这两列数字其实只差一个动作:
- 最小正正规数:把指数域调到 1(正规数允许的最小指数),尾数取 0,得到
。FP32 就是 。 - 最小正非正规数:正规数已经小到极限了,还想更小怎么办?答案是牺牲一点精度换更小的数——把指数域全设 0,得到
,相当于在正规数基础上再小 倍。FP32 就是 。
打个比方:正规数像"必须写成 1.xxx"的科学计数法,指数已经小到不能再小;非正规数就是把 1.xxx 改成 0.00xxx,用丢精度换来更小的数。这正是第 1 节数轴图上"非正规数填补 0 与最小正规数之间的空隙"的意思。
2.3 整数类型的范围:没有指数,全靠位宽
整数比浮点简单得多——没有指数,每一位都在"数数",所以范围完全由位宽决定:
- 有符号 N 位:最高位当正负号,剩下 N−1 位用来数数,最多数到
,所以范围是 。 - 无符号 N 位:N 位全部用来数数,范围是
。
位分配示意:
用 INT8 举个例子:8 位里拿 1 位当符号,剩 7 位数数,最多数到 127,所以范围是 [−128, 127];UINT8 的 8 位全用来数数,最多到 255。
整数还有一个浮点做不到的好处:范围内的每个整数都能精确表示,步长永远是 1——没有"1.9 和 2.0 之间还有别的数"这种事。这也是为什么数字超过 2048 之后,FP16 反而数不出每一个整数了。
3. 浮点类型一览
先用一张表把 6 种浮点类型的全貌看全,再逐类说明它们在深度学习里各自的用武之地。
3.1 总览表
| 类型 | 位宽 | 布局(符号/指数/尾数) | 最大正数 | 最小正正规数 | 最小正非正规数 | 十进制有效数字 |
|---|---|---|---|---|---|---|
| FP32(单精度) | 32 | 1 + 8 + 23 | ≈ 3.40 × 10³⁸ | ≈ 1.18 × 10⁻³⁸ | ≈ 1.40 × 10⁻⁴⁵ | ~7 位 |
| FP16(半精度) | 16 | 1 + 5 + 10 | 65504 | ≈ 6.10 × 10⁻⁵ | ≈ 5.96 × 10⁻⁸ | ~3 位 |
| BF16(bfloat16) | 16 | 1 + 8 + 7 | ≈ 3.39 × 10³⁸ | ≈ 1.18 × 10⁻³⁸ | ≈ 9.18 × 10⁻⁴¹ | ~2 位 |
| FP8 E4M3 | 8 | 1 + 4 + 3 | 448 | 0.015625 | ≈ 1.95 × 10⁻³ | ~1 位 |
| FP8 E5M2 | 8 | 1 + 5 + 2 | 57344 | ≈ 6.10 × 10⁻⁵ | ≈ 1.53 × 10⁻⁵ | ~1 位 |
| FP4 E2M1 | 4 | 1 + 2 + 1 | 6 | 1.0 | 0.5 | <1 位 |
小提示:有符号类型(无论浮点还是整数)的最负值 = 最大正数取负——FP16 是 −65504、INT8 是 −128;无符号类型最小值恒为 0。
3.2 各类型要点
FP32(float32):标准的单精度浮点,约 7 位十进制有效数字,最大约 3.4 × 10³⁸。主权重、梯度累加等对精度要求最高的场合使用。
FP16(float16):Tensor Core 加速的常用半精度,约 3 位有效数字。最大只有 65504——这就是 Softmax 需要"安全 Softmax"的原因:
BF16(bfloat16):与 FP32 相同的 8 位指数(因此范围与 FP32 一致),但尾数仅 7 位(约 2 位有效数字)。混合精度训练的主流选择:范围无忧、显存减半,代价是精度降低。
FP8:8 位浮点有两种硬件变体:
- E4M3(4 指数 + 3 尾数):最大 448,精度略高、范围小,适合权重与前向激活。
- E5M2(5 指数 + 2 尾数):最大 57344,范围大、精度低,适合反向传播的梯度。
FP4(E2M1):4 位浮点(1 符号 + 2 指数 + 1 尾数,bias 1),共 16 种位模式(±0 重合,实际 15 个不同数值),正数取值集为
4. 精度详解:机器精度与有效数字
衡量精度主要看两个视角:机器精度 ε(相对误差的刻度,ε 越小数值越"密"、越精确)和十进制有效数字(最直观的"大概能信几位")。下面逐个类型算出来。
4.1 各类型精度明细
| 类型 | 尾数位 | 机器精度 ε | 十进制有效数字 | 相对误差上限(≈ ε/2) |
|---|---|---|---|---|
| FP32 | 23 | ≈ 1.19 × 10⁻⁷ | ~7 位 | ≈ 6 × 10⁻⁸ |
| FP16 | 10 | ≈ 9.77 × 10⁻⁴ | ~3 位 | ≈ 5 × 10⁻⁴ |
| BF16 | 7 | ≈ 7.81 × 10⁻³ | ~2 位 | ≈ 4 × 10⁻³ |
| FP8 E4M3 | 3 | 0.125 | ~1 位 | ≈ 6 × 10⁻² |
| FP8 E5M2 | 2 | 0.25 | ~1 位 | 0.125 |
| FP4 E2M1 | 1 | 0.5 | <1 位 | 0.25 |
机器精度
4.2 绝对精度随数值增大而变差
先记住一个反直觉的结论:相对精度固定(由 ε 决定),但绝对精度会随数值变大而变差。 打个比方:就像一把刻度为"总长度的 0.1%"的尺子——量 1 米时误差只有 1 毫米,量 1 公里时误差就变成 1 米。数越大,相邻两个可表示数之间的"缝隙"越大。
对位于区间
具体例子:
- FP16 在 1.0 附近:相邻可表示数的间隙约
(约 0.1% 的相对精度);但在最大值 65504 附近,间隙达 ,连整数都无法一一表示。 - FP16 能精确表示的最大整数是 2048(
),超过后开始出现"空洞"。 - FP32 在最大值约
附近:间隙高达约 ——大数做加法时,较小的增量会直接丢失。
4.3 整数类型的精度
整数类型的"精度"即步长,恒为 1:在各自范围内,每一个整数都能被精确表示,不存在舍入误差。这是相对浮点的一大优势——例如 INT8 在 −128~127 内绝对精确,而 FP16 在绝对值超过 2048 时反而做不到。更大的位宽(INT16/INT32/INT64)只是扩展表示范围,步长依然是 1,精度不变。
5. 整数类型
下表汇总各整数类型的完整范围。整数没有指数、每一位都在"数数",所以范围只由位宽决定(推导见第 2.3 节):
| 类型 | 位宽 | 有符号范围 | 无符号范围 | 步长 | 十进制有效数字 |
|---|---|---|---|---|---|
| INT64 | 64 | −9.2 × 10¹⁸ ~ 9.2 × 10¹⁸ | 0 ~ 1.8 × 10¹⁹ | 1 | ~19 位 |
| INT32 | 32 | −2.1 × 10⁹ ~ 2.1 × 10⁹ | 0 ~ 4.3 × 10⁹ | 1 | ~10 位 |
| INT16 | 16 | −32768 ~ 32767 | 0 ~ 65535 | 1 | ~5 位 |
| INT8 | 8 | −128 ~ 127 | 0 ~ 255 | 1 | ~2 位 |
| INT4 | 4 | −8 ~ 7 | 0 ~ 15 | 1 | ~1 位 |
| INT2 | 2 | −2 ~ 1 | 0 ~ 3 | 1 | <1 位 |
- INT64:需要超大整数(如超长上下文、超大矩阵的元素总数)时使用,有符号范围 −9223372036854775808 ~ 9223372036854775807。注意 GPU 上 INT64 算术比 INT32 慢得多,能不用就不用。
- INT32:CPU/GPU 上默认的索引与计数类型(CUDA 的
int、Python 的int),有符号范围 −2147483648 ~ 2147483647,足以覆盖绝大多数序列长度与矩阵维度。 - INT8:W8A8 权重量化的主流,配合
scale/zero_point把浮点权重映射到整数网格。 - INT4:W4A16 等低比特量化(如 GPTQ、AWQ)常用,只有 16 个取值(约 1 位十进制有效数字)。
- INT2:极端低比特研究(双比特/三值量化),仅有 4 个取值,精度损失大,通常只在特定场景探索。
- UINT(无符号):UINT8 0~255、UINT4 0~15、UINT2 0~3,通常配合非对称量化(zero-point)使用。
FP4 与 INT4 的关键区别:同为 4 位,INT4 的取值均匀分布(−8 ~ 7),适合分布接近均匀的权重;FP4 的取值按指数分布(0、±0.5、±1、±1.5、±2、±3、±4、±6),小值更密集,能更好地保留权重中的小值信息,因此在新一代硬件上更受青睐。整数类型的十进制有效数字约等于
6. 精度与范围的权衡直觉
前面讲了原理和数字,这一节总结三条"实战直觉",帮你快速决定该用哪种类型。
- 同一格式内,指数与尾数互相竞争:同样 16 位,BF16 用更多位给指数(范围大、精度低),FP16 用更多位给尾数(精度高、范围小)。两种布局对照如下:
将第 3 节总览表中的 FP16 与 BF16 两行与上图对照,就能看到:位宽相同的两种半精度,只是把有限的 16 位按不同比例分给了"范围"与"精度"。
- 训练:FP32 保存主权重,FP16/BF16 计算激活与梯度,FP8 进一步加速训练。
- 推理:FP16/BF16 为基座,FP4/INT8/INT4 量化权重以压缩显存、提升吞吐,INT2 属前沿探索。
- 溢出风险:FP16 范围窄(max 65504),Softmax 前的 Logits 极易溢出,这正是 Online Softmax 与 FlashAttention 用"减去最大值"规避的原因。
6.1 验证脚本
用 Python 快速打印各类型的极限值:
import numpy as np
import math
print("FP16:", np.finfo(np.float16)) # max ≈ 65504, eps ≈ 9.8e-4
print("FP32:", np.finfo(np.float32)) # max ≈ 3.4e38, eps ≈ 1.2e-7
# 机器精度 ε = 2^(-尾数位数);十进制有效数字 ≈ (尾数位数 + 1) × log10(2)
for name, man in [("FP32", 23), ("FP16", 10), ("BF16", 7), ("E4M3", 3), ("E5M2", 2), ("FP4", 1)]:
eps = 2 ** (-man)
digits = (man + 1) * 0.3010
print(f"{name}: ε = {eps:.3g}, 约 {digits:.1f} 位十进制有效数字")
# 用公式复算最大值:max = (2 - 2^(-尾数位)) × 2^bias
for name, exp, man in [("FP32", 8, 23), ("FP16", 5, 10), ("BF16", 8, 7), ("E5M2", 5, 2)]:
bias = 2 ** (exp - 1) - 1
max_val = (2 - 2 ** (-man)) * 2 ** bias
print(f"{name}: max = (2 - 2^-{man}) × 2^{bias} ≈ {max_val:.3g}")
# 整数范围:有符号 [-2^(N-1), 2^(N-1) - 1]
for n in [2, 4, 8, 16, 32, 64]:
print(f"INT{n}: [{-(2 ** (n - 1))}, {2 ** (n - 1) - 1}]")
# FP4 的完整取值集(E2M1)
print("FP4 取值集:", [0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0])
# 整数类型的十进制有效数字 ≈ log10(取值个数)
for name, n in [("INT64", 2**64), ("INT32", 2**32), ("INT16", 2**16),
("INT8", 2**8), ("INT4", 2**4), ("INT2", 2**2)]:
print(f"{name}: 约 {math.log10(n):.1f} 位十进制")总结
| 关注点 | 结论 |
|---|---|
| 精度由什么决定 | 尾数位数(机器精度 |
| 范围由什么决定 | 指数位数 |
| 最大最小值怎么算 | 浮点:最大尾数 × 最大指数幂( |
| FP16 最大正数 | 65504(Softmax 需防溢出) |
| BF16 与 FP32 的关系 | 范围相同,精度仅 ~2 位 |
| FP8 两种变体的分工 | E4M3 给权重/激活,E5M2 给梯度 |
| FP4 的取值集 | {0, ±0.5, ±1, ±1.5, ±2, ±3, ±4, ±6},最大 6 |
| 整数类型的精度 | 步长恒为 1,范围内整数绝对精确 |
| 标准整数类型 | INT32/INT64 用于索引与计数,INT32 是 CUDA 默认;位宽越大范围越广、精度不变 |
| 机器精度 ε 跨度 | FP32 ≈ 1.2×10⁻⁷ → FP4 为 0.5 |
相关链接
参考
- IEEE 754 浮点数标准 — Wikipedia
- Micikevicius, P., et al. (2022). FP8 Formats for Deep Learning. arXiv:2209.05433.
- FP8: 8-bit floating point for fast training and inference — Zero Entropy
- FP8 Quantization — vLLM Docs
- numpy.finfo 文档