Skip to content

约 11 分钟阅读

常见数值格式存储图解 ​

FP16 的 16 位分给了谁?INT4 为什么能表示负数?下载的“四位模型”为何还需要额外空间?这篇把模型训练、推理和量化工具中常见的格式逐个拆开,跟着实际编码把数读回来。第一次接触二进制,可以先读数字怎样存进计算机,再回来按格式查阅。

每张图上方的字段条按位数比例绘制,下面标出字段名称、位编号、示例编码和读数过程。位编号从最右侧的 0 开始;一位里面只保存 0 或 1,字段规定这些位的用途。浮点图中的粉色是符号,绿色是指数,蓝色是小数部分。

FP32、FP16、BF16 与 FP64 ​

这四种格式都把普通的规格化数拆成三段:符号决定正负,指数决定数量级,小数部分决定当前数量级内的细节。读取时先把指数编码减去偏移量,再计算:

正负号 × (1 + 小数部分) × 2^实际指数

小数部分从左到右的位权依次是 1/2、1/4、1/8……。下面都保存 1.25,所以小数部分从 01 开始,表示 1/4;前面的 1 由规格化规则补回。指数全零等特殊编码另有读取规则,在范围与刻度中继续展开。

FP32 ​

1 位符号、8 位指数、23 位小数部分,共 32 位、4 字节。指数偏移量为 127。训练中的累加、优化器状态以及部分敏感计算经常会用到它。

FP32:逐段标出存储位数、示例编码和读回过程

FP16 ​

1 位符号、5 位指数、10 位小数部分,共 16 位、2 字节。指数偏移量为 15,最大有限值为 65504。在 1 附近,它比 BF16 保留更多小数细节。

FP16:逐段标出存储位数、示例编码和读回过程

BF16 ​

1 位符号、8 位指数、7 位小数部分,共 16 位、2 字节。指数偏移量与 FP32 相同,都是 127。它保留宽数量级范围,把小数部分压缩得更短。常见拼写是 BF16,来自 bfloat16。

BF16:逐段标出存储位数、示例编码和读回过程
FP64:科学计算中的双精度

FP64 有 1 位符号、11 位指数、52 位小数部分,共 8 字节,偏移量为 1023。规格化数算上隐含的开头 1,共有 53 位二进制有效信息。科学计算和高精度数值验证中常见,主流大模型权重通常采用更短的格式。

FP64:逐段标出存储位数、示例编码和读回过程

FP8:同样八位,还有不同约定 ​

FP8 每个数只占 1 字节。E4M3 表示四位指数、三位小数,E5M2 表示五位指数、两位小数;两者都还留了一位符号。实际使用时,需要连同 FN、FNUZ 等后缀一起识别。

E4M3FN ​

偏移量为 7,最大有限值 448。指数全 1 的部分编码继续表示有限数,只在特定位置保留 NaN(无效数),没有无穷大编码。保存 1.25 时,八位写作 0 | 0111 | 010。

FP8 E4M3FN:逐段标出存储位数、示例编码和读回过程

最大正数的编码是 0 | 1111 | 110:指数 15−7 = 8,小数部分 0.110₂ = 3/4,读回 (1 + 3/4) × 2^8 = 448。继续把小数部分写成 111,就到了 NaN 的保留位置。

E5M2 ​

偏移量为 15,最大有限值 57344,支持无穷大与 NaN。与 E4M3FN 相比,多一位指数扩大了范围,少一位小数使局部刻度变疏。它们在同一个 FP8 工作流中也可能用于不同组数据。

FP8 E5M2:逐段标出存储位数、示例编码和读回过程
E4M3FNUZ 与 E5M2FNUZ:偏移量也变了

FNUZ 变体采用有限值、NaN 与单一零的约定:00000000 是零,原本可用于负零的 10000000 改为 NaN。两种格式都没有无穷大编码。

E4M3FNUZ 的偏移量为 8,最大有限值 240。相同的 1.25,其指数编码变成 1000。因此,判断 FP8 的数值还要知道具体变体。

FP8 E4M3FNUZ:逐段标出存储位数、示例编码和读回过程

E5M2FNUZ 的偏移量为 16,最大有限值 57344。保存 1.25 时,指数编码为 10000。带 FNUZ 的 dtype 在部分 AMD/ROCm 等软件路径中会遇到,交换模型时需要对齐完整类型名称。

FP8 E5M2FNUZ:逐段标出存储位数、示例编码和读回过程

FP4 与 FP6:进一步缩短浮点编码 ​

FP4 E2M1 ​

1 位符号、2 位指数、1 位小数,偏移量为 1。正幅值只有 0、0.5、1、1.5、2、3、4、6,再用符号位表示负值,包含正零与负零。所有编码都用于有限值。下面保存 1.5,它恰好能用唯一的小数位表示。

FP4 E2M1:逐段标出存储位数、示例编码和读回过程

1.25 位于 1 和 1.5 之间,通常的就近、平局取偶舍入会选到 1。四位刻度很少,所以 FP4 常与组级 scale 一起工作,让每组数落在合适的范围。

FP6 E2M3 与 E3M2:六位的两种分配

OCP MX 体系还定义了六位浮点。它们保留一位符号,其余五位分配给指数和小数部分,所有编码都表示有限值,支持零和次正规数。

E2M3:2 位指数、3 位小数,偏移量为 1,最大有限值 7.5。与 E2M1 相比,数量级相近,局部刻度更细。

FP6 E2M3:逐段标出存储位数、示例编码和读回过程

E3M2:3 位指数、2 位小数,偏移量为 3,最大有限值 28。它把一位小数预算移给了指数,换取更大的范围。

FP6 E3M2:逐段标出存储位数、示例编码和读回过程

六位是数值编码的长度。具体硬件和文件怎样打包、对齐,还要查看其存储约定。

INT 与 UINT:每一位参与整数求和 ​

浮点数把位分成三种职责;整数按每一位的位权相加。常见有符号整数使用补码:最高位的位权为负,其余位为正。最高位为 1 时,要把那一项负值加入求和。

INT4 ​

四个位权从左到右是 −8、4、2、1。所以 1011 读作 −8 + 2 + 1 = −5,范围是 −8 到 7。模型量化时还会配 scale,把整数刻度映射回小数。

INT4:逐段标出存储位数、示例编码和读回过程

INT8 ​

八个位权为 −128、64、32、16、8、4、2、1。图中的低七位之和为 123,加上最高位的 −128 得到 −5,完整范围为 −128 到 127。

INT8:逐段标出存储位数、示例编码和读回过程
UINT4 与 UINT8:最高位也取正位权

U 表示 Unsigned,即无符号。所有位都取正位权,因此 UINT4 范围为 0 到 15,UINT8 范围为 0 到 255。相同的 1011 在 UINT4 中读作 11;相同的 11111011 在 UINT8 中读作 251。

UINT4:逐段标出存储位数、示例编码和读回过程UINT8:逐段标出存储位数、示例编码和读回过程

图像像素、无符号量化码里经常能见到 UINT8。量化方案可以另存零点,把某个无符号编码指定为原始数值的零。

INT16、INT32 与 INT64:整数长度继续增加

补码读取规则沿用下来,只改变位数。n 位有符号整数的范围是 −2^(n−1) 到 2^(n−1)−1。下面三个例子都保存 −5,最高位的负位权与其余位的正位权相抵后得到它。

INT16 占 2 字节,范围 −32768 到 32767。整数音频 PCM 等数据中常见。

INT16:逐段标出存储位数、示例编码和读回过程

INT32 占 4 字节,范围 −2147483648 到 2147483647。整数量化矩阵乘法常用 INT32 累加中间结果。

INT32:逐段标出存储位数、示例编码和读回过程

INT64 占 8 字节,范围 −9223372036854775808 到 9223372036854775807。模型程序中的索引、计数等也可能采用它。

INT64:逐段标出存储位数、示例编码和读回过程

NF4:四位保存的是查表编号 ​

NF4 来自 NormalFloat 4。它安排了一个固定的 16 项非均匀码本,用来贴近归一化后呈正态分布的权重。四位保存 0 到 15 的索引,读取时先查码本,再乘这组数共用的倍率。这个倍率叫缩放因子(scale),负责把归一化后的刻度恢复到原来一组数的大小。

图中采用 bitsandbytes 的 NF4 编码:索引 0 对应 −1,7 对应 0,15 对应 1。例如 1111 是索引 15,查出 1,再乘约为 0.3 的共享 scale,恢复为约 0.3。码本是固定规则,scale 则随数据块保存。以 64 值分组、FP32 保存 scale 为例,每组有 64 × 4 + 32 = 288 位,平均 4.5 位/值。

NF4:逐段标出存储位数、示例编码和读回过程
NF4 的完整 16 项码本

这里列出约数。例如 0010 对应索引 2,先查到约 −0.525073,再乘这组的 scale。

四位编码索引码本值(约)
00000−1
00011−0.696193
00102−0.525073
00113−0.394917
01004−0.284441
01015−0.184773
01106−0.091050
011170
100080.079580
100190.160930
1010100.246112
1011110.337915
1100120.440710
1101130.562617
1110140.722957
1111151

因此 NF4 的四位内部没有独立的符号、指数和小数区域。双重量化等方案还会压缩 scale,自身的附加信息也需要计入文件大小。

一组数怎样共用 scale ​

假设一组编码能读出 1.5,共享 scale 为 2,最终恢复值就是 1.5 × 2 = 3。把这一组都放大或缩小,可以让有限的低位刻度适应这组数据的范围。代价是需要保存 scale,所以“四位”载荷算上元数据后,平均占用通常会高于四位。

E8M0:专门保存 2 的幂 ​

MX 常用 E8M0 保存 scale,八位全部用于指数,没有符号和小数区域。有限编码从 0 到 254,读为 2^(编码−127);255 用作 NaN。编码 128 就是 2^1 = 2。它只保存正的 2 的幂,没有零编码。

E8M0 缩放格式:逐段标出存储位数、示例编码和读回过程

MXFP4 ​

32 个 E2M1 四位值共用一个八位 E8M0 scale。载荷占 32 × 4 = 128 位,scale 占 8 位,共 136 位、17 字节,平均 4.25 位/值。

MXFP4:逐段标出存储位数、示例编码和读回过程
MXFP8 与 MXFP6:相同的 32 值分组

MXFP8 每 32 个八位值配一个 E8M0 scale,共 33 字节,平均 8.25 位/值。图中数值采用 E4M3;MX 规范也涵盖 E5M2。

MXFP8(图中采用 E4M3):逐段标出存储位数、示例编码和读回过程

MXFP6 每 32 个六位值配一个 E8M0 scale,共 25 字节,平均 6.25 位/值,数值可以采用 E2M3 或 E3M2。

MXFP6:逐段标出存储位数、示例编码和读回过程

NVFP4 ​

张量是按一定形状排列的一组数,例如一行权重或一个矩阵。NVIDIA Transformer Engine 中常见的一维 NVFP4 分组,每 16 个 E2M1 值配一个 FP8 E4M3 块级 scale,还使用一个整个张量共享的 FP32 scale。恢复时把数值编码、块级 scale 和张量级 scale 相乘。FP8 scale 可以保存比 2 的整数次幂更细的比例。

NVFP4:逐段标出存储位数、示例编码和读回过程

图中一组占 16 × 4 + 8 = 72 位,平均 4.5 位/值,再加张量级 scale 的摊销。训练实现还会对权重采用二维分组。具体文件格式会安排自己的字段,例如 GGML 的 NVFP4 块结构就有自己的打包方式;读取模型时要对齐对应实现。

GGUF 模型中的量化块 ​

GGUF 容器内的张量可以采用多种块格式。这里拆开 GGML 常见的 Q4_0 和 Q4_K:它们都保存四位量化码,但共享参数的组织方式不同。

Q4_0:32 个数共用一个 FP16 缩放因子

一块保存 32 个值,包含一个 FP16 缩放因子 d(2 字节),以及 16 字节四位量化码。每个码 q 取 0 到 15,恢复时计算 d × (q−8)。例如 d 为 0.25、q 为 9,得到 0.25。

GGUF / GGML Q4_0:逐段标出存储位数、示例编码和读回过程

一块 18 字节,平均 4.5 位/值。图中的比例条按字段总位数绘制;具体字节中的半字节排列由 GGML 解码实现规定。

Q4_K:256 个数里再分八个子组

一个 256 值超块里有八个 32 值子组。块头保存 FP16 的总 scale d 和总 min dmin;每个子组再保存六位 scale 和六位 min,八组共打包为 12 字节;最后是 128 字节四位量化码。

GGUF / GGML Q4_K:逐段标出存储位数、示例编码和读回过程

组 scale 和组 min 都是 0 到 63 的六位无符号码。组 scale 乘 d,得到这一组的刻度间距;组 min 乘 dmin,得到这一组要减去的偏移。读取时按 d × 组 scale × q − dmin × 组 min 恢复。

例如 d 为 0.25、组 scale 为 2、量化码 q 为 3,正向刻度得到 0.25 × 2 × 3 = 1.5;再取 dmin 为 0.5、组 min 为 1,减去 0.5 × 1,恢复值就是 1。组 min 保存的是参与计算偏移的无符号码。

合计 144 字节、平均 4.5 位/值。模型名中的 Q4_K_M 还包含按不同张量混用量化类型的预设,整个文件的平均位数要继续计入其他张量与元数据。

TF32 与实际字节排列 ​

TF32:乘法时使用哪些位 ​

TF32 是 NVIDIA Tensor Core 的矩阵计算模式。输入可以继续用 FP32 保存,在进入相应乘法路径时舍入到 8 位指数、10 位小数部分,再通常用 FP32 累加。配置里应分别看张量 dtype 与 TF32 开关。

TF32 矩阵计算模式:逐段标出存储位数、示例编码和读回过程

图中的最后 13 位是 FP32 原有的小数低位,它们在舍入时影响保留下来的位。若模型仍按 FP32 存文件,每个数仍占 4 字节。

两个 INT4 拼进一个字节 ​

每个四位编码只占半字节。以 ONNX 的紧凑打包顺序为例,第一个数放低四位,第二个数放高四位。−5 的编码是 1011,3 是 0011,拼成的字节就是 00111011。

两个 INT4 怎样装进一字节:逐段标出存储位数、示例编码和读回过程

其他库可以采用不同的半字节顺序,因此读取文件需要遵循文件自身的约定。奇数个四位值打包时,最后一个字节还会留下半字节用于填充。

位字段与大小端怎样对应 ​

上面的位图统一把数学上的高位画在左边。为了缩短长编码,程序员常把每四位二进制换成一个十六进制数字:0 到 9 沿用数字,10 到 15 用 A 到 F。例如 FP16 的 1.25 是 0011 1101 0000 0000,四组分别读作 3、13、0、0,简写成 0x3D00,其中 0x 表示十六进制。

多字节数放进内存时,还要决定字节先后次序,这就是大小端。这个 FP16 数的小端字节顺序是 00 3D,大端是 3D 00。每个字节仍是八位,FP16 内部的三段分工保持相同。

看到一个精度名称,现在可以沿着图读出三个层次:单个数的位怎样分工,一组数共享哪些附加信息,计算程序怎样还原并使用它。模型名里经常还会出现 AWQ、GPTQ 和 W4A16:前两者是选择量化参数的方法,W4A16 描述权重与激活各用多少位,GGUF 则是装载张量和元数据的文件容器。读到这些名字时,可以继续追问它实际装了哪一种数。

实际使用时怎样取舍 ​

FP16 与 BF16 花的是同样的两字节预算:在两者都处于规格化范围时,FP16 在同一数量级内分得更细;BF16 留出了更宽的指数范围,在极小数附近也可能保留更细的刻度。训练和推理先看模型的数值范围、局部误差与设备支持,再决定哪一种更合适。

降到八位或四位,主要换来更少的存储与搬运;可用刻度减少后,需要缩放、分组和质量验证配合。权重采用四位时,激活、累加和 KV 缓存仍可各选自己的精度。若长对话主要占在缓存上,就把优化重点放到缓存与请求规模。

速度还取决于执行程序能否高效使用这些编码。已有高效八位方案并且质量、内存都达标,可以保留八位;四位省出的空间有实际用途,或设备已有合适的四位路径,再比较四位。FP32 则可以留给敏感计算和对照,帮助找到误差从哪里进入结果。

完整的精度取舍与使用场景分析按方案比较收益与代价,再用 70 亿参数、16 GB 预算的案例推演选择过程。继续理解误差,可以回到舍入、溢出与计算误差;低位刻度的计算过程见FP8、FP4 与整数量化。

别急,先让缓存热一下。