01. 数字怎样存进计算机
模型文件里有大量数字,例如 1.25、-0.03。这些决定计算结果的数称为参数,常见的参数又叫权重。精度问题从它们怎样存进内存开始。这一篇只跟着 1.25,从日常小数走到一段浮点编码。
一位只有两种选择
计算机用 0 和 1 记录信息。一位(bit,缩写 b)就是一个只能填 0 或 1 的位置。可以把它想成只有关、开两种状态的小开关;内存和存储设备用电荷等物理状态来保存这两种选择。
两位有 00、01、10、11 四种组合;三位有八种。每多一位,组合数翻倍。n 位的组合数就是 n 个 2 相乘,简写为 2^n,例如三位有 2 × 2 × 2 = 8 种组合。
8 位组成 1 字节(byte,缩写 B)。小写 b 和大写 B 在这里差了 8 倍:8 bit = 1 byte。字节像一排八个小开关,“位”数的是其中的单个位置。
八个 0 和 1,为什么能表示 5
先按无符号整数的规则读一字节。十进制从右往左是个位、十位、百位;二进制从右往左的位权是 1、2、4、8、16、32、64、128,每向左一位翻倍。某个位置填 1,就加上它的位权;填 0,就加 0。
例如 00000101,只有位权 4 和 1 的两个位置填了 1,所以读出来是 4 + 1 = 5。下图的“位 0”到“位 7”是位置编号,最右边从 0 开始数;每个位置里的值始终只有 0 或 1。
位 7位权 128
位 6位权 64
位 5位权 32
位 4位权 16
位 3位权 8
位 2位权 4
位 1位权 2
位 0位权 1
00000101₂ = 4 + 1 = 5
8 位组成 1 字节,当前按无符号整数读取。
把最左侧的位 7 从 0 改成 1,数值会增加 128,变成 133;改动最右侧的位 0,只会相差 1。八位全为 0 时读作 0,全为 1 时读作 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = 255。因此一个无符号字节有 256 种组合,表示 0 到 255。
位负责保存 0 或 1,数值格式规定怎样读这些位。后面会看到,有的位表示正负,有的一组位用来保存指数。相同的 1011,按无符号整数读取是 11;若约定最左位的位权为 −8,其余仍为 4、2、1,就会读作 −8 + 2 + 1 = −5。这种负数编码叫补码,第五篇会结合 INT4 继续解释。
模型里的位数怎样换成文件大小
32 位是 4 字节,16 位是 2 字节,8 位是 1 字节。4 位占半字节,紧凑存储时可以把两个四位编码拼进一个字节。
一个有 70 亿个参数的模型,如果每个参数都占 4 字节,原始参数就需要 70 亿 × 4 = 280 亿字节,约 28 GB;换成 2 字节则约 14 GB。这里的 GB 按十亿字节计算。量化格式还会保存缩放因子等附加信息,后面计算 INT4 模型大小时会把它们一起算上。
位数和空间的关系清楚了,接着把视线从整数移到小数。保存模型参数时,我们还需要表示 1.25、0.001,以及负数。
二进制也有小数点
十进制 1.25 中,小数点右侧的第一位表示十分之一,第二位表示百分之一。二进制也有位权,只是每向右一位除以 2:
| 位置 | 小数点左一位 | 右一位 | 右二位 | 右三位 |
|---|---|---|---|---|
| 位权 | 1 | 1/2 | 1/4 | 1/8 |
1.01₂ 的数字 | 1 | 0 | 1 | 0 |
所以 1.01₂ = 1 + 0 × 1/2 + 1 × 1/4 = 1.25。下标 2 表示这串数字按二进制阅读。
也有写不完的小数。十进制的 1/3 = 0.333… 会无限延续;十进制 0.1 换成二进制,也会出现无限循环。存储位数有限时,就得在某处停止并舍入。即使使用 FP32,也会遇到这种情况。
小数点为什么要“浮动”
如果总在小数点前后固定留几位,很大的整数和很小的小数很难兼顾。先认识一种简写:10^3 表示 10 × 10 × 10 = 1000,上面的 3 称为指数;10^-3 表示它的倒数,也就是 1/1000。任何非零数的零次方都约定为 1。
科学计数法把 1250 写成 1.25 × 10^3,把 0.00125 写成 1.25 × 10^-3,用指数移动数量级。二进制中,2^3 = 8,2^-3 = 1/8,2^0 = 1,计算方法相同。
二进制浮点采用同样思路。对通常的规格化数,可以写成:
数值 = 正负号 × (1 + 小数部分) × 2^指数
1.25 = +1.01₂ × 2^0:符号为正,指数是 0,小数部分是 .01₂。把小数点移到第一个非零数字后面的写法,称为规格化。二进制中非零数字只有 1,所以规格化后开头总是 1,通常省去这一位,只保存它后面的部分。这样同样的位数可以多留一位有效信息。
换成整数 5,它的二进制是 101₂。把小数点从末尾向左移两位,写成 1.01₂,再乘 2^2 = 4 补回大小,于是 5 = 1.01₂ × 2^2。它与 1.25 的有效数字相同,只是指数变了,这正是“浮点”所做的工作。
把 1.25 装进 FP16
FP16 一共 16 位:1 位符号、5 位指数、10 位小数部分。正数的符号位写 0;小数部分 .01 后面补零,成为 0100000000。
指数还要解决负数怎样保存的问题。FP16 为普通指数约定了偏移量 15:实际指数 0 保存为 0 + 15 = 15,二进制是 01111。这样实际负指数也能用一串无符号位记录。
合起来就是:0 | 01111 | 0100000000。读取时反过来计算:
(+1) × (1 + 1/4) × 2^(15−15) = 1.25
指数位全 0 或全 1 时有特殊用途,例如零、非常小的数、无穷大和无效数;后面会结合溢出与下溢解释。先掌握普通数的三段分工,就能看懂大部分精度对比。
浮点格式规定了怎样用有限的位记录正负、数量级和小数细节。下一篇FP32、FP16 与 BF16保持这三段结构,只改变每段分到的位数,看看会发生什么。也可以在常见数值格式存储图解中按名称查看每种格式的内部结构。
