大模型量化到底在做什么:从浮点数表示到 Qwen FP8 实践

大模型量化到底在做什么:从浮点数表示到 Qwen FP8 实践

一、为什么大模型必须量化

大模型推理的第一个现实问题,往往不是"算不算得动",而是"放不放得下、搬不搬得快"。

27B 参数模型为例,如果权重以 BF16FP16 存储,每个参数大致占 2 Byte,那么仅权重本身就需要:

Plain 复制代码
27B x 2 Byte ≈ 54 GB

这还没算 KV Cache、激活值、中间缓冲区和运行时碎片,上下文越长、并发越高,KV Cache 占用还会继续增长。

量化要解决的核心矛盾:

  • 用更少的 bit 存权重,降低模型常驻显存

  • 用更少的数据搬运完成一次前向,缓解显存带宽瓶颈

  • 在精度可接受的前提下,让同一张卡承载更大的 batch、更长的上下文或更大的模型

因此,量化不是单纯为了"文件变小",而是大模型部署里绕不开的工程手段: 它把一部分数值精度让出来,换取更现实的显存预算和推理吞吐。

二、浮点数在计算机里是怎么表示的

理解量化之前,最好先把浮点数的表示方式弄明白。因为现代大模型使用的很多"低精度格式",本质上都是在浮点数的三段结构上做取舍。

浮点数可以看成二进制世界里的科学计数法。它通常由三部分组成:

  • 符号位 sign

  • 指数位 exponent

  • 尾数位 mantissafraction

对一个规格化浮点数来说,它的值可以写成:

Plain 复制代码
value = (-1)^sign x 2^(exponent - bias) x 1.fraction

这里有三个关键点。

  1. 符号位只决定正负,即0 是正,1 是负。

  2. 指数位决定动态范围,也就是这个格式到底能表示多大的数。指数通常不是直接存真实指数,而是存一个"加了偏移量的指数",这个偏移量叫 bias,比如 8 位指数常见的偏移量是 127,因此:

Plain 复制代码
真实指数 = 存储指数 - 127

为什么需要这个偏移量?

因为真实指数既可能是正数,也可能是负数,比如 2^3 表示放大 8 倍,2^-3 表示缩小到 1/8。可是在硬件里,指数位本身更适合按无符号整数来存和比较。于是 IEEE 754 采用了一个很工程化的办法: 先把真实指数整体向右平移一个固定值,存进去时加上 bias,读出来时再减掉 bias。这样,负指数、零指数、正指数都能塞进同一个无符号字段里。

举个直观例子,8 位指数的 bias = 127,那么真实指数 -3 会存成 124,真实指数 0 会存成 127,真实指数 3 会存成 130

  1. 尾数位决定精度:二进制浮点数在规格化之后,前面通常都会写成 1.xxxxx x 2^n 的形式。也就是说,小数点前这一位在非零情况下几乎总是 1。既然它几乎是固定的,硬件就没必要每次都专门存一遍,只需要存后面的 xxxxx 部分,读取时再默认把前面的 1. 补回来。这就是所谓的 hidden bit。

一个简单例子: -6.5 如何表示

先把 6.5 写成二进制:

Plain 复制代码
6.5 = 110.1

规格化之后是:

Plain 复制代码
1.101 x 2^2

于是:

  • 符号位: 1

  • 真实指数: 2

  • 若偏移量为 127,存储指数就是 129

  • 尾数存 101...

这件事的重要性在于: 当你把指数位和尾数位砍短时,你牺牲的不是同一种能力

  • 砍指数位,主要损失动态范围,更容易溢出或下溢

  • 砍尾数位,主要损失有效精度,数值会变"糙"

这也是为什么 BF16FP16 虽然都只有 16 位,但它们的工程含义完全不同。

三、FP32 / FP16 / BF16 / FP8 到底差在哪

先把最常见的几种格式放在一起看:

表里的 符号位 / 指数位 / 尾数位 表示这个格式内部的 bit 分配。1B 参数权重约占用 只估算权重本体,不包含 scale、KV Cache、激活值和运行时额外开销。

格式 位宽 符号位 指数位 尾数位 1B 参数权重约占用 特点 常见用途
FP32 32 bit 1 8 23 4 GB 范围大,精度高,最稳 训练基准、优化器状态、调试
FP16 16 bit 1 5 10 2 GB 精度还行,但指数位少,范围窄 早期混合精度训练、推理
BF16 16 bit 1 8 7 2 GB 指数位与 FP32 一样,范围大但精度较粗 现代大模型训练默认格式
FP8-E4M3 8 bit 1 4 3 1 GB 精度略好、范围较小,最大值约 ±448 前向权重、激活值
FP8-E5M2 8 bit 1 5 2 1 GB 范围更大、精度更低,最大值约 ±57344 梯度、更强调范围的场景

为什么 BF16 在大模型训练里几乎成为默认选项

一句话总结: BF16 用 16 bit 拿到了接近 FP32 的动态范围

它保留了 8 位指数,所以不容易因为数值过大或过小而溢出;同时把尾数从 FP32 的 23 位砍到 7 位,牺牲一部分精度来换显存和吞吐。

对大模型训练来说,范围稳定通常比小数点后很多位更重要,所以 BF16 成了现代大模型训练里的主力格式。

为什么 FP8 又能再往下压一层

FP8 把单个数压到 8 bit,相比 BF16/FP16 又减半一次。对推理来说,这直接减少权重存储和显存带宽压力;如果硬件有原生 FP8 Tensor Core,还可能带来更高吞吐。

它的代价也很明确: 指数位和尾数位都更少,动态范围和精度都更脆弱,因此 FP8 很依赖缩放因子、量化粒度和混合精度设计,通常不会把所有层、所有计算都一刀切成 FP8。

四、量化到底在做什么

量化不是简单把模型文件里的 BF16 改成 FP8INT8,更准确地说,它是在做三件事:

  1. 找出哪些张量可以低精度保存。

  2. 给这些张量选择合适的低精度格式和缩放因子。

  3. 推理时用低精度存储、高精度累加的方式完成计算。

先决定压谁

大模型里能量化的对象很多,但最常见的是三类:

  • 权重: 参数固定,最适合提前离线量化,也是最直接的显存节省来源。

  • 激活值: 用户输入后动态生成,通常需要运行时动态 scale,收益更大但实现更难。

  • KV Cache: 长上下文推理时占用很高,量化后可以显著降低长上下文显存压力。

所以你看到的 W8A8,意思通常就是 Weight 8-bit, Activation 8-bit,也就是权重和激活都用 8 bit 表示,只压权重则可以理解为 W8A16 或类似形式。

再决定怎么压

量化的关键是 scale。对于整数型量化,最常见的简化公式是:

Plain 复制代码
x_q = round(x / s)
x ≈ x_q x s

这里 s 就是缩放因子,它负责把原始数值范围映射到低比特格式能表达的范围里

FP8 虽然是浮点数,不像 INT8/INT4 那样完全套这个公式,但工程上同样离不开 scale。原因是不同层、不同通道、不同 block 的数值分布差异很大,如果大家共用一个范围,误差会非常明显。

量化粒度决定误差和成本

scale 可以有不同粒度:

  • per-tensor: 整个张量一个 scale

  • per-channel: 每个输出通道一个 scale

  • per-group: 若干元素一组一个 scale

  • per-block: 二维 block 一个 scale,例如 128 x 128

粒度越细,越能适应局部数值分布,误差通常越小;但 scale 元数据更多,内核实现也更复杂。很多现代 FP8 模型会用 block-wise 量化,因为它比 per-tensor 更稳,又比过细粒度更容易和高性能内核对齐。

什么时候做量化

部署里最常见的是 PTQ,也就是 Post-Training Quantization: 模型训练完之后再量化。它不需要重训底座模型,成本低,适合把现成模型转换成推理版本。

PTQ 大致会做这几步:

  1. 加载已经训练好的高精度模型,比如 BF16/FP16

  2. 选一批校准数据,或者直接扫描权重分布。

  3. 统计每层、每通道或每个 block 的数值范围。

  4. 根据目标格式计算 scale,把权重或激活映射到低精度范围。

  5. 保存量化后的权重、scale 元数据和量化配置。

所以 PTQ 的核心不是"继续训练模型",而是"给训练好的模型重新找一套低精度表示方式"。如果只做权重量化,甚至可以不需要大量校准数据;如果要量化激活值,校准数据就更重要,因为激活分布依赖真实输入。

另一类是 QAT,也就是 Quantization-Aware Training: 训练时就模拟量化误差,让模型提前适应低精度。它通常效果更稳,但成本更高,更像训练工程而不是普通部署工程。

所以,如果你的目标是"把一个现成大模型跑起来",一般先看官方量化版或 PTQ;如果你要极致压缩且质量要求很高,再考虑 QAT。

五、以Qwen3.5-27B-FP8为例,看 FP8 量化如何落地

Qwen/Qwen3.5-27B-FP8 不是"训练时全程 FP8"的模型,而是一个面向推理发布的 FP8 量化检查点,官方模型卡的说法是: 仓库包含 post-trained model 的 FP8 量化权重和配置文件,采用 fine-grained FP8 quantization,block size 为 128

原始模型通常仍然来自高精度训练

大模型训练阶段更关注数值稳定性,所以主干训练常用 BF16 这类格式,FP8 版本一般是在训练完成之后,通过 PTQ 产出的推理检查点。

也就是说,常见流程是:

  1. 先得到高精度模型,例如 BF16

  2. 对主要权重做 FP8 量化。

  3. 保存 FP8 权重、量化配置和 scale 元数据。

  4. 推理框架读取这些信息,用支持 FP8 的 kernel 执行推理。

2. block size = 128 说明它不是粗粒度量化

如果整个权重矩阵只用一个 scale,局部数值分布差异会被抹平,误差很容易变大。block-wise 的做法是把矩阵切成小块,每个 block 单独记录 scale。

以一个尺寸接近 [5120, 17408] 的线性层为例,如果按 128 x 128 分块,scale 数量大致是:

Plain 复制代码
5120 / 128 = 40
17408 / 128 = 136
scale 数量 ≈ 40 x 136

这比整个矩阵共用一个 scale 更精细,也比每个元素一个 scale 更容易被高性能内核支持。FP8 的难点就在这里: 不是只把权重转成 8 bit,而是要给不同 block 找到合适的数值范围。

3. weight_scale_inv 是反量化线索

打开量化模型的索引或权重文件,常能看到类似 weight_scale_inv 的张量名。它说明 FP8 权重旁边还保存了 scale 相关元数据。

可以把推理时的逻辑理解成:

  • 权重本体以 FP8 保存,降低显存和带宽压力。

  • 每个 block 配套保存 scale 或 scale 的倒数。

  • GEMM 时 kernel 使用这些 scale,把低精度输入映射回合适的数值空间。

  • 累加通常仍在更高精度里完成,例如 FP16 / BF16 / FP32

所以 FP8 推理不是"所有数学都用 8 bit 算完",而是"低精度存储和搬运,高精度累加和关键计算"。

混合精度是质量的关键

高质量量化通常不会把所有层都压成 FP8,像 embed_tokenslm_headlayernorm 这类层更容易影响最终输出分布,参数占比也未必大,保留较高精度通常更划算,如上图所示 embed_tokenslm_head权重仍为 BF16

真正值得压的是那些参数量大、矩阵乘法密集、对轻微误差相对不敏感的线性层,FP8 的收益主要来自这里: 大头权重变小,搬运更快,同时用 scale 和混合精度把质量损失控制住。

六、如果我想自己做量化,应该怎么做

先明确目标

量化通常有三种目标:

  • 模型放不下: 优先做权重量化,先解决显存。

  • 吞吐不够: 关注 W8A8、FP8 kernel、batch 场景下的端到端吞吐。

  • 长上下文撑不住: 除了权重,还要看 KV Cache 量化。

目标不同,最优方案不一样:FP8 很适合有原生硬件支持、又希望尽量保留质量的推理场景;如果是在消费级显卡上硬塞模型,INT4/GPTQ/AWQ/GGUF 可能更现实。

路线 A: 直接使用官方量化检查点

这是最省心的方式。如果目标模型已经有官方 FP8、GPTQ、AWQ 或 GGUF 版本,通常优先用官方或社区验证较多的版本。

例如用 vLLM 跑 Qwen/Qwen3.5-27B-FP8:

Bash 复制代码
pip install vllm
vllm serve "Qwen/Qwen3.5-27B-FP8"

这条路线适合上线前验证,也适合先建立性能和质量基线。

路线 B: 在线量化,快速试效果

如果手里只有 BF16/FP16 模型,可以用推理框架在加载时做在线量化。例如 vLLM 支持类似下面的方式:

Bash 复制代码
vllm serve meta-llama/Llama-3.1-8B --quantization fp8_per_tensor
vllm serve meta-llama/Llama-3.1-8B --quantization fp8_per_block

在线量化适合快速实验,但它更像"部署时临时压缩",不一定是质量和速度最优的最终形态。

路线 C: 离线 PTQ,生成自己的量化检查点

如果要长期部署,最好做离线 PTQ,把量化后的模型保存下来。它和在线量化的区别是: scale 计算、权重转换和文件保存提前完成,部署时直接加载量化检查点。

实践上,PTQ 通常就是一个 Python 脚本。这个脚本一般接收三类输入:

  • 原始模型: 通常是 Hugging Face 上的 BF16/FP16 checkpoint。

  • 量化方案: 比如 FP8_BLOCKW8A8-INT8INT4 等。

  • 忽略层和校准数据: 哪些层不压,以及是否需要用样本统计激活分布。

llmcompressor 的 FP8 block 量化为例,一个最小脚本大概长这样:

Python 复制代码
from compressed_tensors.offload import dispatch_model
from transformers import AutoModelForCausalLM, AutoTokenizer

from llmcompressor import oneshot
from llmcompressor.modifiers.quantization import QuantizationModifier

MODEL_ID = "Qwen/Qwen3-30B-A3B"
SAVE_DIR = "Qwen3-30B-A3B-FP8-BLOCK"

model = AutoModelForCausalLM.from_pretrained(MODEL_ID, dtype="auto")
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)

recipe = QuantizationModifier(
    targets="Linear",
    scheme="FP8_BLOCK",
    ignore=["lm_head", "re:.*embed_tokens.*"],
)

oneshot(model=model, recipe=recipe)

# 简单 sanity check: 看量化后还能不能正常生成。
dispatch_model(model)
input_ids = tokenizer("Hello my name is", return_tensors="pt").input_ids.to(model.device)
output = model.generate(input_ids, max_new_tokens=20)
print(tokenizer.decode(output[0], skip_special_tokens=True))

model.save_pretrained(SAVE_DIR)
tokenizer.save_pretrained(SAVE_DIR)

跑完之后,SAVE_DIR 里就是一个新的量化模型目录,里面会包含量化后的权重、scale 元数据和配置文件。后续可以用 vLLMTransformers 加载它做推理测试。

这里真正要关心的不是代码有多长,而是几个选择:

  • targets: 量化作用到哪些模块。targets="Linear" 表示只量化线性层,因为大模型里大部分参数量和矩阵乘法开销都集中在 Linear 层。

  • scheme: 用 FP8_BLOCK,还是更粗的 per-tensor。

  • ignore: 哪些层不量化,比如 lm_headembed_tokenslayernorm

  • eval: 用哪些真实任务样本验证质量。

  • hardware: 目标 GPU 是否真的支持对应格式和 kernel。

离线量化还有一个容易忽略的点: 做量化时通常需要先加载原始高精度模型,所以量化过程本身也需要足够显存。

七、总结

量化的本质不是改一个 dtype,而是重新设计数值表达、scale 粒度和推理计算路径

BF16 适合训练,是因为它保留了足够大的动态范围;FP8 适合推理,是因为它能进一步降低权重存储和显存带宽压力。以 Qwen3.5-27B-FP8 为例,真正起作用的是细粒度 block-wise FP8、scale 元数据、混合精度和推理框架的 kernel 支持。

做实践时,最稳的路线是: 先跑高精度基线,再试官方量化版或在线量化,最后根据真实任务决定是否做离线 PTQ。不要迷信最低 bit,能在质量、显存、吞吐和硬件兼容之间取得平衡,才是好的量化方案。

参考资料

相关推荐
JieE2121 小时前
LeetCode35. 搜索插入位置:二分查找的基础与细节
javascript·算法
8Qi81 小时前
LeetCode 188 & 123:股票买卖问题(限制交易次数)—— 联合题解
算法·leetcode·职场和发展·动态规划
KaMeidebaby1 小时前
卡梅德生物技术快报|细胞周期检测抗原流式分析:参数调试、软件拟合与问题排查
网络·人工智能·python·网络协议·tcp/ip·算法·机器学习
一只齐刘海的猫1 小时前
【Leetcode】三数之和
数据结构·算法·leetcode
lightqjx1 小时前
【算法】数据结构_扩展域并查集
数据结构·算法·并查集·扩展域并查集
无忧.芙桃1 小时前
debug实例与分析(一)
开发语言·c++·算法
sheeta19981 小时前
LeetCode 补拙笔记 日期:2026.06.07 题目:49. 字母异位词分组
笔记·算法·leetcode
装不满的克莱因瓶1 小时前
了解不同机器学习模型的分类
人工智能·python·算法·机器学习·ai·分类·数据挖掘
QiLinkOS1 小时前
合肥气链科技有限公司本质总结
c++·科技·算法·gitee·开源