大模型精度详解:从浮点数存储到混合精度训练
大模型精度详解:从浮点数存储到混合精度训练
大模型精度
1. 通俗理解浮点数在计算机储存数据的方式
在计算机科学中,所有底层数据都是以二进制形式存储的。浮点数在二进制中的存储涉及到对数值进行连续的区间分割。具体地,这些分割定义在不同的二次幂区间,每个区间映射到特定的二进制表示。例如,我们可以观察以下数值区间及其对应的二进制范围:
- $2^{-2} \sim 2^{-1} \quad [0.25 \sim 0.5)$
- $2^{-1} \sim 2^{0} \quad [0.5, 1)$
- $2^{0} \sim 2^{1} \quad [1, 2)$
- $2^{1} \sim 2^{2} \quad [2, 4)$
- $2^{2} \sim 2^{3} \quad [4, 8)$
- ...
以数值9.86为例,我们首先使用$2^3$(即8)作为分割点,从而将数值划分为9.86 - 8 = 1.86。接着,使用$2^0$(即1)继续分割,得到1.86 - 1 = 0.86。通过这种方式,理论上我们可以无限迭代分割数值,直至达到预定的近似精度。
但是存在着一个权衡问题,随着最小分割单位的越来越小,尽管我们计算精度尽管提高了,但储存成本大大提升了。那我们能不能确定一个合适的最小分割单位例如$2^{-2}$来作为标准呢?似乎在9.86这个案例是合适的,但如果数值是0.02呢,用更小的分割单位吗?如果数值是101000,使用$2^{-2}$是不是多次一举了呢?那我们应该如何确定最小的分割单位呢?
最好的方法便是根据这个数的本身大小确定,首先确定一个小于9.86的最小的$2^n$,接着将$2^n$到$2^{n+1}$均分为1024份小区间,再取这个数最大能覆盖的小区间数,这样的动态方法确保了一定的相对精确性。比较有意思的是$2^n$到$2^{n+1}$的长度也是$2^n$,即便是相对误差最大的情况,即当这个数为$2^n + \frac{2^n}{1024} - \delta$(无穷小)时,相对误差也仅为1/1024。当然,如果分割得越细,那么相对误差越小;当数值越大时,绝对误差越大。
2. 常见精度类型
2.1 FP16(半精度浮点数)
FP16是最常见的浮点精度之一,FP指的是“浮点数”(Floating Point),16则代表16位。
- 最左边是符号位,如果是0,则表示正数;如果是1,则表示负数。
- 接下来的5位是指数位,使用偏移值(bias)32。指数位,即代表小于这个数的最大的$2^n$。具有5位,能表示32个数字,通常表示$2^{-14}$到$2^{15}$(其中00000和11111代表特殊符号,代表零、无穷大和NaN的特殊情况)。
- 最后的9到0位,称为尾数位,则表示将$2^n$到$2^{n+1}$分割成的1024($2^{10}$)个小区间的第几个小区间,从而达到近似的效果。
2.2 FP32(单精度浮点数)
FP32,也称为单精度浮点数,是另一种广泛使用的浮点格式。在FP32中,总共使用32位二进制位。比FP16具有更大的范围和更高的精确性,当然也更占内存。其中:
- 最左边的位是符号位,同样地,0表示正数,1表示负数。
- 接下来的8位是指数位,使用偏移值(bias)127。这意味着指数范围从$2^{-126}$到$2^{127}$,但实际表示的指数范围是$2^{-126}$到$2^{128}$(其中00000000和11111111代表特殊符号,代表零、无穷大和NaN的特殊情况)。
- 最后的9到0位,称为尾数位,则表示将$2^n$到$2^{n+1}$分割成的$2^{23}$个小区间的第几个小区间,从而达到近似的效果。
2.3 FP64(双精度浮点数)
FP64,或称为双精度浮点数,是对于需要极高数值精度的应用场景(如科学计算和工程模拟)的理想选择。在FP64中,共使用64位二进制位。其中:
- 符号位占据最左边的一位,同样地,0表示正数,1表示负数。
- 指数位占据11位,使用偏移值1023,表示的指数范围从$2^{-1022}$到$2^{1023}$,实际范围从$2^{-1022}$到$2^{1024}$(其中00000000000和11111111111代表特殊符号,代表零、无穷大和NaN的特殊情况)。
- 最后的9到0位,称为尾数位,则表示将$2^n$到$2^{n+1}$分割成的$2^{52}$个小区间的第几个小区间,从而达到近似的效果。
2.4 BF16(Brain Floating Point 16)
BF16是一种被设计来优化深度学习和其他需要高吞吐量计算的浮点数格式。它使用16位二进制来表示浮点数,结构上与FP16类似,但分配给指数和尾数的位数有所不同:
- 最左侧的1位仍然是符号位。
- 接下来的8位用于指数,这与FP32中的指数位数相同,使用的偏移值也是127。这使得BF16在指数范围上与FP32一致,可以更好地处理更广泛的数值范围。
- 剩余的7位分配给尾数,相比于FP16的10位尾数,BF16的尾数精度较低。
2.5 TF32(Tensor Float 32)
TF32是NVIDIA推出的一种新的浮点格式,专为AI训练和推理优化。它在NVIDIA Ampere架构GPU中首次引入,旨在提高计算效率而不牺牲过多的精度。TF32使用32位格式,但其内部结构与传统的FP32有所不同:
- 符号位占1位,与FP32一致。
- 指数位占8位,也与FP32相同,使用偏移值127,因此在表示指数的范围上与FP32相匹配。
- 尾数位占10位,较FP32中的23位大大减少,这一改变降低了计算的复杂度,加快了处理速度。
3. 常见的数值表示问题
3.1 上溢出(对应指数位)
上溢出发生在尝试表示超过该精度格式能够表示的最大数值时,二进制表示为全1。
以FP16为例,理论上最大能表示的数为$2^{15} + 2^{15} * \frac{1023}{1024}$,即65504。但如果尝试表示65505使用FP16,尽管65505达到了$2^{15} + 2^{15} * \frac{1023}{1024}$,但却小于$2^{15} + 2^{15} * \frac{1024}{1024}$。因此,65505超出了FP16能够表示的精度范围,最后还是被表示为65504,这导致了精度的损失。因此,65504是在精度不损失的情况下能表达出的最大数,但实际上FP16可以表示小于$2^{16}$的所有值,而$2^{16}$是上溢出值。
在大型模型中,上溢出常发生在梯度爆炸、学习率设置过高等情况。
3.2 下溢出(对应尾数位)
下溢出通常发生在尝试表示一个非常接近于零的数,但该数的绝对值小到无法在该精度格式下准确表示,二进制表示为全0。
以FP16为例,当所表达的值小于$\frac{2^{-14}}{1024}$时,结果将为全0,即发生下溢出。这种情况通常出现在进行非常细微的数值计算时,如处理具有极低概率事件的数据或在科学计算中处理极小的物理量。
3.3 NaN(Not a Number)
在浮点数计算中,NaN(Not a Number,非数字)是一种特殊的值,用于表示那些无法用正常浮点数表示的结果。这包括一些未定义的数学操作,例如零除以零、无穷大除以无穷大、负数的平方根等。
4. 大模型精度类型和显存占用
fp16, bf16储存两字节一个参数,fp32储存4个字节一个参数。1GB约等于1B的字节,即和1B参数量的数据量级一致。
全参微调为例:
- 模型状态:模型参数(fp16),模型梯度(fp16)和Adam状态(fp32的模型参数备份,fp32的动量,fp32的variance)
假设模型参数为X,则全参微调共需要$2 * X + 2 * X + (4 * X + 4 * X + 4 * X) = 16X$的显存大小 - 剩余状态:除了模型状态之外的显存占用 ,包括激活值、各种临时缓冲区以及无法使用的显存碎片
- 模型参数(Model Parameters) - fp16
- 这些是模型的权重,用于在前向传播过程中计算输出。
- 使用半精度 (fp16) 格式可以减少显存使用量并加速计算过程。
- 模型梯度(Model Gradients) - fp16
- 在反向传播过程中计算的梯度,用于更新模型参数。
- 这些梯度通常与模型参数具有相同的形状和数据类型。
- Adam状态
- 参数备份(Model Parameters Backup) - fp32
- 在使用 fp16 进行训练时,通常将参数的一个备份以 fp32 格式存储,以保持训练过程的数值稳定性。
- 动量(Momentum) - fp32
- Adam 优化器中用于加速梯度下降的一种方法,它累积过去梯度的加权平均值。
- 方差(Variance) - fp32
- 另一组在 Adam 优化器中使用的梯度累积量,表示梯度的加权方差,用于自适应调整各参数的学习率。
- 激活值(Activations)
- 在模型的前向传播过程中生成的中间输出值,通常在反向传播时需要存储用于梯度计算。
- 临时缓冲区(Temporary Buffers)
- 在训练过程中使用的各种临时数据存储。
- 显存碎片(Memory Fragmentation)
- 在频繁申请和释放显存的过程中可能出现的无法有效使用的小块显存。
推理的话,假设qwen-7B模型,采用bf16保存,整体显存占用需要 2*7=14GB显存左右。理论上全参微调占用显存是模型推理的8倍。
5. 大模型精度中常见问题
5.1 上溢和下溢
FP16 的表示范围较小,超过 6.5e4 的数值将会上溢为无穷大(inf),而小于 5.9e-8 的数值会下溢为 0。下溢现象更为常见,特别是在神经网络训练的后期阶段,模型的梯度值可能会非常小,甚至小于 FP16 的最小界限(5.9e-8)。这种情况下,梯度值会变为 0,导致模型参数无法得到有效更新。上溢出常发生在,$e^x$的指数计算中,在计算sigmoid函数和softmax函数时尤为常见。
5.2 舍入误差
在数值计算中,如果两个数的大小相差极大,比如一个数非常小,小到达到或低于另一个数的有效数字的最低位,那么在进行加减运算时,较小的数的影响可能会完全丧失,这是由于舍入误差所引起的。这种情况下,较小的数对结果的贡献可能会因为数值表示的限制而被忽略,导致计算结果不能精确反映理论上的数学结果。
import torch
epsilon_16 = torch.tensor(2.718, dtype=torch.float16)
print("2.718 in float16 representation", epsilon_16) # Outputs: 2.71875
epsilon_32 = torch.tensor(2.718, dtype=torch.float32)
print("2.718 in float32 representation", epsilon_32) # Outputs: 2.7179999351501465
print("Conversion from float16 to float32", epsilon_16.float()) # Outputs: 2.71875
delta = torch.tensor(0.0001, dtype=torch.float16)
print(epsilon_16 + delta) # Outputs: 2.71875 (no change due to precision limit)
print(epsilon_32 + delta) # Outputs: 2.718100070953369
print(epsilon_16.float() + delta) # Outputs: 2.7188501358032227
6. 混合精度
为了解决上述问题,17年百度和英伟达共同提出了一种混合精度训练的新方法。
具体流程:
- 前向传播:使用半精度(FP16)权重进行前向传播,计算模型的输出。
- 损失放大:计算单精度(FP32)的损失,并进行放大(scaling),以减少半精度计算中可能出现的数值下溢问题。
- 反向传播:基于放大的损失值进行反向传播,计算半精度(FP16)的梯度。
- 拷贝:将半精度梯度拷贝为单精度梯度。
- 去除放大效果:对单精度梯度去除之前的放大效果,并进行其他必要的调整(如梯度裁剪等)。
- 应用梯度:应用这些梯度来更新单精度的主权重。
- 拷贝权重:将更新后的单精度权重拷贝回半精度,用于下一轮的前向传播。
混合精度训练巧妙地利用了FP16的高效计算和内存节省特点,与FP32的高精度和稳定性相结合。通过在训练过程中动态调整使用的数据类型,混合精度训练能够显著提高深度学习模型的训练速度,同时在内存使用、模型精度和数值稳定性之间取得了良好的平衡。
参考资料
- 2-大模型的数值精度(BF16,F16,F32)和混合精度训练法_哔哩哔哩_bilibili
- What is FP64, FP32, FP16? Defining Floating Point | Exxact Blog (exxactcorp.com)
- Video Series: Mixed-Precision Training Techniques Using Tensor Cores for Deep Learning | NVIDIA Technical Blog
- LLM分布式训练第五课-Deepspeed_Zero - 大模型知识库|大模型训练|开箱即用的企业大模型应用平台|智能体开发|53AI
- 2101.06840 (arxiv.org)
