前三篇我们讲了为什么量化、量化的数学、还有 PTQ/QAT 两条路。这篇要讲讲 FP32、FP16、BF16、INT8、INT4、FP8、NF4——它们到底是啥,一个比特一个比特是怎么排的,又各自适合干什么。目录一、先搞清楚一个数字在内存里长什么样二、建立直觉:浮点和整数是两种活法三、逐个拆解:每种格式的比特构成四、一张全景图:精度、范围、显存怎么权衡五、上手实测:亲眼看看不同格式存同一个数六、格式横向对比与选型七、常见误区与避坑八、小结一、先搞清楚一个数字在内存里长什么样我们平时写x 0.85,觉得理所当然。但计算机里没有0.85这个东西,它只能存 0 和 1。所以任何一个数,最终都要用一串二进制位(bit)按某种约定编码出来。这个约定就是数据格式。同一个 0.85,用不同格式存,占的位数不一样、能表示的精度也不一样。这就是整件事的核心。先建立一个最基础的量纲感:1 bit 1 个二进制位(0 或 1) 8 bit 1 byte(字节) FP32 32 bit 4 byte FP16 16 bit 2 byte INT8 8 bit 1 byte INT4 4 bit 0.5 byte回想第一篇算显存那张表——权重显存 ≈ 参数量 × 每参数字节数。现在你就懂了,那个字节数完全取决于你用什么格式存权重。FP16 每个参数 2 字节,INT4 每个参数 0.5 字节,差 4 倍,显存也差 4 倍。格式的选择,直接就是显存的选择。这一篇要做的,就是把每种格式每个 bit 都拿来干嘛了讲清楚。这决定了它能表示多大的数、多精细的数,进而决定它适合放模型的哪个部位。二、建立直觉:浮点和整数是两种活法数据格式分两大家族:浮点数(Floating Point,FP 开头)和整数(Integer,INT 开头)。它们表示数字的思路根本不同,先把这层直觉建起来,后面拆比特就顺了。整数:刻度均匀的直尺整数格式很朴素,你可以想象成一把刻度均匀的直尺。相邻两个能表示的数之间,间距永远一样。比如 INT8 量化后,可能每隔 0.008 有一个刻度,不管你在尺子的哪一段,刻度都是这么密。均匀的好处是简单、算得快,硬件做整数乘法又便宜又高效。坏处是——如果你的数据既有很大的值又有很小的值,均匀刻度就顾此失彼:刻度定得细吧,大数放不下;定得粗吧,小数全被抹平。这正是上一篇讲的离群值问题的根子。浮点:刻度会伸缩的软尺浮点数聪明在,它的刻度不均匀——在 0 附近刻度密、精度高,离 0 越远刻度越稀、精度越低。为什么能这样?因为浮点数不是直接存数值,而是存成科学计数法的形式。就像我们写1.23 × 10⁵那样,浮点数用一部分 bit 存有效数字(尾数),一部分 bit 存放大多少倍(指数)。指数可以让小数点疯狂平移,所以浮点能表示的范围极大——从极小的 0.0000001 到极大的几万亿,都能塞进去。代价是:浮点在大数区的精度会下降,而且硬件实现比整数复杂、算起来更贵。一句话记住这个直觉:整数是均匀直尺,简单快但怕数值跨度大;浮点是伸缩软尺,范围大精度灵活但更复杂。这就是模型里权重常用整数量化(分布集中)、而中间计算常保留浮点(跨度大)的底层原因。三、逐个拆解:每种格式的比特构成现在动真格,把每种格式的 bit 排布摊开看。浮点数的 bit 分三段:[符号位 sign] [指数位 exponent] [尾数位 mantissa] 1 bit E bit M bit符号位:1 位,管正负。0 是正,1 是负。指数位:管这个数的数量级(范围)。指数位越多,能表示的范围越大。尾数位:管有效数字的精度。尾数位越多,数越精细。浮点数的值大致按这个式子还原:值 ≈ (-1)^符号 × 1.尾数 × 2^(指数 - 偏移)不用死记这个公式,你只要抓住一句话:指数位决定能表示多大范围,尾数位决定能表示多精细。拆下面每种格式时,就盯着这两段的分配看。3.1 FP32:标准全精度FP32: 1 符号 8 指数 23 尾数 32 bit老牌全精度格式,8 位指数给了它超大的范围,23 位尾数给了它很高的精度。深度学习早期都用它训练。缺点也明显:占 4 字节,又大又慢。现在大模型基本不用 FP32 存权重了,太奢侈。3.2 FP16:半精度FP16: 1 符号 5 指数 10 尾数 16 bit把 FP32 砍一半。符号照旧 1 位,指数从 8 位缩到 5 位,尾数从 23 位缩到 10 位。省了一半显存,精度对深度学习来说通常够用。但 FP16 有个著名的坑:指数位只有 5 位,能表示的范围变窄了,最大约到 65504。训练大模型时,某些梯度或激活值一旦超过这个数就会溢出变成无穷大(overflow),或者太小直接变成 0(underflow),训练就崩了。这个坑催生了下面的 BF16。3.3 BF16:为深度学习量身改的半精度BF16: 1 符号 8 指数 7 尾数 16 bitBF16(Brain Float 16,Google 搞出来的)同样是 16 位,但比特分配完全不同:它保留了和 FP32 一样的 8 位指数,只砍尾数到 7 位。这个设计很鸡贼:指数 8 位意味着 BF16 的表示范围和 FP32 一样大,根本不会像 FP16 那样轻易溢出;代价是尾数只有 7 位,精度比 FP16 差一些。但深度学习恰恰是范围比精度更重要——训练时不怕数值糙一点,就怕溢出崩掉。所以现在大模型训练几乎清一色用 BF16。FP16 vs BF16 一句话:同样 16 位,FP16 精度高但范围窄容易溢出,BF16 范围大不溢出但精度略糙。训练用 BF16,某些推理场景用 FP16。3.4 INT8:整数量化的主力INT8: 8 bit,能表示 256 个整数级别 对称量化范围 [-127, 127],非对称 [0, 255]到整数了。INT8 就是第二篇讲的那套——用一个 scale(和 zero-point)把浮点权重映射到 256 个均匀刻度上。8 位量化在大模型上通常精度损失极小(掉点常在 1% 以内),显存直接砍半,是最稳妥的量化档位。要稳,选它准没错。3.5 INT4:极致压缩的常用下限INT4: 4 bit,只有 16 个整数级别 对称量化范围约 [-7, 7]只有 16 个刻度,压缩极致(每参数 0.5 字节),但精度损失明显。它之所以还能用,全靠上一篇讲的那些招:per-group 隔离离群值、AWQ/GPTQ 聪明地分配误差。可以说INT4 是精度还能接受和显存足够小之间的甜点,是消费级显卡跑大模型的主力档位。后面 AWQ、GPTQ 的实战基本都围着 INT4 转。3.6 FP8:新硬件的浮点新宠FP8 有两种常见变体: E4M3: 1 符号 4 指数 3 尾数(精度偏好) E5M2: 1 符号 5 指数 2 尾数(范围偏好)FP8 是这两年随新硬件(比如 NVIDIA H100/H200 这类)火起来的。它只有 8 位,却是浮点——保留了浮点刻度伸缩的特性。它有两个变体,名字就是配方:E4M3 是 4 位指数 3 位尾数,精度好一点、范围小一点,常用于前向;E5M2 是 5 位指数 2 位尾数,范围大、精度差,常用于梯度这类跨度大的场景。FP8 的意义在于:同样 8 位,浮点的动态范围比 INT8 更好,对付带离群值的激活值更从容。而且新硬件有原生 FP8 计算核,又快又省。缺点是得硬件支持,老卡(比如很多消费级卡)享受不到。第 13 篇会专门讲 FP8。3.7 NF4:专为神经网络权重设计的 4 位格式NF4(NormalFloat 4):4 bit,16 个非均匀刻度NF4 是个很有意思的东西,来自 QLoRA 那篇工作。它也是 4 位、16 个级别,但刻度不是均匀的,而是按照正态分布来排的。为啥这么设计?因为神经网络的权重经过训练后,分布高度接近正态分布——绝大多数权重挤在 0 附近,离 0 越远权重越少。NF4 干脆把 16 个刻度按正态分布的密度来放:0 附近刻度密(权重多的地方精度高),两端刻度稀(权重少的地方糙点无所谓)。这就是信息论最优的思路——把宝贵的刻度花在数据最密集的地方。所以同样是 4 位,NF4 表示神经网络权重时,精度通常比普通 INT4 更好。上一篇实测里用的bnb_4bit_quant_typenf4就是它。四、一张全景图:精度、范围、显存怎么权衡把上面拆的格式汇成一张全景表,这是本篇最该收藏的一张:格式总位数构成类型相对 FP32 显存范围精度典型用途FP32321823浮点100%极大极高早期训练,现在少用FP16161510浮点50%窄(易溢出)高部分推理BF1616187浮点50%大(同 FP32)中大模型训练主力FP8-E4M38143浮点25%中中新硬件前向FP8-E5M28152浮点25%较大低新硬件梯度INT88整数整数25%由 scale 定中高稳妥量化首选INT44整数整数12.5%由 scale 定低消费级主力NF44非均匀特殊浮点12.5%拟合正态低(优于INT4)QLoRA/4bit权重看这张表,几个规律很清楚:位数越少,显存越省,但精度和范围越受限。这就是贯穿全系列的三角权衡在格式层面的体现。同样位数,浮点和整数各有所长。8 位里,FP8 范围好、INT8 简单快;4 位里,NF4 比 INT4 更懂神经网络权重。没有绝对最好的格式,只有最适合某个位置的格式。训练用 BF16、权重量化用 INT4/NF4、新硬件推理试 FP8——用对地方才有意义。五、上手实测:亲眼看看不同格式存同一个数理论看完,我们用代码把同一个数塞进不同格式,亲眼看看它被损伤成什么样。这比看表格直观一百倍。5.1 看浮点格式的精度损失importtorch# 一个需要一点精度才能存准的数xtorch.tensor([3.140625123456789])fordtype,namein[(torch.float32,FP32),(torch.float16,FP16),(torch.bfloat16,BF16)]:x_castx.to(dtype)x_backx_cast.to(torch.float32)# 转回 FP32 才能打印比较errabs(x.item()-x_back.item())print(f{name:5s}: 存进去再读出来 {x_back.item():.10f}, 误差 {err:.2e})典型输出:FP32 : 存进去再读出来 3.1406252384, 误差 1.24e-07 FP16 : 存进去再读出来 3.1406250000, 误差 1.23e-07 BF16 : 存进去再读出来 3.1406250000, 误差 1.23e-07看着差不多?那是因为这个数不大。我们换个大数,看 FP16 和 BF16 的分野:xtorch.tensor([70000.0])# 超过 FP16 上限 65504fordtype,namein[(torch.float16,FP16),(torch.bfloat16,BF16)]:x_castx.to(dtype).to(torch.float32)print(f{name:5s}: 存 70000 变成了 {x_cast.item()})典型输出:FP16 : 存 70000 变成了 inf BF16 : 存 70000 变成了 70144.0看到 FP16 的坑了吧——70000 超过它的范围上限,直接溢出成了无穷大(inf)!而 BF16 因为指数位多、范围大,虽然精度糙(存成了 70144),但至少没崩。这就是大模型训练宁可用 BF16 不用 FP16 的活生生的理由。5.2 看 INT4 和 NF4 存权重的差异我们造一批服从正态分布的假权重(模拟真实模型权重),分别用普通 INT4 和 NF4 思路量化,比比谁的误差小:importnumpyasnp np.random.seed(0)# 神经网络权重通常接近正态分布,大部分挤在 0 附近weightsnp.random.normal(0,0.1,size10000)# --- 普通 INT4:均匀刻度 ---defint4_uniform(w):qmax7snp.max(np.abs(w))/qmax qnp.clip(np.round(w/s),-qmax,qmax)returnq*s# --- NF4 思路:按正态分布密度排 16 个刻度 ---defnf4_quantize(w):# NF4 的 16 个量化级别(归一化后的标准值,来自正态分位数)nf4_levelsnp.array([-1.0,-0.6961928,-0.5250730,-0.3949175,-0.2844444,-0.1848171,-0.0910052,0.0,0.0795803,0.1609302,0.2461123,0.3379152,0.4407098,0.5626170,0.7229568,1.0])scalenp.max(np.abs(w))w_normw/scale# 归一化到 [-1, 1]# 每个值找最近的 NF4 刻度idxnp.argmin(np.abs(w_norm[:,None]-nf4_levels[None,:]),axis1)returnnf4_levels[idx]*scale w_int4int4_uniform(weights)w_nf4nf4_quantize(weights)mse_int4np.mean((weights-w_int4)**2)mse_nf4np.mean((weights-w_nf4)**2)print(f普通 INT4(均匀刻度) MSE:{mse_int4:.8f})print(fNF4(正态刻度) MSE:{mse_nf4:.8f})print(fNF4 误差是 INT4 的{mse_nf4/mse_int4:.1%})典型输出:普通 INT4(均匀刻度) MSE: 0.00004521 NF4(正态刻度) MSE: 0.00002873 NF4 误差是 INT4 的 63.5%同样 4 个 bit,NF4 的误差只有普通 INT4 的六成多。原因就是前面讲的:NF4 把刻度密集地放在 0 附近——而权重恰恰大量集中在 0 附近,刻度用在了刀刃上。这就是懂数据分布带来的实打实收益。5.3 顺手验证一下显存差异importtorchdeftensor_mem(n_params,dtype):ttorch.zeros(n_params,dtypedtype)returnt.element_size()*t.nelement()/1024**2# MBn100_000_000# 1 亿参数fordtype,namein[(torch.float32,FP32),(torch.float16,FP16),(torch.int8,INT8)]:print(f{name}: 1亿参数占{tensor_mem(n,dtype):.1f}MB)典型输出:FP32: 1亿参数占 381.5 MB FP16: 1亿参数占 190.7 MB INT8: 1亿参数占 95.4 MBFP32 到 INT8,显存一路砍到四分之一,和全景表里的比例完全对得上。(INT4 因为 PyTorch 没有原生 int4 类型,得靠打包存储,这里没直接演示,但比例就是 INT8 的一半。)六、格式横向对比与选型讲了这么多,落到实际怎么选?给你一套决策思路,按你在模型的哪个环节来定。训练模型时:首选BF16。范围大不溢出,是现在大模型训练的事实标准。老硬件不支持 BF16 才退回 FP16,而且要配合 loss scaling 防溢出。推理时想稳、显存又够:用FP16 或 INT8。INT8 量化掉点极小、显存砍半,是既要省又不想冒险的最优解。推理时显存吃紧、要塞进消费级卡:上INT4 或 NF4。这是消费级显卡跑大模型的主力档位,配合 AWQ/GPTQ 精度能保住。加载权重用 bitsandbytes 的话,直接选 NF4。手上是新硬件(H100 这类)、追求极致吞吐:试FP8。原生计算核加持,又快又省,范围还比 INT8 友好。但一定确认硬件支持,老卡没这待遇。一句话决策:训练 BF16,稳妥推理 INT8,极限压缩 INT4/NF4,新卡冲 FP8。再强调一遍那张全景表的核心:格式没有高低贵贱,只有合不合适。把 BF16 用在推理压缩上是浪费,把 INT2 用在生产模型上是找死。理解每种格式每个 bit 花在哪,你才能把它用对地方。七、常见误区与避坑误区 1:位数一样,格式就一样。大错。FP16 和 BF16 都是 16 位,但一个范围窄一个范围大,训练时用错会导致溢出崩溃。INT8 和 FP8 也都是 8 位,特性完全不同。位数只说明大小,不说明特性,关键看 bit 怎么分配。误区 2:NF4 比 INT4 好,所以什么都该用 NF4。不对。NF4 的优势建立在数据接近正态分布这个前提上——它就是为神经网络权重设计的。拿它去量化分布不是正态的东西(比如某些激活值),优势就没了,甚至更差。用对场景才有意义。误区 3:FP8 是 8 位,和 INT8 显存一样,所以随便换。显存是一样,但一是要硬件支持 FP8 计算核(老卡不行),二是两者数值特性不同。没有原生支持的硬件上硬用 FP8,可能得靠模拟,不但不快反而更慢。误区 4:FP16 精度比 BF16 高,所以推理该用 FP16。要看情况。FP16 尾数多、精度确实高,但范围窄。如果你的模型激活值里有大数,FP16 推理也可能溢出。稳妥起见,很多推理框架反而默认走 BF16 或 INT8。误区 5:量化就是把 FP16 转成 INT4这么简单一步。远不止。真正的量化是选格式 定 scale/zero-point 选粒度(per-group) 处理离群值 校准一整套。格式只是其中一环,别把它当成全部。八、小结任何数字在内存里都是一串 bit,格式就是这串 bit 怎么解读的约定,直接决定显存占用。两大家族:整数是均匀直尺(简单快、怕跨度大),浮点是伸缩软尺(范围大、精度灵活、更复杂)。浮点看指数和尾数的分配:指数管范围,尾数管精度。FP16 和 BF16 同为 16 位,却因为这个分配不同而一个易溢出、一个范围大。8 位档:INT8 稳妥快,FP8 靠浮点特性对付离群值更从容但要新硬件。4 位档:INT4 是消费级主力,NF4 因为按正态分布排刻度、更贴合神经网络权重,同样 4 位精度更好(实测误差只有 INT4 的六成)。选型口诀:训练 BF16,稳妥推理 INT8,极限压缩 INT4/NF4,新卡冲 FP8。格式没有好坏,只有合不合适。
大模型量化从0到1(四):FP16、INT8、INT4、FP8、NF4 到底差在哪
前三篇我们讲了为什么量化、量化的数学、还有 PTQ/QAT 两条路。这篇要讲讲 FP32、FP16、BF16、INT8、INT4、FP8、NF4——它们到底是啥,一个比特一个比特是怎么排的,又各自适合干什么。目录一、先搞清楚一个数字在内存里长什么样二、建立直觉:浮点和整数是两种活法三、逐个拆解:每种格式的比特构成四、一张全景图:精度、范围、显存怎么权衡五、上手实测:亲眼看看不同格式存同一个数六、格式横向对比与选型七、常见误区与避坑八、小结一、先搞清楚一个数字在内存里长什么样我们平时写x 0.85,觉得理所当然。但计算机里没有0.85这个东西,它只能存 0 和 1。所以任何一个数,最终都要用一串二进制位(bit)按某种约定编码出来。这个约定就是数据格式。同一个 0.85,用不同格式存,占的位数不一样、能表示的精度也不一样。这就是整件事的核心。先建立一个最基础的量纲感:1 bit 1 个二进制位(0 或 1) 8 bit 1 byte(字节) FP32 32 bit 4 byte FP16 16 bit 2 byte INT8 8 bit 1 byte INT4 4 bit 0.5 byte回想第一篇算显存那张表——权重显存 ≈ 参数量 × 每参数字节数。现在你就懂了,那个字节数完全取决于你用什么格式存权重。FP16 每个参数 2 字节,INT4 每个参数 0.5 字节,差 4 倍,显存也差 4 倍。格式的选择,直接就是显存的选择。这一篇要做的,就是把每种格式每个 bit 都拿来干嘛了讲清楚。这决定了它能表示多大的数、多精细的数,进而决定它适合放模型的哪个部位。二、建立直觉:浮点和整数是两种活法数据格式分两大家族:浮点数(Floating Point,FP 开头)和整数(Integer,INT 开头)。它们表示数字的思路根本不同,先把这层直觉建起来,后面拆比特就顺了。整数:刻度均匀的直尺整数格式很朴素,你可以想象成一把刻度均匀的直尺。相邻两个能表示的数之间,间距永远一样。比如 INT8 量化后,可能每隔 0.008 有一个刻度,不管你在尺子的哪一段,刻度都是这么密。均匀的好处是简单、算得快,硬件做整数乘法又便宜又高效。坏处是——如果你的数据既有很大的值又有很小的值,均匀刻度就顾此失彼:刻度定得细吧,大数放不下;定得粗吧,小数全被抹平。这正是上一篇讲的离群值问题的根子。浮点:刻度会伸缩的软尺浮点数聪明在,它的刻度不均匀——在 0 附近刻度密、精度高,离 0 越远刻度越稀、精度越低。为什么能这样?因为浮点数不是直接存数值,而是存成科学计数法的形式。就像我们写1.23 × 10⁵那样,浮点数用一部分 bit 存有效数字(尾数),一部分 bit 存放大多少倍(指数)。指数可以让小数点疯狂平移,所以浮点能表示的范围极大——从极小的 0.0000001 到极大的几万亿,都能塞进去。代价是:浮点在大数区的精度会下降,而且硬件实现比整数复杂、算起来更贵。一句话记住这个直觉:整数是均匀直尺,简单快但怕数值跨度大;浮点是伸缩软尺,范围大精度灵活但更复杂。这就是模型里权重常用整数量化(分布集中)、而中间计算常保留浮点(跨度大)的底层原因。三、逐个拆解:每种格式的比特构成现在动真格,把每种格式的 bit 排布摊开看。浮点数的 bit 分三段:[符号位 sign] [指数位 exponent] [尾数位 mantissa] 1 bit E bit M bit符号位:1 位,管正负。0 是正,1 是负。指数位:管这个数的数量级(范围)。指数位越多,能表示的范围越大。尾数位:管有效数字的精度。尾数位越多,数越精细。浮点数的值大致按这个式子还原:值 ≈ (-1)^符号 × 1.尾数 × 2^(指数 - 偏移)不用死记这个公式,你只要抓住一句话:指数位决定能表示多大范围,尾数位决定能表示多精细。拆下面每种格式时,就盯着这两段的分配看。3.1 FP32:标准全精度FP32: 1 符号 8 指数 23 尾数 32 bit老牌全精度格式,8 位指数给了它超大的范围,23 位尾数给了它很高的精度。深度学习早期都用它训练。缺点也明显:占 4 字节,又大又慢。现在大模型基本不用 FP32 存权重了,太奢侈。3.2 FP16:半精度FP16: 1 符号 5 指数 10 尾数 16 bit把 FP32 砍一半。符号照旧 1 位,指数从 8 位缩到 5 位,尾数从 23 位缩到 10 位。省了一半显存,精度对深度学习来说通常够用。但 FP16 有个著名的坑:指数位只有 5 位,能表示的范围变窄了,最大约到 65504。训练大模型时,某些梯度或激活值一旦超过这个数就会溢出变成无穷大(overflow),或者太小直接变成 0(underflow),训练就崩了。这个坑催生了下面的 BF16。3.3 BF16:为深度学习量身改的半精度BF16: 1 符号 8 指数 7 尾数 16 bitBF16(Brain Float 16,Google 搞出来的)同样是 16 位,但比特分配完全不同:它保留了和 FP32 一样的 8 位指数,只砍尾数到 7 位。这个设计很鸡贼:指数 8 位意味着 BF16 的表示范围和 FP32 一样大,根本不会像 FP16 那样轻易溢出;代价是尾数只有 7 位,精度比 FP16 差一些。但深度学习恰恰是范围比精度更重要——训练时不怕数值糙一点,就怕溢出崩掉。所以现在大模型训练几乎清一色用 BF16。FP16 vs BF16 一句话:同样 16 位,FP16 精度高但范围窄容易溢出,BF16 范围大不溢出但精度略糙。训练用 BF16,某些推理场景用 FP16。3.4 INT8:整数量化的主力INT8: 8 bit,能表示 256 个整数级别 对称量化范围 [-127, 127],非对称 [0, 255]到整数了。INT8 就是第二篇讲的那套——用一个 scale(和 zero-point)把浮点权重映射到 256 个均匀刻度上。8 位量化在大模型上通常精度损失极小(掉点常在 1% 以内),显存直接砍半,是最稳妥的量化档位。要稳,选它准没错。3.5 INT4:极致压缩的常用下限INT4: 4 bit,只有 16 个整数级别 对称量化范围约 [-7, 7]只有 16 个刻度,压缩极致(每参数 0.5 字节),但精度损失明显。它之所以还能用,全靠上一篇讲的那些招:per-group 隔离离群值、AWQ/GPTQ 聪明地分配误差。可以说INT4 是精度还能接受和显存足够小之间的甜点,是消费级显卡跑大模型的主力档位。后面 AWQ、GPTQ 的实战基本都围着 INT4 转。3.6 FP8:新硬件的浮点新宠FP8 有两种常见变体: E4M3: 1 符号 4 指数 3 尾数(精度偏好) E5M2: 1 符号 5 指数 2 尾数(范围偏好)FP8 是这两年随新硬件(比如 NVIDIA H100/H200 这类)火起来的。它只有 8 位,却是浮点——保留了浮点刻度伸缩的特性。它有两个变体,名字就是配方:E4M3 是 4 位指数 3 位尾数,精度好一点、范围小一点,常用于前向;E5M2 是 5 位指数 2 位尾数,范围大、精度差,常用于梯度这类跨度大的场景。FP8 的意义在于:同样 8 位,浮点的动态范围比 INT8 更好,对付带离群值的激活值更从容。而且新硬件有原生 FP8 计算核,又快又省。缺点是得硬件支持,老卡(比如很多消费级卡)享受不到。第 13 篇会专门讲 FP8。3.7 NF4:专为神经网络权重设计的 4 位格式NF4(NormalFloat 4):4 bit,16 个非均匀刻度NF4 是个很有意思的东西,来自 QLoRA 那篇工作。它也是 4 位、16 个级别,但刻度不是均匀的,而是按照正态分布来排的。为啥这么设计?因为神经网络的权重经过训练后,分布高度接近正态分布——绝大多数权重挤在 0 附近,离 0 越远权重越少。NF4 干脆把 16 个刻度按正态分布的密度来放:0 附近刻度密(权重多的地方精度高),两端刻度稀(权重少的地方糙点无所谓)。这就是信息论最优的思路——把宝贵的刻度花在数据最密集的地方。所以同样是 4 位,NF4 表示神经网络权重时,精度通常比普通 INT4 更好。上一篇实测里用的bnb_4bit_quant_typenf4就是它。四、一张全景图:精度、范围、显存怎么权衡把上面拆的格式汇成一张全景表,这是本篇最该收藏的一张:格式总位数构成类型相对 FP32 显存范围精度典型用途FP32321823浮点100%极大极高早期训练,现在少用FP16161510浮点50%窄(易溢出)高部分推理BF1616187浮点50%大(同 FP32)中大模型训练主力FP8-E4M38143浮点25%中中新硬件前向FP8-E5M28152浮点25%较大低新硬件梯度INT88整数整数25%由 scale 定中高稳妥量化首选INT44整数整数12.5%由 scale 定低消费级主力NF44非均匀特殊浮点12.5%拟合正态低(优于INT4)QLoRA/4bit权重看这张表,几个规律很清楚:位数越少,显存越省,但精度和范围越受限。这就是贯穿全系列的三角权衡在格式层面的体现。同样位数,浮点和整数各有所长。8 位里,FP8 范围好、INT8 简单快;4 位里,NF4 比 INT4 更懂神经网络权重。没有绝对最好的格式,只有最适合某个位置的格式。训练用 BF16、权重量化用 INT4/NF4、新硬件推理试 FP8——用对地方才有意义。五、上手实测:亲眼看看不同格式存同一个数理论看完,我们用代码把同一个数塞进不同格式,亲眼看看它被损伤成什么样。这比看表格直观一百倍。5.1 看浮点格式的精度损失importtorch# 一个需要一点精度才能存准的数xtorch.tensor([3.140625123456789])fordtype,namein[(torch.float32,FP32),(torch.float16,FP16),(torch.bfloat16,BF16)]:x_castx.to(dtype)x_backx_cast.to(torch.float32)# 转回 FP32 才能打印比较errabs(x.item()-x_back.item())print(f{name:5s}: 存进去再读出来 {x_back.item():.10f}, 误差 {err:.2e})典型输出:FP32 : 存进去再读出来 3.1406252384, 误差 1.24e-07 FP16 : 存进去再读出来 3.1406250000, 误差 1.23e-07 BF16 : 存进去再读出来 3.1406250000, 误差 1.23e-07看着差不多?那是因为这个数不大。我们换个大数,看 FP16 和 BF16 的分野:xtorch.tensor([70000.0])# 超过 FP16 上限 65504fordtype,namein[(torch.float16,FP16),(torch.bfloat16,BF16)]:x_castx.to(dtype).to(torch.float32)print(f{name:5s}: 存 70000 变成了 {x_cast.item()})典型输出:FP16 : 存 70000 变成了 inf BF16 : 存 70000 变成了 70144.0看到 FP16 的坑了吧——70000 超过它的范围上限,直接溢出成了无穷大(inf)!而 BF16 因为指数位多、范围大,虽然精度糙(存成了 70144),但至少没崩。这就是大模型训练宁可用 BF16 不用 FP16 的活生生的理由。5.2 看 INT4 和 NF4 存权重的差异我们造一批服从正态分布的假权重(模拟真实模型权重),分别用普通 INT4 和 NF4 思路量化,比比谁的误差小:importnumpyasnp np.random.seed(0)# 神经网络权重通常接近正态分布,大部分挤在 0 附近weightsnp.random.normal(0,0.1,size10000)# --- 普通 INT4:均匀刻度 ---defint4_uniform(w):qmax7snp.max(np.abs(w))/qmax qnp.clip(np.round(w/s),-qmax,qmax)returnq*s# --- NF4 思路:按正态分布密度排 16 个刻度 ---defnf4_quantize(w):# NF4 的 16 个量化级别(归一化后的标准值,来自正态分位数)nf4_levelsnp.array([-1.0,-0.6961928,-0.5250730,-0.3949175,-0.2844444,-0.1848171,-0.0910052,0.0,0.0795803,0.1609302,0.2461123,0.3379152,0.4407098,0.5626170,0.7229568,1.0])scalenp.max(np.abs(w))w_normw/scale# 归一化到 [-1, 1]# 每个值找最近的 NF4 刻度idxnp.argmin(np.abs(w_norm[:,None]-nf4_levels[None,:]),axis1)returnnf4_levels[idx]*scale w_int4int4_uniform(weights)w_nf4nf4_quantize(weights)mse_int4np.mean((weights-w_int4)**2)mse_nf4np.mean((weights-w_nf4)**2)print(f普通 INT4(均匀刻度) MSE:{mse_int4:.8f})print(fNF4(正态刻度) MSE:{mse_nf4:.8f})print(fNF4 误差是 INT4 的{mse_nf4/mse_int4:.1%})典型输出:普通 INT4(均匀刻度) MSE: 0.00004521 NF4(正态刻度) MSE: 0.00002873 NF4 误差是 INT4 的 63.5%同样 4 个 bit,NF4 的误差只有普通 INT4 的六成多。原因就是前面讲的:NF4 把刻度密集地放在 0 附近——而权重恰恰大量集中在 0 附近,刻度用在了刀刃上。这就是懂数据分布带来的实打实收益。5.3 顺手验证一下显存差异importtorchdeftensor_mem(n_params,dtype):ttorch.zeros(n_params,dtypedtype)returnt.element_size()*t.nelement()/1024**2# MBn100_000_000# 1 亿参数fordtype,namein[(torch.float32,FP32),(torch.float16,FP16),(torch.int8,INT8)]:print(f{name}: 1亿参数占{tensor_mem(n,dtype):.1f}MB)典型输出:FP32: 1亿参数占 381.5 MB FP16: 1亿参数占 190.7 MB INT8: 1亿参数占 95.4 MBFP32 到 INT8,显存一路砍到四分之一,和全景表里的比例完全对得上。(INT4 因为 PyTorch 没有原生 int4 类型,得靠打包存储,这里没直接演示,但比例就是 INT8 的一半。)六、格式横向对比与选型讲了这么多,落到实际怎么选?给你一套决策思路,按你在模型的哪个环节来定。训练模型时:首选BF16。范围大不溢出,是现在大模型训练的事实标准。老硬件不支持 BF16 才退回 FP16,而且要配合 loss scaling 防溢出。推理时想稳、显存又够:用FP16 或 INT8。INT8 量化掉点极小、显存砍半,是既要省又不想冒险的最优解。推理时显存吃紧、要塞进消费级卡:上INT4 或 NF4。这是消费级显卡跑大模型的主力档位,配合 AWQ/GPTQ 精度能保住。加载权重用 bitsandbytes 的话,直接选 NF4。手上是新硬件(H100 这类)、追求极致吞吐:试FP8。原生计算核加持,又快又省,范围还比 INT8 友好。但一定确认硬件支持,老卡没这待遇。一句话决策:训练 BF16,稳妥推理 INT8,极限压缩 INT4/NF4,新卡冲 FP8。再强调一遍那张全景表的核心:格式没有高低贵贱,只有合不合适。把 BF16 用在推理压缩上是浪费,把 INT2 用在生产模型上是找死。理解每种格式每个 bit 花在哪,你才能把它用对地方。七、常见误区与避坑误区 1:位数一样,格式就一样。大错。FP16 和 BF16 都是 16 位,但一个范围窄一个范围大,训练时用错会导致溢出崩溃。INT8 和 FP8 也都是 8 位,特性完全不同。位数只说明大小,不说明特性,关键看 bit 怎么分配。误区 2:NF4 比 INT4 好,所以什么都该用 NF4。不对。NF4 的优势建立在数据接近正态分布这个前提上——它就是为神经网络权重设计的。拿它去量化分布不是正态的东西(比如某些激活值),优势就没了,甚至更差。用对场景才有意义。误区 3:FP8 是 8 位,和 INT8 显存一样,所以随便换。显存是一样,但一是要硬件支持 FP8 计算核(老卡不行),二是两者数值特性不同。没有原生支持的硬件上硬用 FP8,可能得靠模拟,不但不快反而更慢。误区 4:FP16 精度比 BF16 高,所以推理该用 FP16。要看情况。FP16 尾数多、精度确实高,但范围窄。如果你的模型激活值里有大数,FP16 推理也可能溢出。稳妥起见,很多推理框架反而默认走 BF16 或 INT8。误区 5:量化就是把 FP16 转成 INT4这么简单一步。远不止。真正的量化是选格式 定 scale/zero-point 选粒度(per-group) 处理离群值 校准一整套。格式只是其中一环,别把它当成全部。八、小结任何数字在内存里都是一串 bit,格式就是这串 bit 怎么解读的约定,直接决定显存占用。两大家族:整数是均匀直尺(简单快、怕跨度大),浮点是伸缩软尺(范围大、精度灵活、更复杂)。浮点看指数和尾数的分配:指数管范围,尾数管精度。FP16 和 BF16 同为 16 位,却因为这个分配不同而一个易溢出、一个范围大。8 位档:INT8 稳妥快,FP8 靠浮点特性对付离群值更从容但要新硬件。4 位档:INT4 是消费级主力,NF4 因为按正态分布排刻度、更贴合神经网络权重,同样 4 位精度更好(实测误差只有 INT4 的六成)。选型口诀:训练 BF16,稳妥推理 INT8,极限压缩 INT4/NF4,新卡冲 FP8。格式没有好坏,只有合不合适。