精度对齐基础概念:数据格式与精度损失
做算子映射精度对齐,说白了就是搞清楚一件事:同一个计算,在不同数据类型下跑,结果到底差多少。
我刚开始接触这行时,觉得精度对齐不就是比对比对数值嘛。后来踩了不少坑才明白——不理解数据格式的本质,你连精度损失从哪来的都找不到。今天咱们就把FP32、FP16、INT8、BF16这些格式彻底聊透。
1. 浮点数的通用结构
先看一张图,这是所有浮点格式的通用骨架:
这个公式是所有浮点格式的根基。指数位决定了动态范围,尾数位决定了精度。你想想看,尾数位越多,你能表示的小数就越精细——这就是精度损失的源头之一。
2. 主流数据格式详解
2.1 FP32(单精度浮点)
FP32是AI领域的"黄金标准"。1个符号位、8个指数位、23个尾数位。它能表示约7.2位有效十进制数字。
我在项目中习惯把FP32当作"ground truth"。为什么?因为大多数训练框架的默认精度就是FP32,而且硬件对它的支持最成熟。做精度对齐时,FP32的结果就是那个"标准答案"。
| 属性 | FP32 | FP16 | BF16 | INT8 |
|---|---|---|---|---|
| 总位数 | 32 | 16 | 16 | 8 |
| 指数位 | 8 | 5 | 8 | 无 |
| 尾数位 | 23 | 10 | 7 | 无 |
| 最大正数 | ~3.4×10³⁸ | 65504 | ~3.4×10³⁸ | 127 |
| 最小正数 | ~1.2×10⁻³⁸ | ~6.1×10⁻⁵ | ~1.2×10⁻³⁸ | 1 |
| 有效精度 | ~7.2位十进制 | ~3.3位十进制 | ~2.3位十进制 | 整数 |
2.2 FP16(半精度浮点)
FP16只有5个指数位和10个尾数位。它的动态范围非常有限——最大只能表示65504。这意味着什么?
举个例子,如果你的网络权重里有个值是70000,用FP16直接存就溢出了。我在做BERT模型对齐时就遇到过这个问题,中间激活值动不动就上几万,FP16根本扛不住。
1. 溢出:数值超出65504或小于6.1×10⁻⁵
2. 舍入:尾数只有10位,很多小数无法精确表示
2.3 BF16(脑浮点)
BF16是Google搞出来的"黑科技"。它保留了FP32的8个指数位,但把尾数砍到只剩7位。
你可能会问:尾数这么少,精度够用吗?
嗯,这里要注意。BF16的巧妙之处在于——它牺牲了精度,但保住了动态范围。对于深度学习来说,动态范围往往比精度更重要。我测试过ResNet-50,BF16推理精度和FP32几乎没差别,但显存占用直接减半。
2.4 INT8(8位整型)
INT8是推理加速的利器。它只有256个离散值(-128到127),和浮点数完全不是一个世界。
把FP32转成INT8,本质上是个量化过程。你需要找到合适的scale和zero_point,把浮点范围映射到整数范围。
// 一个简单的量化示例
float scale = (max_val - min_val) / 255.0f;
int zero_point = (int)(-min_val / scale);
// 量化
int8_t q_val = (int8_t)(f_val / scale + zero_point);
// 反量化
float f_val = (q_val - zero_point) * scale;
INT8的精度损失主要来自量化误差。说白了就是:你用一个256级的阶梯去近似一个连续空间,肯定会有偏差。我见过最夸张的情况——某个模型FP32精度95%,INT8直接掉到82%。后来发现是激活值分布太不均匀,量化参数没选好。
3. 精度损失来源深度分析
做精度对齐,你得知道损失从哪来。我总结了四个主要来源:
3.1 舍入误差
这是最基础的损失。比如FP16的尾数只有10位,它无法精确表示0.1这个小数。你算一下:0.1的二进制是无限循环的,FP16只能截断或舍入。
我曾经调试过一个模型,FP32和FP16的精度差只有0.1%,但就是这0.1%导致最终输出完全不对。后来发现是softmax里有个很小的数值,FP16直接把它舍成0了。
3.2 溢出误差
FP16的最大值是65504,BF16的最大值是3.4×10³⁸。如果你的网络里有大数值,FP16就危险了。
3.3 量化误差
INT8的量化误差和浮点格式不同。它不是"近似表示",而是"离散化"。你把一个连续区间切成256份,落在格子里的值都会被映射到格子边界。
量化误差的大小取决于两个因素:
- 数据分布:如果数据集中在某个小范围,量化效果就好
- 量化方法:对称量化 vs 非对称量化,per-tensor vs per-channel
3.4 累积误差
单个运算的误差可能很小,但深度学习模型动辄几百层。误差一层层传下去,就像滚雪球一样。
我记得有个项目,单层FP16误差只有0.01%,但50层之后误差累积到了5%。这就是为什么精度对齐不能只看单层,要看端到端。
4. 精度对齐的核心指标
做精度对齐时,我一般用这几个指标来衡量:
| 指标 | 公式 | 说明 |
|---|---|---|
| 绝对误差 | |a - b| | 直观,但受数值范围影响大 |
| 相对误差 | |a - b| / |a| | 更公平,但a接近0时不稳定 |
| 余弦相似度 | cos(a, b) | 看方向是否一致,常用于向量 |
| ULP误差 | |a - b| / ULP(a) | 看差了多少个最小精度单位 |
我个人习惯先用绝对误差和相对误差做快速筛查,再用余弦相似度做最终确认。如果余弦相似度大于0.999,基本可以认为精度对齐了。
5. 一个真实案例
最后分享一个我实际做过的案例。当时要把一个FP32的YOLOv5模型转成INT8部署到边缘设备上。
第一步,我统计了所有层的激活值分布。发现大部分值集中在[-5, 5]之间,但有个别层有离群值到-30。
第二步,我尝试了两种量化策略:
- 策略A:全局量化,用[-30, 30]的范围
- 策略B:per-channel量化,每层独立找范围
结果策略A的mAP从0.75掉到了0.62,策略B只掉到了0.73。为什么?因为全局量化把大部分值的精度都浪费在了离群值上。
这个案例告诉我们:精度对齐不是简单的"转格式",而是要理解你的数据长什么样。
好了,关于数据格式和精度损失的基础概念就聊到这。这些知识是后续做算子映射精度对齐的基石。你想想看,连数据格式的脾气都没摸透,怎么去对齐精度呢?
想要更多资料,联系可可