ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

浮点数精度陷阱与FP16/BF16选型:从原理到排查

浮点数精度陷阱与FP16/BF16选型:从原理到排查 《4-浮点数和陷进》最容易踩坑的地方往往不是不知道浮点数是什么而是已经写了很久代码突然发现0.1 0.2不等于0.3或者模型部署时换到 FP16 之后精度掉得不可接受。这篇文章不聊空泛的概念直接给出浮点数为什么有精度问题、哪些场景最容易踩坑、FP32 / FP16 / BF16 / TF32 怎么选以及排查精度问题的一套具体流程。文章会涉及规格化浮点数的底层原理也会结合串口打印、PLC 浮点运算、深度学习模型部署这些实际场景展开。读完你至少能判断你的项目里到底要不要换双精度模型推理用 FP16 还是 BF16遇到精度异常该从哪里开始查。1. 核心知识点速览先把这篇文章覆盖的关键内容列成一张速览表。浮点数这个话题本身很大但实际开发中高频遇到的问题基本集中在下面几条。知识点关键要点典型场景IEEE 754 标准浮点数由符号位、指数位、尾数位组成所有主流语言和硬件都遵循规格化浮点数尾数前导位默认是 1不显式存储理解为什么有效位数是固定的非规格化数用于表示接近 0 的极小数值会损失精度且性能更慢极小数运算、特殊数值处理FP32单精度约 7 位十进制有效数字常规数值计算、旧接口数据交换FP16半精度适合深度学习推理但动态范围小模型推理、梯度混合精度训练BF16半精度但保留大动态范围尾数较少大模型训练/推理通常配合 FP32 累积TF32NVIDIA 的截断格式介于 FP32 和 FP16 之间Ampere 以上 GPU 上的矩阵运算加速常见陷阱比较不相等、大数吃小数、抵消、累积误差金额计算、科学计算、控制系统排查工具Python Decimal / math.ulp / numpy.finfo / isclose精度问题定位与验证这里先给结论如果你只记一条那就是浮点数不是实数它是二进制下对真实值的近似。所有浮点数问题的根源几乎都来自这一点。2. 浮点数为什么“不精确”二进制近似的本质很多初学者会以为浮点数不精确是因为“位数不够”这个说法只对了一半。更准确地说浮点数不精确是因为二进制无法有限表示所有十进制小数。2.1 十进制转二进制的近似过程十进制整数转二进制可以精确转换比如 5 转成101。但十进制小数转二进制用的是乘 2 取整法。以0.1为例0.1 × 2 0.2整数部分是 00.2 × 2 0.4整数部分是 00.4 × 2 0.8整数部分是 00.8 × 2 1.6整数部分是 10.6 × 2 1.2整数部分是 10.2 × 2 0.4整数部分是 0……你会发现这个过程会无限循环下去。0.1在二进制里是一个无限循环小数0.1 ≈ 0.000110011001100110011001100110011...而计算机存储有限位所以只能在某个位置截断或四舍五入。这就产生了表示误差。2.2 有效数字和指数位以最常见的 IEEE 754 单精度浮点数FP32为例它占用 32 位分成三部分1 位符号位0 表示正数1 表示负数。8 位指数位采用偏移量 127 的编码方式可以表示很大的范围。23 位尾数位存储有效数字的小数部分。双精度浮点数FP64则是1 位符号位。11 位指数位。52 位尾数位。有效数字越多能表示的十进制精度就越高。FP32 大约对应 7 位十进制有效数字FP64 大约对应 15 到 17 位十进制有效数字。这里说的“有效数字”不是小数点后几位而是从第一个非零数字开始算的总位数。用 Python 看一个简单例子import struct # 把 0.1 在内存里的二进制表示打出来 bits struct.pack(f, 0.1) print(bin(struct.unpack(I, bits)[0])) # 输出类似 0b111101110011001100110011001101 # 可以看出尾数部分是 0.1 的二进制近似这段代码不需要精确记住核心是理解FP32 存 0.1 的时候存的是一个最接近 0.1 的二进制近似值。2.3 精度和范围的区分很多人把“精度”和“范围”混在一起。范围是指浮点数能表示的最大值和最小值精度是指相邻两个可表示数之间的距离。FP16 的范围比较小最大值约 65504超过就会溢出为 Inf。BF16 的范围和 FP32 一样大但精度很低只有大约 3 位十进制有效数字。这就导致同样是“半精度”FP16 适合需要小数精度但数值范围可控的场景BF16 适合需要大范围但可以容忍低精度的场景尤其是深度学习。3. 浮点数的规格化与非规格化“浮点数的规格化”是理解浮点数格式的关键。规格化的本意是让浮点数的表示更唯一、更高效。3.1 规格化浮点数是什么在 IEEE 754 标准下一个规格化浮点数的尾数有效数字总是被调整到如下形式1.xxxxx × 2^E也就是说尾数的整数部分固定是 1小数点后是真正存储的二进制位。由于这个“1”是固定的标准在存储时就把它省略掉不占用存储位因此尾数可以多出一位精度。这就是为什么 FP32 有 23 位尾数但实际精度等效于 24 位。举个例子十进制1.5在二进制中是1.1规格化之后尾数部分是1.1000...存储时只保存1000...这一段指数位保存偏移后的指数。3.2 非规格化数Subnormal为什么存在当指数位全为 0 时浮点数进入非规格化状态。此时尾数不再自动附带隐含的 1而是表示0.xxxxx × 2^-126这样的形式。非规格化数的目的是填补真正接近 0 的间隙让两个极小的浮点数之间还有可表示的值避免直接下溢为 0。代价是表示精度下降。运算性能通常比规格化数慢很多。某些硬件对非规格化数的处理策略不同可能导致速度骤降。在嵌入式、DSP 和 GPU 上如果出现大量非规格化数程序可能突然变慢。很多高性能库会开启“将非规格化数冲刷为 0”flush-to-zero的选项换取速度一致性但代价是极小值被直接截断为 0。3.3 规格化过程对开发的意义理解规格化之后你就能解释两个常见现象第一浮点数能表示的最大有效数字位数是固定的不管你用什么指数。比如 FP32 用户看到123456789.0和123456792.0可能是同一个浮点数因为超过 7 位有效数字后后面的位数已经被格式吃掉。第二代码里判断两个浮点数相等不能直接使用。因为表示误差的存在看似相同的计算结果在底层可能是不同的二进制。a 0.1 0.2 b 0.3 print(a b) # False print(abs(a - b)) # 5.551115123125783e-17这类问题不是 Python 独有C、C、Java、Rust、JS 都会遇到因为底层规范是同一套。规避方法是使用绝对误差或相对误差判断或者直接用math.isclose。4. 常见浮点数陷阱盘点这部分是重点。浮点数的问题不止0.1 0.2实际项目里还有一批高频陷阱很多看起来像是“程序 bug”实际上都是浮点数特性导致的。4.1 大数吃小数当两个数量级差异很大的浮点数相加时较小的数可能因为精度不足而被“吃掉”。large 1e16 small 1.0 print(large small large) # True被吃掉了原因FP64 在1e16这个量级上相邻可表示数的间隔已经大于 1所以1e16 1无法被区分出来。在统计学、控制系统和物理模拟中这类“大数吃小数”会导致迭代结果不更新。规避思路不要直接在大数上累加小数。如果可以将计算分块或者用math.fsum这类精度更高的求和算法。4.2 两个相近数相减造成有效数字丢失如果两个几乎相等的浮点数相减结果的有效数字位数会急剧减少。x 1.0000000001 y 1.0000000000 print(x - y) # 1.0000000116860974e-10 左右前几位还有误差在数值计算中这叫“灾难性抵消”。比如计算sqrt(x1) - sqrt(x)时x 很大时直接相减会丢失大量精度更好的做法是改写成(sqrt(x1) - sqrt(x)) 1 / (sqrt(x1) sqrt(x))原则是尽量避免两个相近浮点数直接相减能转化就转化。4.3 累积误差浮点误差会随着计算次数累积。在循环里反复累加会因为舍入误差而偏离真实值。total 0.0 for _ in range(1000000): total 0.1 print(total) # 结果很可能不是 100000.0而是 99999.99999179...规避思路使用math.fsum进行高精度求和。数量级差异大的数据先排序再求和。对关键指标定期修正而不是完全依赖累加值。4.4 比较陷阱浮点数不能直接比较前面已经提到。还有两个比较场景很多人容易忽略。一个是与0的比较。浮点数可能出现-0.0它与0.0在某些语言里相等但在某些除法或位运算场景下会有区别。另一个是 NaN。NaN 不等于自身任何与 NaN 的比较都是 False这在排序、去重、数据库存储里都可能引起诡异问题。nan float(nan) print(nan nan) # False print(nan ! nan) # True4.5 溢出、下溢和除零FP32 最大约 3.4e38超过就是 Inf。FP16 最大只有 65504深度学习里经常出现梯度溢出。下溢则是结果太小直接变成 0。整型除法中除零会直接报错浮点除法中除零通常得到 Inf 或 NaN。在模型训练里如果 FP16 梯度出现大量 Inf 或 NaN最常见的原因就是下溢和溢出因此需要损失缩放Loss Scaling。4.6 打印问题“串口怎么打印浮点数”是嵌入式开发中的高频问题。C 语言中很多人直接用printf(%f\n, value);但单片机上如果启用浮点 printf代码体积和栈开销会变大。推荐的打印方案有使用snprintf并控制精度例如%.4f。将浮点数拆成整数部分和小数部分用整数方式发送接收端再合并。将浮点数按 IEEE 754 的 4 字节或 8 字节原始字节发送接收端用memcpy还原。使用十六进制转浮点的在线工具或脚本验证协议是否正确。在串口协议里建议明确约定字节序是大端还是小端是 ASCII 字符串还是原始二进制。很多串口调试问题最后发现不是浮点数算法的问题而是字节序或者转换函数写错了。5. 不同场景下的浮点数精度问题浮点数陷阱不是统一的不同领域表现差异很大。5.1 PLC 与工业控制器中的浮点精度工控场景里“三菱 PLC 浮点运算精度低”的问题很典型。PLC 的浮点数默认就是 32 位单精度这本身就有局限。如果在 PLC 里做长时间积分、累计计数或大数小数混合运算精度问题会被放大。排查思路确认 PLC 中变量是REAL32 位还是LREAL64 位。如果精度不满足改成 64 位浮点变量。在触摸屏或上位机上显示时设置合理的小数位数不要直接显示 7 位以上。对累计量做定期清零或补偿防止误差长期累积。与上位机通信时明确浮点字节序和寄存器映射。5.2 深度学习模型部署中的浮点格式选择深度学习部署是当前浮点数最密集的实战场景。部署时必须弄清楚FP32、FP16、BF16、TF32 到底有什么不同。格式总位数指数位尾数位动态范围精度典型用途FP3232823大约 7 位十进制常规训练、CPU 推理FP1616510小约 3 位十进制GPU 推理、混合精度训练BF161687和 FP32 相同约 3 位十进制大模型训练、部分推理TF3232 输入内部截断到 19 位810大中等NVIDIA Ampere 以上 GPU 的矩阵加速TF32 严格说不是一种存储格式而是 NVIDIA GPU 上的一种计算模式。它把 FP32 的尾数截断到 10 位用来提升矩阵乘法的吞吐量精度损失介于 FP32 和 FP16 之间但不需要像 FP16 那样处理动态范围极小的问题。选择建议如果你跑的是传统模型对精度敏感先保持 FP32。如果显存不够且模型对精度不太敏感FP16 是优先选择但必须做精度对比验证。如果训练大模型或动态范围很重要选 BF16 更好因为它和 FP32 的范围一致。如果用的是 Ampere 或更新的 NVIDIA GPUTF32 可以作为精度和速度的中间点。5.3 高精度计算工具在 Python 中如果 FP64 还不够可以用decimal.Decimal或fractions.Fraction。Julia 中则可以用 BigFloat 进行高精度浮点计算Julia 的特点是数值计算语法自然但也同样要注意中间结果的精度设置。Julia 示例using MPFR x BigFloat(0.1) y BigFloat(0.2) println(x y BigFloat(0.3)) # false需要设置精度这里也提示一个问题高精度不是自动的。即使你用了高精度浮点类型也要设置合理精度和检查舍入模式。6. 精度问题的排查与验证方法面对一个怀疑由浮点引起的 bug不要上来就怀疑编译器问题。建议按下面的流程走。6.1 复现最小案例把复杂计算简化成最小案例。比如神经网络输出不一致可以先构造一个固定输入、固定权重的单层网络单独跑一条数据对比输出。6.2 添加精度日志打印关键中间变量的值并且用足够多的位数打印。不要只打印默认 6 位小数因为 6 位小数根本无法暴露浮点误差。print(f{value:.17g})g格式会自动选择最紧凑的科学计数法17位对于 FP64 足够。6.3 对比不同精度的结果用 FP32 和 FP64 分别计算同一套输入观察差异大小。如果两者结果在期望公差内说明精度满足要求如果差异很大算法本身可能不稳定。6.4 使用标准判断函数不要用abs(a-b) 1e-6这种硬编码方式可以基于需求选择math.isclose(a, b)numpy.isclose(a, b)numpy.allclose(a, b)torch.allclose(a, b)对于深度学习建议设置atol和rtol并进行多组输入对比import torch def verify_precision(fn_a, fn_b, input_data): out_a fn_a(input_data) out_b fn_b(input_data) return torch.allclose(out_a, out_b, atol1e-4, rtol1e-3)atol表示绝对误差rtol表示相对误差。合理取值要看任务比如分类模型输出概率的误差容忍度可以松一些回归模型要严格一些。6.5 用数学工具分析误差界限常用numpy.finfo查看某类型的最小正数、最大有限数、十进制精度import numpy as np print(np.finfo(np.float32)) print(np.finfo(np.float64))输出里最关键的字段eps机器精度也就是 1.0 与下一个可表示的浮点数之间的差值。FP32 约1.19e-7FP64 约2.22e-16。tiny最小正规格化数。max最大有限数。6.6 二进制层面还原在调试协议时经常需要把浮点转成十六进制看。Python 可以使用如下方式import struct def float_to_hex(f): return hex(struct.unpack(I, struct.pack(f, f))[0]) print(float_to_hex(1.0)) # 0x3f800000上位机发送数据时如果出现乱码先把浮点转换后的字节打出来看是否与协议文档一致。很多“浮点传错”的问题最后都是大小端问题。7. 合理规避浮点数陷阱的编程实践这一节给出一套可以直接用的编程习惯能减少绝大部分浮点精度问题。7.1 数值计算上不要直接比较浮点相等使用isclose或误差阈值。大数累加小数时先对数据排序或者使用math.fsum。避免两个相近数直接相减尽量改写成稳定形式。对概率、比例类计算尽量转成对数域例如log_softmax和log_sum_exp。对极小概率做乘法时使用对数域防止下溢。在循环中定期清理累积误差不要把全部数值都靠累加得到。7.2 深度学习部署上先跑 FP32 基线再跑 FP16/BF16/TF32用同一组输入对比输出。查看推理输出的范围如果输出很小FP16 容易下溢如果输出接近 65504FP16 容易溢出。对 BatchNorm 或 LayerNorm 这类对精度敏感的结构关注统计量计算是否还在 FP32。如果模型权重很接近零BF16 的尾数劣势可能更明显FP16 可能更合适。这个要实测数据分布。不要忽略部署框架对低精度算子的支持差异。同一个模型在不同框架下混合精度策略可能不一样。以上对比结果都要留档方便后续更换硬件后再校验。合规上要注意如果模型是用公开或第三方数据训练的部署和商用前需要确认模型来源和授权范围。7.3 嵌入式与通信上使用浮点数传输时明确大小端和前导字节。发送小数时尽量用字符串加指定小数位简单且便于调试。接收端先做包序号和校验和再做浮点解析避免错位后解析出 NaN。对解析到的 NaN 和 Inf 做保护避免后续计算污染全部结果。8. 常见问题与排查方法这里整理一张针对浮点数场景的排查表。问题现象可能原因排查方式解决方案0.1 0.2 ! 0.3二进制近似误差打印更多位数使用math.isclose循环累加结果偏差大舍入误差累积用math.fsum对比改用高精度求和或定期修正大数加小数结果不变大数量级下相邻浮点间隔过大打印大数的np.nextafter差值分块求和或先排序PLC 运算结果低PLC 变量是 32 位 REAL确认变量类型和扫描周期改用 64 位 LREAL 或调整算法串口打印浮点乱码字节序或格式不一致打印浮点原始十六进制按协议统一大小端和字节序FP16 训练梯度 Inf/NaNFP16 动态范围太小查看梯度最大值开启损失缩放或改用 BF16模型 FP16 推理精度下降权重或激活动态范围大对比 FP32 输出并检查输出分布尝试 BF16/TF32 或混合精度策略解析到 NaN 后结果全错NaN 会传播对输入数据做合法性校验检测并跳过 NaN/Inf排查原则可以记成一句话先确认是不是浮点数表示误差再确认是否算法不稳定最后再考虑换数据类型。9. 浮点数教学与自学建议这个标题看起来像“4-浮点数和陷进”很可能是一节课程或阅读材料的一小节。如果是为了学习或讲解建议按下面顺序展开先用实际代码演示0.1 0.2的问题让读者产生直观认识。再讲 IEEE 754 的存储结构重点讲符号位、指数位、尾数位。然后讲规格化浮点数以及隐含的 1这能解释很多人看不懂十六进制浮点表示的问题。接着做一次十进制二进制转换实操。最后配合代码练习比较、求和、打印高精度、协议解析。学习时最容易踩的坑是“只看公式不上手”。建议每学一个知识点立刻写一段 10 行以内的代码验证尤其是import math print(math.isclose(0.1 0.2, 0.3, rel_tol1e-9)) print(f{0.1 0.2:.17g}) print(math.ulp(1e16))这样比记一百个结论都有效。10. 总结与下一步浮点数这块内容最值得记住的三件事是第一浮点数是二进制近似不是精确实数第二规格化浮点数决定了有效数字的上限数据类型不同能用的动态范围和精度不同第三比较、累加、相减、溢出和协议解析是实际开发中最容易出现问题的五个位置。换类型之前先确认算法本身是否稳定再决定用 FP32、FP16、BF16 还是 TF32。建议把这篇文章当成一张排查清单保存。下次遇到精度问题直接对照第 8 节的表格从复现最小案例开始打印更高位数的中间值对比不同数据类型的输出差异。浮点数这个主题虽然基础一旦能熟练定位精度问题你在嵌入式、控制系统还是 AI 部署里都会少走很多弯路。
返回列表