PTQ vs QAT:为什么有的量化几分钟完成,有的却要重新训练好几天?

从量化公式、误差来源、校准、伪量化和 STE,一路讲到 AWQ、GPTQ、GGUF、QLoRA 与真实部署选型

模型量化看起来像一件很简单的事:把原本用 FP32、FP16 或 BF16 保存的数字,换成 INT8、INT4,甚至 2-bit,不就结束了吗?

可真正上手以后,你会发现同样都叫“量化”,工程流程却可能完全不同:

  • 有的方法加载一个训练好的模型,喂几十到几百条校准样本,几分钟或几十分钟后就能导出低比特权重;
  • 有的方法需要重新进入训练循环,准备数据、优化器、学习率、反向传播和检查点,耗时从几小时到几天不等;
  • 还有一些方法明明使用了梯度,却仍然被论文归在 PTQ;另一些方法虽然在“量化模型上训练”,却不一定属于经典 QAT。

这背后的分界线,就是 PTQ 与 QAT。

但只记住“PTQ 不训练,QAT 要训练”还远远不够。这个说法适合第一分钟入门,却不足以解释下面这些更实际的问题:

  1. 为什么 AWQ 和 GPTQ 都属于 PTQ,但它们又需要校准数据?
  2. 为什么有些 PTQ 方法也会做局部优化,甚至调用反向传播?
  3. 为什么 GGUF 不能和 AWQ、GPTQ 并列成一种量化算法?
  4. 为什么 QLoRA 使用了 4-bit 模型训练,却通常不直接等同于 QAT?
  5. 为什么同一个“4-bit 模型”,有的只省显存不提速,有的却能明显加速?
  6. 为什么量化到 2-bit 时,QAT 往往更有价值,而 8-bit 时 PTQ 常常已经够用?

这篇文章会把这些岔路口一次讲清。你不需要先学过数值分析,也不需要懂硬件指令集。读完以后,你应该能做到三件事:

  • 看懂 PTQ 与 QAT 在数学和训练流程上的真正差别;
  • 正确判断 AWQ、GPTQ、SmoothQuant、GGUF、bitsandbytes、QLoRA 分别处在什么位置;
  • 面对自己的模型、硬件和业务指标时,知道应该先试哪条路线,以及什么时候才值得为 QAT 付出训练成本。

目录

  1. 先看三个真实场景
  2. 一句话理解 PTQ 与 QAT,但不要停在这一句话
  3. 在比较路线之前,先把“量化”本身讲明白
  4. 量化误差从哪里来:舍入、截断与离群值
  5. PTQ 到底在做什么
  6. 动态量化、静态量化与权重-only:三个概念不要混用
  7. AWQ、GPTQ、SmoothQuant、GGUF 与 bitsandbytes 各自是什么
  8. QAT 到底在训练什么
  9. round 不可导,STE 为什么还能让模型学下去
  10. PTQ 与 QAT 并不是绝对二分,而是一条连续谱
  11. 成本、精度、速度与显存:真正的工程权衡
  12. 一个更公平、可复现的最小实验
  13. 如何正确解读实验结果
  14. 真实项目中的 PTQ 工作流
  15. 真实项目中的 QAT 工作流
  16. 一套可以直接使用的选型决策树
  17. 常见误区与避坑
  18. 横向对比总表
  19. 小结:先 PTQ,不迷信 PTQ;需要 QAT,也不必从头重训
  20. 附录:术语表与延伸阅读

一、先看三个真实场景

场景 A:把 7B 大模型塞进消费级显卡

你从 Hugging Face 下载了一个 7B 参数的指令模型,准备放到自己的消费级显卡上做本地推理。模型原始权重是 FP16,仅权重的理论体积就大约是:

[
7\times 10^9\times 2\text{ Byte}\approx 14\text{ GB}
]

这还没有算 KV Cache、临时工作区、CUDA 上下文、算子缓存和框架额外开销。模型也许勉强能加载,却很难留出足够空间跑长上下文或较大的 batch。

你的目标很明确:

  • 不准备重新训练;
  • 没有原始预训练数据;
  • 最多能拿一些有代表性的文本做校准;
  • 希望尽快得到一个能运行的 4-bit 模型。

这几乎就是标准的 PTQ 场景。你拿已有检查点做离线量化,评估质量,再交给支持相应格式和内核的推理框架。

场景 B:端侧模型必须在 INT8 或更低精度运行

你在做一个部署到 MCU、NPU 或移动端 DSP 上的小模型,例如语音唤醒、手势识别、关键词分类或轻量视觉模型。目标硬件只对 INT8 算子有高效支持,甚至要求权重与激活都压到 4-bit。

你先做 PTQ,发现:

  • 浮点模型准确率 96%;
  • INT8 PTQ 还能接受;
  • 但 W4A4 或更低精度时,召回率明显下降;
  • 某些少数类几乎完全失效。

与此同时,你拥有完整训练数据、损失函数、训练代码和部署测试集,也愿意用额外训练换回精度。

这时 QAT 才真正有发挥空间:训练阶段就模拟目标低精度,让参数主动适应舍入与截断误差。

场景 C:你用 QLoRA 微调一个 4-bit 模型

你把基础大模型以 NF4 形式加载,冻结基础权重,只训练 LoRA 适配器。训练显存显著下降,于是很自然地产生一个疑问:

我都已经在 4-bit 模型上训练了,这是不是 QAT?

不一定。

经典 QAT 的核心目标,是让最终要部署的量化权重或激活在训练中显式适应量化算子;QLoRA 的核心目标,则是借助冻结的 4-bit 基础模型降低微调显存,只把梯度更新到 LoRA 适配器。训练完以后,你可以保留适配器,也可以合并后重新量化。两者有交集,但不能因为都出现了“低比特训练”就直接画等号。

这三个场景揭示了一个重要事实:

判断 PTQ 还是 QAT,不能只看文件是不是 4-bit,也不能只看代码里有没有 backward;要看低比特误差是否进入了模型参数适应的训练闭环,以及最终部署对象是什么。


二、一句话理解 PTQ 与 QAT,但不要停在这一句话

最适合入门的定义是:

PTQ(Post-Training Quantization,训练后量化):模型已经训练完成,在不进行完整量化感知训练的前提下,把现有检查点转换成低精度表示。它可以完全不使用数据,也可以使用少量无标签校准数据,还可能执行逐层或逐块的局部优化。

QAT(Quantization-Aware Training,量化感知训练):在训练或微调阶段显式模拟目标量化行为,让模型参数在优化过程中适应量化误差,训练结束后再转换为真实低精度部署形式。

为了好记,可以继续使用“改衣服”的比喻:

  • PTQ 像衣服做好以后再改尺寸。速度快,原版型基本不动,但修改空间有限;
  • QAT 像在打版和试穿过程中不断按目标尺寸调整。成本高,却能主动修正不合身的地方。

不过这个比喻有两个容易误导的地方。

第一,PTQ 不一定只是“机械改小”。GPTQ 会根据近似二阶信息决定如何量化并补偿误差,AWQ 会利用激活统计识别重要通道,OmniQuant 甚至会在小规模校准数据上优化量化参数。它们仍然属于 PTQ,因为没有进行面向完整任务目标的端到端量化训练。

第二,QAT 不一定等于“从头训练”。现实中更常见的是从一个已经训练好的浮点检查点出发,进行较短的量化感知微调。对于大模型,还可以只更新低秩参数、量化尺度或部分模块,而不是把所有参数完整重训。

所以更准确的分界是:

PTQ 的重点是“在既有模型附近寻找更好的量化表示”;QAT 的重点是“把量化误差放进训练目标,让模型本身为低精度部署而适应”。


三、在比较路线之前,先把“量化”本身讲明白

很多人第一次接触量化,会把它理解成“把 float 转成 int”。这只说对了表面。

量化真正做的是:用有限个离散数值,近似表示原来更密集、范围更大的连续或高精度数值。

例如,一个 FP16 权重可能是:

-0.1372, -0.0428, 0.0061, 0.0934, 0.2817

如果你只允许 2-bit,那么最多只有 (2^2=4) 个编码。你必须让许多不同的浮点数映射到同一个离散格点。信息损失由此产生。

3.1 最常见的仿射量化公式

设浮点数为 (x),整数编码为 (q),量化步长为 (s),零点为 (z),整数范围为 ([q_{\min},q_{\max}])。常见仿射量化写作:

[
q=\operatorname{clip}\left(\operatorname{round}\left(\frac{x}{s}\right)+z, q_{\min},q_{\max}\right)
]

反量化得到近似值:

[
\hat{x}=s(q-z)
]

这里的三个动作分别对应三种直觉:

  1. 除以 (s):把浮点范围缩放到整数网格;
  2. round:把连续位置吸附到最近的格点;
  3. clip:超出可表示范围的值只能压到边界。

如果数据范围是 ([x_{\min},x_{\max}]),一种基础的 min-max 取法是:

[
s=\frac{x_{\max}-x_{\min}}{q_{\max}-q_{\min}}
]

[
z=\operatorname{round}\left(q_{\min}-\frac{x_{\min}}{s}\right)
]

这叫非对称或仿射量化,因为零点 (z) 不一定等于 0。它适合分布明显偏向一侧的数据,例如 ReLU 后非负激活。

3.2 对称量化

如果权重分布大致围绕 0 对称,可以设:

[
a=\max(|x_{\min}|,|x_{\max}|)
]

[
s=\frac{a}{q_{\max}},\quad z=0
]

这样实现简单,也更容易对应某些整数矩阵乘内核。

不过“对称”不代表所有实现都使用完全相同的整数范围。例如 INT8 有的实现使用 ([-127,127]) 的窄范围,有的使用 ([-128,127])。到了 2-bit,这个细节尤其重要:如果你简单令 (q_{\max}=1),再裁剪到 ([-1,1]),实际只得到 (-1,0,1) 三个级别,严格说更接近三值量化,而不是完整利用 2-bit 的四个编码。

这也是很多教学代码里隐藏的第一个坑:代码写着“2-bit”,实际量化级别却未必有四个。

3.3 scale 和 zero-point 不是附属品

低比特权重不能只保存整数本身。推理时还需要知道如何还原近似数值,因此通常还要保存:

  • scale;
  • 可选的 zero-point;
  • 分组边界或张量形状;
  • 打包布局;
  • 某些格式中的重要性信息、混合精度标记或校验元数据。

所以 7B 模型从 FP16 量化到 4-bit,原始权重的理论下限是从约 14 GB 降到约 3.5 GB,但实际文件通常会高于 3.5 GB。原因不是“量化失败”,而是尺度、分组、对齐、元数据以及部分未量化张量都需要空间。

3.4 粒度决定精度与开销

scale 可以覆盖不同范围:

Per-tensor

整个张量共用一个 scale。开销最小,但如果张量内部各通道范围差异很大,一小撮极端值会迫使所有元素共用很粗的步长。

Per-channel

每个输出通道或输入通道使用独立 scale。更能适应通道差异,通常比 per-tensor 精度更好,但需要保存更多尺度,并要求内核支持相应布局。

Per-group

把通道再切成固定大小的小组,例如每 32、64 或 128 个权重共用一个 scale。它位于 per-tensor 与 per-channel 之间,也是大模型 4-bit 权重量化中非常常见的折中。

组越小,通常越容易降低量化误差;但尺度元数据更多、打包更复杂,某些硬件上的吞吐也可能受影响。

3.5 W4A16、W8A8 到底在说什么

量化配置常写成:

  • W8A8:权重 8-bit,激活 8-bit;
  • W4A16:权重 4-bit,激活仍用 FP16 或 BF16;
  • W4A8:权重 4-bit,激活 8-bit;
  • W4A4:权重和激活都 4-bit。

对大模型来说,AWQ、GPTQ 常见部署形态主要是低比特权重加高精度激活,也就是常说的 weight-only quantization。它最大的直接收益是压缩权重并降低读取权重的内存带宽压力。

而 W8A8、W4A8 或 W4A4 不仅压权重,还试图让矩阵乘的输入和计算路径进入整数或低精度内核。这样理论上的计算收益更大,但激活往往比权重更难量化,工程难度也更高。

3.6 不要忘了 KV Cache

大模型推理的显存不只有权重。长上下文生成时,KV Cache 可能成为显存大户。仅做 W4A16 能大幅压缩模型权重,却不会自动把 KV Cache 也压成 4-bit。

因此,同样是“4-bit 模型”,在短上下文下可能主要受权重读取影响,在长上下文或高并发下却可能转而受 KV Cache 和注意力计算影响。量化方案是否真的解决你的瓶颈,必须结合上下文长度、batch、并发和缓存精度一起看。


四、量化误差从哪里来:舍入、截断与离群值

要理解 PTQ 与 QAT,先要知道模型究竟在适应什么误差。

4.1 舍入误差

假设一个值落在两个量化格点之间,它只能被 round 到其中一个。若步长为 (s),且没有发生截断,那么最简单的均匀量化中,单个标量的舍入误差绝对值通常不超过约 (s/2)。

问题在于,神经网络里一次矩阵乘会累积大量这样的误差。单个权重偏一点看似无关紧要,经过多层传播、非线性和残差相加后,最终输出可能产生明显变化。

4.2 截断误差

为了让步长更细,我们常常不会覆盖所有极端值,而是选择一个较窄的量化范围。超出范围的值会被 clip 到边界。

这带来一个经典矛盾:

  • 范围放得很宽:极端值保住了,但 scale 变大,大多数普通值的格点变粗;
  • 范围收得很窄:主体分布表示得更细,但少数离群值被严重截断。

量化算法的许多技巧,本质上都在解决这个矛盾:怎样选择范围,怎样保护重要离群通道,怎样把难量化的分布变得更平滑。

4.3 权重通常比激活更“好管”

权重在训练结束后固定不变。你可以完整遍历它们,精确知道每个张量、通道或分组的最小值、最大值、分位数和误差。

激活则依赖输入:

[
a_l=f_l(x;W)
]

换一批输入,某一层激活的分布可能就变化。语言模型中,不同语言、代码、数学文本、长上下文和特殊格式都可能激活不同通道。

这就是校准数据存在的原因。你需要用一小批尽量代表真实使用场景的数据做前向传播,观察激活范围、离群值、通道重要性或近似二阶统计。

但“权重量化不需要数据”也只能算半句话:

  • 最朴素的 per-tensor min-max 权重量化,确实只看权重即可;
  • 现代高质量 weight-only PTQ,例如 AWQ、GPTQ,往往仍会利用校准输入来判断哪些权重更重要、怎样补偿误差。

4.4 误差大小不等于任务损失大小

另一个常见误区是:只要让 (|W-\hat W|_2) 最小,模型质量就一定最好。

并不一定。

同样大小的权重误差,落在不重要的方向上可能几乎没有影响;落在高敏感方向上,却可能显著改变输出。于是更先进的方法会尝试利用激活、Hessian 近似或层输出重建,让量化误差更贴近“模型实际会不会受伤”这个目标,而不只是机械追求每个权重的数值距离最小。

这正是 GPTQ 使用近似二阶信息、AWQ 使用激活统计的动机之一。


五、PTQ 到底在做什么

PTQ 的最简流程确实很短:

浮点检查点
  ↓
确定目标位宽、粒度和部署后端
  ↓
可选:用校准数据做前向统计
  ↓
计算 scale / zero-point / clipping / 分组策略
  ↓
离线生成量化权重并打包
  ↓
导出、部署、评估

但真正决定质量的,不是“有没有调用 quantize()”,而是中间几步做得多精细。

5.1 最朴素的无数据 PTQ

最简单的方法只看权重本身:

  1. 对每个张量或通道计算 min/max;
  2. 根据目标位宽计算 scale;
  3. round 和 clip;
  4. 保存整数编码和尺度。

优点是快、简单、完全不需要数据。缺点是它不知道某个权重在真实输入下是否重要,也不知道量化误差会怎样传递到层输出。

在 INT8 或本身非常稳健的小模型上,这种方法可能已经够用;在 LLM 的 4-bit、3-bit 或 2-bit 场景中,它常常不是质量最好的选择。

5.2 带校准数据的 PTQ

校准数据通常不需要标签,因为目标不是重新训练任务,而是观察模型内部统计。常见用途包括:

  • 估计激活范围;
  • 统计通道幅度或离群值;
  • 构造近似 Hessian 或输入协方差;
  • 比较不同 clipping 阈值的重建误差;
  • 搜索分组尺度;
  • 判断哪些层或张量应保留更高精度。

校准集的关键不在于“越大越好”,而在于覆盖真实输入分布。中文对话模型应包含中文对话;代码模型应包含代码;长文本应用应让校准序列长度接近部署长度;结构化输出任务应覆盖真实模板和特殊 token。

几百条高度同质的样本,可能不如几十条覆盖核心场景的样本有用。

5.3 PTQ 也可能包含优化

把 PTQ 理解成“绝对不能出现梯度”是不准确的。

有些 PTQ 方法会在小规模校准数据上优化 clipping、尺度或等价变换,甚至逐块最小化浮点输出与量化输出之间的重建误差。OmniQuant 就会在可微框架中优化量化参数,但仍被归类为 PTQ,因为它没有像 QAT 那样对完整模型进行面向训练目标的长期量化适应,而是在已有检查点附近做高效校准优化。

因此,“有没有 backward”不是唯一判据。更有用的问题是:

  • 更新的是完整模型参数,还是少量量化参数?
  • 优化目标是任务损失,还是局部重建误差?
  • 使用的是完整训练数据,还是少量无标签校准样本?
  • 训练过程是否让模型系统性适应目标低精度?

5.4 PTQ 的优点

  • 不需要重建完整训练流程;
  • 对开源检查点、第三方模型和无法获得原始数据的模型尤其友好;
  • 时间成本低,适合快速尝试多个位宽、分组大小和后端;
  • 便于把同一个模型导出为多种部署格式;
  • 大模型场景中通常是第一选择。

5.5 PTQ 的局限

  • 模型参数本身不会主动为量化误差让路;
  • 位宽越低,可选格点越少,单靠校准越来越难;
  • 激活离群值、特殊层和分布漂移可能造成明显退化;
  • 校准集与真实业务不匹配时,离线评估好、线上表现差;
  • 算法得到“数值上不错”的低比特权重,不代表部署后端一定有高效内核。

PTQ 的本质不是免费午餐,而是用算法设计、校准统计和硬件友好布局,换取“不重训模型”的巨大成本优势。


六、动态量化、静态量化与权重-only:三个概念不要混用

原始入门文章里最常见的一处表述是:

AWQ、GPTQ 基本都是静态量化。

这句话不够准确。

6.1 经典语境中的动态量化

在传统 CPU INT8 推理里,动态量化通常指:

  • 权重提前量化;
  • 激活的 scale 或范围在推理时根据当前输入动态计算;
  • 计算前后进行必要的量化、反量化或整数累积。

它不依赖离线激活校准,部署简单,但运行时需要统计激活范围,存在额外开销。

6.2 经典语境中的静态量化

静态量化通常指:

  • 权重提前量化;
  • 激活的 scale 也通过校准数据提前确定;
  • 推理时直接使用固定量化参数。

它更依赖校准质量,但可让推理路径更稳定,也更容易进入完整整数算子。

6.3 AWQ、GPTQ 更准确的标签是“离线 weight-only PTQ”

AWQ 和 GPTQ 的常见部署形式是:

  • 权重离线压到 4-bit、3-bit 等;
  • 激活仍保持 FP16/BF16,或者在内核内部按需要处理;
  • 推理时读取打包低比特权重,解包或融合反量化后参与矩阵乘。

它们没有在经典意义上为“激活静态量化”预先固定一个 INT8 scale。因此,称它们为“离线权重量化”或“weight-only PTQ”通常比笼统说“静态量化”更清晰。

6.4 为什么这个术语区别重要

因为它直接决定你在期待什么收益:

  • Weight-only 主要压缩模型权重与内存带宽;
  • W8A8 静态量化还希望把激活和矩阵乘路径一起低精度化;
  • 动态量化则在部署便利性和运行时开销之间折中。

如果术语混在一起,就很容易出现“我已经 4-bit,为什么 GPU 利用率没有明显提升”“为什么文件小了,prefill 却没快多少”之类的误解。


七、AWQ、GPTQ、SmoothQuant、GGUF 与 bitsandbytes 各自是什么

这一节非常关键,因为很多教程把算法、文件格式、运行库和训练方法混在一张表里。

7.1 GPTQ:利用近似二阶信息的 one-shot 权重量化

GPTQ 是面向生成式预训练 Transformer 的训练后权重量化方法。它的核心不是简单逐元素 round,而是利用校准输入形成的近似二阶信息,逐块或逐列量化权重,并把已经产生的误差补偿到尚未量化的权重中。

直觉上可以这样理解:

如果一个权重必须被推到邻近格点,GPTQ 不只是接受这次偏差,而是根据输入敏感方向,调整后续权重,让整层输出尽量接近原模型。

因此 GPTQ 仍然是 PTQ,但它比朴素 min-max 更“懂”量化误差对层输出的影响。原论文将其描述为基于近似二阶信息的 one-shot 权重量化方法,并展示了在超大模型上进行 3-bit、4-bit 量化的可行性。[1]

7.2 AWQ:激活感知的权重-only PTQ

AWQ 的出发点是:权重并非同等重要,某些与大幅激活通道相关的权重对输出更敏感。它利用少量校准数据统计激活分布,识别重要通道,再通过等价缩放等方式降低这些通道的量化误差,同时保持硬件友好的低比特权重布局。

AWQ 不是“把激活也量化了”。这里的 activation-aware,主要是说它借助激活统计来决定怎样量化权重。原论文将其定位为硬件友好的低比特 weight-only 方法,并强调不依赖反向传播或重建。[2]

7.3 SmoothQuant:把激活量化难题迁移一部分给权重

LLM 的激活常存在明显离群通道,直接 W8A8 容易受影响。SmoothQuant 通过数学等价的离线缩放,让激活分布更平滑,同时把一部分量化难度迁移到相对更容易处理的权重侧,从而支持训练后 W8A8 量化。[3]

它与 AWQ 都会谈到激活和缩放,但目标不同:

  • AWQ 常见目标是高质量低比特 weight-only;
  • SmoothQuant 重点是让权重和激活都能进入 INT8 路径。

7.4 GGUF:容器格式,不是量化算法

这是需要明确纠正的一点。

GGUF 是 ggml/llama.cpp 生态中用于保存模型张量和元数据的文件格式。一个 GGUF 文件可以保存较高精度权重,也可以保存多种量化类型。llama.cpp 的量化工具可以把 F32、BF16 等 GGUF 模型转换成量化 GGUF,但“GGUF”本身不等于某种具体的量化算法。[4]

更准确的说法是:

  • AWQ、GPTQ 是量化方法或算法家族;
  • Q4_K_M、IQ4_XS 等是特定生态中的量化类型或布局;
  • GGUF 是承载张量、量化数据和模型元信息的容器格式;
  • llama.cpp 是可以加载、量化和运行这类模型的推理生态。

把 GGUF 与 AWQ、GPTQ 并列为“PTQ 三大算法”,就像把 JPEG、某个降噪算法和 Photoshop 放在同一个算法分类里,层级不一致。

7.5 bitsandbytes:库与实现集合,不是一种单独算法

bitsandbytes 提供了多种低精度加载、矩阵运算和优化器能力。你可能通过它做 8-bit 推理,也可能用 4-bit NF4 加载基础模型进行 QLoRA 微调。

因此“bitsandbytes 属于 PTQ 还是 QAT”没有唯一答案,要看具体功能:

  • 直接把预训练模型以 8-bit 或 4-bit 形式加载用于推理,更接近 PTQ/低精度推理工具链;
  • 用 NF4 冻结基础模型、训练 LoRA 适配器,是 QLoRA 的训练范式;
  • 它不是一个与 GPTQ、AWQ 完全同层级的单一算法名称。

7.6 QLoRA:低显存微调,不应自动等同于 QAT

QLoRA 把冻结的预训练模型以 4-bit 形式保存和计算,把梯度传给 LoRA 适配器,从而显著降低微调内存。原论文的重点是高效微调量化模型,而不是让基础权重通过 fake quant 主动学习目标部署误差。[5]

当然,工程上可以把 LoRA、低比特基础权重和量化感知目标结合,形成参数高效 QAT。但那需要看训练图里到底模拟了什么量化、更新了哪些参数,以及最终如何导出,不能只凭“用了 4-bit”判断。


八、QAT 到底在训练什么

QAT 的核心不是在训练时真的把所有参数永久改成整数,而是在前向传播中模拟目标量化误差,同时保留可更新的高精度参数副本。

经典流程可以写成:

高精度参数 W
  ↓
Fake Quant:量化 Q(W) 再反量化 D(Q(W))
  ↓
使用近似权重 W_hat 做前向
  ↓
计算任务损失
  ↓
通过 STE 等近似梯度更新高精度 W

也就是说,模型真正被优化的是“影子浮点权重”,但它每次前向看到的,都是受量化限制后的近似值。

8.1 伪量化为什么要“量化再反量化”

训练框架中的多数算子仍然使用浮点张量与浮点反向传播。如果直接把权重变成真正整数,很多优化器、梯度更新和算子都无法按普通训练方式工作。

于是 fake quant 做两步:

  1. 用目标整数网格模拟 round、clip 和有限位宽;
  2. 立刻反量化回浮点张量,继续交给普通矩阵乘。

数值已经被限制在量化格点上,但数据类型仍可保持浮点,方便训练。

要特别强调:

Fake quant 训练不等于已经获得真实整数推理速度。

训练时的“量化”主要是数值模拟。最终能否加速,仍取决于 convert/export 阶段是否生成了目标后端支持的低比特权重、整数算子和打包布局。

8.2 QAT 可以量化哪些对象

QAT 不一定只量化权重。常见配置包括:

  • 只对权重 fake quant;
  • 权重与激活同时 fake quant;
  • 只量化部分线性层;
  • 保留首层、末层、embedding、归一化或敏感模块为高精度;
  • 对不同层使用不同位宽;
  • 训练 scale、clipping 阈值或 zero-point;
  • 连 KV Cache 或注意力中间量也纳入低精度设计。

目标硬件支持什么算子,往往比“理论上能量化到几位”更重要。

8.3 QAT 的训练起点

QAT 有两种常见起点:

从头做量化感知训练

适用于模型较小、训练流程完全可控、目标低比特非常明确的场景。训练全程都暴露于量化误差,但成本最高。

从浮点检查点做量化感知微调

这是更现实的做法:

  1. 先得到高质量浮点模型;
  2. 插入 fake quant;
  3. 用较小学习率做一段微调;
  4. 逐步稳定 observer、scale 和权重;
  5. 导出低比特模型。

对于大模型,QAT 更常是“量化感知微调”,而不是“把预训练重做一遍”。PyTorch 与相关工具链已经展示了面向 LLM 的端到端 QAT 流程;研究中也出现了低秩、参数高效的 QAT 方法,用更小的训练内存逼近传统全参数 QAT。[6][7]

8.4 QAT 真正学到的是什么

模型可能通过多种方式适应量化误差:

  • 把权重从量化格点边界附近推向更稳定的位置;
  • 调整通道尺度,减少少数离群值对整体步长的绑架;
  • 改变冗余参数之间的分工,让某个权重的舍入误差由其他权重补偿;
  • 学习更合适的 clipping 阈值或 scale;
  • 让中间表示对低精度噪声更鲁棒;
  • 在任务损失直接监督下,优先保护真正影响业务指标的方向。

这也是 QAT 相比纯 PTQ 最本质的优势:PTQ 只能在现有参数附近寻找更好的表示,QAT 则允许参数本身移动。


九、round 不可导,STE 为什么还能让模型学下去

QAT 最大的数学障碍来自 round。

9.1 round 的真实导数

round 是阶梯函数:

  • 在每个台阶内部,输出不随输入小幅变化,导数为 0;
  • 在台阶跳变点,函数不可导。

因此如果严格使用真实导数:

[
\frac{\partial\operatorname{round}(x)}{\partial x}=0
]

几乎处处成立。梯度经过量化节点后就会消失,前面的权重无法获得有效学习信号。

9.2 STE 的基本思想

Straight-Through Estimator 的思路是:

  • 前向传播使用真实或模拟的离散操作;
  • 反向传播使用一个更容易优化的替代导数。

最简单的 identity STE 直接假设:

[
\frac{\partial\operatorname{round}(x)}{\partial x}\approx 1
]

于是梯度像穿过恒等函数一样继续向前传。

这不是 round 的真实梯度,而是一个有偏的近似估计器。它之所以有用,不是因为数学上“突然可导了”,而是因为它给优化器提供了方向一致、方差较低且工程上可用的学习信号。STE 的经典表述可以追溯到对离散或随机神经元梯度估计的研究。[8]

9.3 一行 PyTorch 写出 identity STE

有一个非常直观的写法:

x_ste = x + (x_hat - x).detach()

前向看数值:

x + x_hat - x = x_hat

所以模型实际使用量化后的近似值 (x_{hat})。

反向看梯度:detach() 内部不参与求导,因此:

[
\frac{\partial x_{ste}}{\partial x}=1
]

这就实现了“前向用量化值,反向把梯度当作恒等映射穿过去”。

9.4 更真实的 STE 不一定处处传 1

教学代码常直接返回 grad_output,但实际实现可能更谨慎:

  • 只对未超出 clipping 范围的值传梯度;
  • 对被截断的区域设梯度为 0;
  • 对 scale 使用单独的梯度缩放;
  • 使用可学习步长,如 LSQ;
  • 使用可学习激活截断,如 PACT;
  • 在训练早期和后期采用不同 observer 或冻结策略。

所以“STE 就是把 round 的梯度永远设成 1”是最适合入门的版本,不是所有 QAT 算法的完整定义。LSQ 等方法还会直接学习量化步长,使 quantizer 本身也成为优化对象。[9]

9.5 STE 的代价

STE 给出的不是精确梯度,因此可能出现:

  • 训练震荡;
  • scale 更新不稳定;
  • 低比特下梯度与真实离散目标偏差过大;
  • 训练损失下降,但真正整数导出后效果不一致;
  • fake quant 配置与部署内核不匹配。

这就是为什么 QAT 不只是“插个 round 再继续训练”。学习率、冻结时机、量化粒度、敏感层处理、数据分布和导出一致性都很重要。


十、PTQ 与 QAT 并不是绝对二分,而是一条连续谱

真实世界中的方法可以大致排成一条从“完全不训练”到“完整量化训练”的连续谱:

无数据 min-max PTQ
   ↓
带校准统计的 PTQ
   ↓
带误差补偿 / 二阶近似的 PTQ
   ↓
带局部重建与少量可学习量化参数的 PTQ
   ↓
量化感知微调
   ↓
参数高效 QAT / 低秩 QAT
   ↓
全参数 QAT
   ↓
从头低比特训练

10.1 为什么边界会模糊

因为“训练”本身有多个维度:

  • 是否有反向传播;
  • 是否更新原始模型权重;
  • 是否使用任务标签;
  • 是否优化整个模型;
  • 是否只用少量校准样本;
  • 是否以任务损失还是重建误差为目标;
  • 是否持续多个 epoch;
  • 最终部署量化误差是否进入训练闭环。

只看其中一个维度,很容易误判。

10.2 一个实用分类原则

可以用下面三个问题判断:

问题 1:模型主体参数是否为目标低精度部署而更新?

如果没有,只是计算尺度、做搜索或局部补偿,更偏 PTQ。

问题 2:优化目标是否是完整任务损失?

如果主要最小化层输出重建误差,更偏 PTQ 校准;如果直接用语言建模损失、分类损失或蒸馏损失让模型适应量化,更偏 QAT。

问题 3:最终部署时使用的量化行为,是否在训练前向中被持续模拟?

如果是,这是 QAT 的强信号。

10.3 参数高效 QAT 改变了“QAT 一定贵得离谱”的印象

传统全参数 QAT 确实成本高,但低秩 QAT、LoRA 化 QAT、只训练 scale 或部分敏感层等方法,可以显著降低训练内存和更新参数量。LR-QAT 等工作就尝试用低秩更新实现更轻量的 LLM 量化感知训练。[7]

这不意味着 QAT 从此“和 PTQ 一样便宜”,而是说明选项不再只有两个极端:

  • 几分钟纯 PTQ;
  • 几周完整预训练。

中间还有大量量化感知微调方案。


十一、成本、精度、速度与显存:真正的工程权衡

“PTQ 快、QAT 准”只是第一层结论。真实选型至少要同时考虑五个维度:模型质量、量化耗时、训练资源、推理内存和实际延迟。

11.1 模型体积:先算理论下限,再看实际开销

假设模型有 (N) 个参数,每个参数使用 (b) bit,纯权重理论体积约为:

[
M_{weight}=\frac{N\times b}{8}\text{ Byte}
]

以 7B 模型为例:

权重精度 理论纯权重体积
FP32 约 28 GB
FP16/BF16 约 14 GB
INT8 约 7 GB
4-bit 约 3.5 GB
2-bit 约 1.75 GB

但实际显存或文件体积还会包括:

  • scale 与 zero-point;
  • 分组元数据;
  • 对齐和打包开销;
  • 未量化的 embedding、归一化、输出头或敏感层;
  • 运行时解包缓冲;
  • 激活与临时张量;
  • KV Cache;
  • 框架和设备上下文。

因此,“4-bit 就一定正好是 FP16 的四分之一”只能作为粗略估算,不能当作部署承诺。

11.2 推理速度:位宽下降不等于延迟按比例下降

很多人会自然地推断:权重从 16-bit 变成 4-bit,速度就应该快四倍。实际通常没有这么简单。

一次低比特推理可能经历:

  1. 从显存读取打包的低比特权重;
  2. 在寄存器、共享内存或融合内核中解包;
  3. 反量化或转换为计算类型;
  4. 执行矩阵乘;
  5. 写回高精度累积结果。

最终速度取决于:

  • 当前阶段是内存带宽瓶颈还是计算瓶颈;
  • GPU、CPU、NPU 是否有原生低比特指令;
  • 内核是否支持你的 group size、量化格式和模型结构;
  • 解包与反量化能否和矩阵乘融合;
  • batch、序列长度和并发大小;
  • prefill 与 decode 哪一段占主导;
  • KV Cache 是否仍是高精度;
  • 是否发生频繁的数据类型转换。

所以量化首先几乎总能带来“权重更小”的收益,但能否带来“明显更快”的收益,需要实际 benchmark。

一个常见情况是:单 token decode 对权重带宽敏感,weight-only 量化很有价值;而大 batch prefill 更偏计算密集,未必按同样比例加速。另一个常见情况是模型文件缩小很多,但使用的后端没有匹配内核,只能先解包成 FP16 再算,于是显存节省明显,吞吐提升却有限。

11.3 QAT 的训练成本为什么高

普通全参数训练已经需要保存:

  • 前向权重;
  • 梯度;
  • 优化器状态;
  • 激活;
  • 临时计算缓冲。

经典 fake-quant QAT 往往还需要:

  • 高精度主权重;
  • fake quant 节点;
  • observer 或量化参数;
  • 额外的转换与统计操作。

因此 QAT 不是“训练时直接用 INT4,所以训练显存自动变成四分之一”。传统 QAT 的低比特主要服务于最终推理数值模拟,训练更新通常仍依赖高精度参数和高精度优化器状态。

不过也不能把这句话绝对化。参数高效 QAT、低秩 QAT、冻结大部分参数、低精度优化器以及激活检查点,都可以降低训练成本。正确表述应该是:

经典全参数 fake-quant QAT 通常不会像低比特推理那样线性节省训练显存;现代参数高效方案可以缓解,但仍比纯 PTQ 更复杂。

11.4 精度优势不是只由 bit 决定

“8-bit PTQ 足够,2-bit QAT 更强”是常见趋势,却不是无条件定律。实际差异还受这些因素影响:

  • 模型架构;
  • 参数规模;
  • 权重和激活分布;
  • per-tensor、per-channel、per-group 粒度;
  • group size;
  • 对称或非对称量化;
  • 是否量化激活、KV Cache、embedding 和输出头;
  • 校准数据是否匹配;
  • PTQ 算法强弱;
  • QAT 数据量、学习率和训练轮数;
  • 业务指标对少数错误是否敏感。

一个 4-bit weight-only 模型在通用困惑度上几乎不掉,不代表它在数学推理、代码补全、JSON 约束输出或罕见语言上也不掉。量化对某些“边缘能力”的影响,可能比平均指标更明显。

11.5 “精度”必须换成你的业务指标

分类模型可以看:

  • Accuracy;
  • Precision / Recall / F1;
  • ROC-AUC;
  • 少数类召回;
  • 校准误差。

语言模型可以看:

  • Perplexity;
  • 任务 benchmark;
  • 代码编译通过率;
  • 数学题准确率;
  • 结构化输出合法率;
  • 工具调用成功率;
  • 人工偏好;
  • 长上下文一致性;
  • 安全和拒答行为。

部署指标则包括:

  • 首 token 延迟;
  • 每 token 延迟;
  • tokens/s;
  • 峰值显存;
  • 单请求和高并发吞吐;
  • 能耗;
  • 模型加载时间;
  • 文件体积。

如果只比较一个公开 benchmark,就可能选出“论文上最好、业务里最差”的配置。

11.6 PTQ 与 QAT 的收益曲线

可以把位宽从高到低想象成不断收紧的瓶颈:

高位宽区间

例如 FP8、INT8 或较温和的 weight-only 8-bit。量化格点仍较密,模型冗余足够,优秀 PTQ 常能取得很小损失。QAT 的边际收益可能不够覆盖训练成本。

主流低比特区间

例如 4-bit weight-only。现代 PTQ 已经很强,很多大模型可以保持较好的整体质量,但对任务、模型和后端仍需实测。QAT 可能继续提升,是否值得取决于业务价值。

极低比特区间

例如 3-bit、2-bit、三值甚至二值。量化级别骤减,舍入和截断误差变得难以仅靠校准解决。模型参数若能在训练中重新组织,通常更有价值,因此 QAT 或量化感知微调的吸引力上升。

权重与激活同时极低比特

W4A4、W2A4 等不仅要处理权重,还要处理输入相关的激活分布。此时 QAT 的价值通常比单纯 W4A16 更明显,但硬件、算子和导出的一致性也更难。

结论不是“低于 4-bit 一律 QAT”,而是:

位宽越低、量化对象越多、业务容错越小,越应该认真评估 QAT;位宽越高、PTQ 算法越成熟、训练资源越有限,越应该优先 PTQ。


十二、一个更公平、可复现的最小实验

原始玩具实验有几个值得修正的问题:

  1. PTQ 模型和 QAT 模型从不同随机初始化开始,结果不完全可比;
  2. 只跑一次随机种子,偶然性很大;
  3. 简单的“2-bit 对称量化”实际可能只有三个量化级别;
  4. scale 为 0 时存在除零风险;
  5. 直接硬编码一组“典型输出”,容易让读者误以为结果必然如此;
  6. QAT 从头训练 200 轮,而实际项目更常从同一个浮点检查点做短程量化感知微调;
  7. 代码只模拟权重量化,却容易让人误以为同时量化了激活;
  8. fake quant 仍执行浮点矩阵乘,不能用来测试真实 INT4 速度和显存。

下面给出一个更严谨的教学实验。它仍然是玩具模型,但做了四件事:

  • 所有 PTQ 和 QAT 都从同一个浮点检查点出发;
  • 使用真正包含 (2^b) 个编码的仿射量化;
  • 对 5 个随机种子取均值和标准差;
  • 明确只模拟 weight-only 量化,不声称获得真实整数加速。

12.1 实验目标

我们构造一个三分类螺旋数据集。这个任务对非线性决策边界有一定要求,又能在 CPU 上快速完成。

流程是:

  1. 训练一个浮点 MLP;
  2. 把同一检查点直接做 fake-quant 评估,模拟 PTQ 数值误差;
  3. 从同一检查点开始,用 fake-quant 前向做短程微调,模拟 QAT;
  4. 比较 8、4、3、2 bit;
  5. 重复 5 个随机种子。

12.2 完整代码

from __future__ import annotations

import statistics
from dataclasses import dataclass

import torch
import torch.nn as nn
import torch.nn.functional as F

# 可选:避免小模型在某些 CPU 上因线程过多反而变慢
# 删除这一行也不影响算法含义
torch.set_num_threads(2)


# ---------------------------------------------------------
# 1. 仿射伪量化:前向使用量化近似值,反向使用 identity STE
# ---------------------------------------------------------

def fake_quant_per_tensor_affine(
    x: torch.Tensor,
    bits: int,
    eps: float = 1e-8,
) -> torch.Tensor:
    """Per-tensor affine fake quantization with identity STE.

    说明:
    - 使用无符号整数编码 [0, 2**bits - 1];
    - zero-point 允许把包含正负值的浮点范围映射到该编码;
    - x_hat 是量化再反量化后的近似值;
    - x + (x_hat - x).detach() 让前向等于 x_hat,反向 d/dx 等于 1;
    - 这是教学实现,不等于某个生产后端的完整量化规则。
    """
    if bits < 2:
        raise ValueError("This demo expects bits >= 2.")

    qmin = 0
    qmax = 2**bits - 1

    # 量化参数不通过这个简化版本学习,因此 detach
    x_min = x.detach().amin()
    x_max = x.detach().amax()

    # clamp_min 避免全零张量导致除零
    scale = ((x_max - x_min) / float(qmax - qmin)).clamp_min(eps)

    zero_point = (qmin - x_min / scale).round()
    zero_point = zero_point.clamp(qmin, qmax)

    q = (x / scale + zero_point).round()
    q = q.clamp(qmin, qmax)

    x_hat = (q - zero_point) * scale

    # identity STE
    return x + (x_hat - x).detach()


# ---------------------------------------------------------
# 2. 只对权重做 fake quant 的线性层
# ---------------------------------------------------------

class QuantLinear(nn.Linear):
    def __init__(
        self,
        in_features: int,
        out_features: int,
        bits: int,
        bias: bool = True,
    ) -> None:
        super().__init__(in_features, out_features, bias=bias)
        self.bits = bits

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        w_hat = fake_quant_per_tensor_affine(self.weight, self.bits)
        return F.linear(x, w_hat, self.bias)


# ---------------------------------------------------------
# 3. 浮点网络与量化模拟网络
#    两者参数名和形状一致,方便加载同一 state_dict
# ---------------------------------------------------------

class FloatNet(nn.Module):
    def __init__(self) -> None:
        super().__init__()
        self.fc1 = nn.Linear(2, 64)
        self.fc2 = nn.Linear(64, 64)
        self.fc3 = nn.Linear(64, 3)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        x = torch.tanh(self.fc1(x))
        x = torch.tanh(self.fc2(x))
        return self.fc3(x)


class QuantNet(nn.Module):
    def __init__(self, bits: int) -> None:
        super().__init__()
        self.fc1 = QuantLinear(2, 64, bits=bits)
        self.fc2 = QuantLinear(64, 64, bits=bits)
        self.fc3 = QuantLinear(64, 3, bits=bits)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        x = torch.tanh(self.fc1(x))
        x = torch.tanh(self.fc2(x))
        return self.fc3(x)


# ---------------------------------------------------------
# 4. 三分类螺旋数据
# ---------------------------------------------------------

def make_spiral(
    seed: int,
    n_per_class: int = 400,
    noise: float = 0.18,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
    generator = torch.Generator().manual_seed(seed)

    xs: list[torch.Tensor] = []
    ys: list[torch.Tensor] = []

    for class_id in range(3):
        radius = torch.linspace(0.05, 1.0, n_per_class)
        angle = torch.linspace(
            class_id * 4.0,
            (class_id + 1) * 4.0,
            n_per_class,
        )
        angle = angle + torch.randn(n_per_class, generator=generator) * noise

        points = torch.stack(
            [radius * torch.sin(angle), radius * torch.cos(angle)],
            dim=1,
        )

        xs.append(points)
        ys.append(torch.full((n_per_class,), class_id, dtype=torch.long))

    x = torch.cat(xs, dim=0)
    y = torch.cat(ys, dim=0)

    permutation = torch.randperm(len(y), generator=generator)
    x = x[permutation]
    y = y[permutation]

    train_size = int(0.8 * len(y))
    return x[:train_size], y[:train_size], x[train_size:], y[train_size:]


# ---------------------------------------------------------
# 5. 训练和评估
# ---------------------------------------------------------

def fit(
    model: nn.Module,
    x_train: torch.Tensor,
    y_train: torch.Tensor,
    epochs: int,
    learning_rate: float,
) -> None:
    optimizer = torch.optim.AdamW(
        model.parameters(),
        lr=learning_rate,
        weight_decay=1e-4,
    )

    model.train()
    for _ in range(epochs):
        optimizer.zero_grad(set_to_none=True)
        logits = model(x_train)
        loss = F.cross_entropy(logits, y_train)
        loss.backward()
        optimizer.step()


@torch.no_grad()
def accuracy(
    model: nn.Module,
    x_test: torch.Tensor,
    y_test: torch.Tensor,
) -> float:
    model.eval()
    prediction = model(x_test).argmax(dim=1)
    return (prediction == y_test).float().mean().item()


@dataclass
class Result:
    seed: int
    bits: int
    float_acc: float
    ptq_acc: float
    qat_acc: float


def run_one_seed(seed: int) -> list[Result]:
    torch.manual_seed(seed)

    x_train, y_train, x_test, y_test = make_spiral(seed)

    # A. 先训练一个浮点基线
    float_model = FloatNet()
    fit(
        float_model,
        x_train,
        y_train,
        epochs=600,
        learning_rate=3e-3,
    )
    float_acc = accuracy(float_model, x_test, y_test)

    rows: list[Result] = []

    for bits in (8, 4, 3, 2):
        # B. PTQ 模拟:加载同一浮点检查点,不做训练,直接 fake quant 评估
        ptq_model = QuantNet(bits)
        ptq_model.load_state_dict(float_model.state_dict())
        ptq_acc = accuracy(ptq_model, x_test, y_test)

        # C. QAT 模拟:从同一检查点出发,小学习率做量化感知微调
        qat_model = QuantNet(bits)
        qat_model.load_state_dict(float_model.state_dict())
        fit(
            qat_model,
            x_train,
            y_train,
            epochs=150,
            learning_rate=3e-4,
        )
        qat_acc = accuracy(qat_model, x_test, y_test)

        rows.append(
            Result(
                seed=seed,
                bits=bits,
                float_acc=float_acc,
                ptq_acc=ptq_acc,
                qat_acc=qat_acc,
            )
        )

    return rows


# ---------------------------------------------------------
# 6. 多随机种子统计
# ---------------------------------------------------------

def mean_std(values: list[float]) -> str:
    mean = statistics.mean(values)
    std = statistics.stdev(values) if len(values) > 1 else 0.0
    return f"{mean:.4f} ± {std:.4f}"


def main() -> None:
    all_results: list[Result] = []

    for seed in range(5):
        all_results.extend(run_one_seed(seed))

    print("bits | float          | PTQ            | QAT")
    print("-----|----------------|----------------|----------------")

    for bits in (8, 4, 3, 2):
        current = [row for row in all_results if row.bits == bits]

        float_values = [row.float_acc for row in current]
        ptq_values = [row.ptq_acc for row in current]
        qat_values = [row.qat_acc for row in current]

        print(
            f"{bits:>4} | "
            f"{mean_std(float_values):<14} | "
            f"{mean_std(ptq_values):<14} | "
            f"{mean_std(qat_values):<14}"
        )


if __name__ == "__main__":
    main()

12.3 一次参考输出

在一组 CPU 环境中运行上述固定代码,可以得到类似结果:

bits | float          | PTQ            | QAT
-----|----------------|----------------|----------------
   8 | 0.9983 ± 0.0037 | 0.9983 ± 0.0037 | 0.9983 ± 0.0037
   4 | 0.9983 ± 0.0037 | 0.9983 ± 0.0037 | 0.9983 ± 0.0037
   3 | 0.9983 ± 0.0037 | 0.9983 ± 0.0037 | 0.9983 ± 0.0037
   2 | 0.9983 ± 0.0037 | 0.9575 ± 0.0252 | 0.9983 ± 0.0037

不同 PyTorch 版本、硬件、线程数和随机实现可能造成轻微差异,所以不要把这组数字当成固定答案。真正值得观察的是:

  • 8-bit、4-bit 和 3-bit 下,这个简单模型对 weight-only PTQ 很稳健;
  • 2-bit 时,直接 PTQ 的平均准确率下降且方差增大;
  • 从同一检查点做短程 QAT 后,模型重新适应了粗糙量化网格,恢复明显。

这个结果比“预先写死 8-bit 差 0.2%、4-bit 差 3%、2-bit 差 20%”更有教学价值,因为它同时说明:

QAT 的优势不是每个位宽都会自动出现;当 PTQ 本来就没有造成明显损失时,QAT 没有什么可恢复的。

12.4 这段代码到底模拟了什么

它模拟的是:

  • 每个线性层权重做 per-tensor 仿射 fake quant;
  • 激活保持浮点;
  • bias 保持浮点;
  • 前向使用量化再反量化的权重;
  • QAT 通过 identity STE 更新浮点主权重。

它没有模拟:

  • per-channel 或 per-group 量化;
  • 静态激活 observer;
  • 真正 INT4 矩阵乘;
  • 权重打包和硬件内核;
  • GPTQ 的二阶误差补偿;
  • AWQ 的激活感知缩放;
  • scale 学习;
  • 混合精度敏感层;
  • LLM 的注意力、归一化和 KV Cache。

因此它只能证明 QAT 的机制,不代表真实大模型上的绝对数值。

12.5 为什么这个实验比原版更公平

同一浮点起点

PTQ 与 QAT 都加载同一个 state_dict,差异来自是否进行量化感知微调,而不是随机初始化运气。

多随机种子

不仅看均值,还看标准差。2-bit PTQ 的方差变大,本身就是稳定性下降的信号。

完整使用量化编码

仿射量化使用 ([0,2^b-1]) 的全部编码,2-bit 真正对应 4 个整数级别。

不声称真实加速

fake quant 仍然执行浮点 F.linear,只模拟数值误差。要测速度,必须换成真实部署后端。

QAT 是微调而非从头重训

这更接近大多数现实项目:先有一个好模型,再让它适应目标低精度。


十三、如何正确解读实验结果

13.1 “2-bit QAT 恢复了”不等于所有 2-bit 大模型都能无损

玩具网络参数少、任务单一、训练集完整,QAT 很容易重新组织决策边界。大模型要同时保留语言、知识、推理、指令跟随、安全和长上下文能力,难度高得多。

极低比特大模型的效果还受到模型规模、训练 token、量化结构、蒸馏、架构设计和专用内核影响。不能用一个螺旋分类实验推出“2-bit LLM 都能回到 FP16”。

13.2 “4-bit 没差”也不等于 QAT 永远没用

这个实验只量化权重,而且任务简单。真实 W4A4、特殊小模型、边缘设备模型或严格业务指标下,4-bit QAT 仍可能有价值。

更重要的是,现代 PTQ 在大模型 4-bit weight-only 上已经非常强。如果你的对手是 AWQ、GPTQ、OmniQuant,而不是朴素 per-tensor min-max,QAT 的相对优势可能比玩具实验更小,也可能在特定任务上更大。必须实测。

13.3 不要只看平均准确率

假设平均准确率只下降 0.3%,但下降全部集中在高价值客户、唤醒词漏检、金融实体或安全拒答上,业务仍可能不能接受。

评估至少要分层:

  • 常见样本与长尾样本;
  • 短输入与长输入;
  • 中文、英文、代码和符号;
  • 普通任务与高敏感任务;
  • 平均质量与最坏情况;
  • 离线数据与真实线上流量。

13.4 量化评估必须固定解码设置

语言模型输出具有随机性。比较浮点和量化模型时,应尽量固定:

  • prompt;
  • tokenizer;
  • chat template;
  • max tokens;
  • temperature;
  • top-p / top-k;
  • random seed;
  • stop token;
  • 系统提示;
  • 上下文截断方式。

否则你看到的差异可能来自采样,而不是量化。

13.5 同一个模型要同时测质量与系统指标

一个完整表格至少应该长这样:

配置 质量指标 文件体积 峰值显存 首 token 延迟 tokens/s 备注
FP16 基线 基线 基线 原模型
INT8 PTQ 轻微变化 实测 实测 后端 A
W4A16 GPTQ 实测 实测 实测 group 128
W4A16 AWQ 实测 实测 实测 kernel B
W4A4 QAT 实测 更小 更低 实测 实测 目标 NPU

没有系统指标,无法判断压缩是否真正转化为部署价值;没有质量指标,速度再快也可能失去业务意义。


十四、真实项目中的 PTQ 工作流

下面给出一套比“加载模型—点一下量化—结束”更可靠的 PTQ 流程。

14.1 第一步:先写清部署目标,不要先选算法

至少回答这些问题:

  • 部署在 NVIDIA GPU、AMD GPU、CPU、Apple Silicon、手机 NPU,还是 MCU?
  • 目标后端真正支持哪些位宽和算子?
  • 更缺显存、内存带宽,还是算力?
  • 主要优化首 token、decode 吞吐,还是批量 prefill?
  • 上下文多长,KV Cache 占比多大?
  • 可接受的质量损失是多少?
  • 模型要不要频繁切换或动态加载?

量化不是脱离硬件的纯数学题。一个质量极好的格式,如果目标设备没有高效内核,可能不如稍弱但硬件原生支持的格式。

14.2 第二步:建立浮点基线

在量化之前,先固定并记录:

  • 模型版本和权重哈希;
  • tokenizer 版本;
  • chat template;
  • 推理框架版本;
  • 解码参数;
  • 测试集;
  • 质量指标;
  • 延迟和显存基线。

没有浮点基线,后面就无法区分:问题来自量化、框架变化、模板变化,还是模型本身。

14.3 第三步:选择量化对象

不要一上来就“全模型 4-bit”。先确定:

  • 只量化 Linear 权重?
  • embedding 是否量化?
  • 输出头是否保留高精度?
  • attention 与 MLP 是否同位宽?
  • 激活是否量化?
  • KV Cache 是否量化?
  • 是否允许混合精度?

在很多项目中,少量敏感层保留高精度,带来的文件增量很小,却能显著降低质量风险。

14.4 第四步:构造校准集

校准集不一定需要标签,但要有代表性。

对于大语言模型,可以考虑覆盖:

  • 真实用户问题长度分布;
  • 中文与英文比例;
  • 代码、数学、表格或 JSON;
  • 系统提示和 chat template;
  • 长上下文片段;
  • 特殊 token;
  • 业务高价值场景;
  • 容易出现离群激活的格式。

不要直接把公开通用语料当作万能校准集。你的线上请求如果高度结构化,校准也应该反映这种结构。

14.5 第五步:从保守配置开始

合理的试验顺序通常是:

  1. INT8 或 8-bit weight-only;
  2. 4-bit per-channel / per-group;
  3. 调整 group size;
  4. 比较对称与非对称;
  5. 比较 AWQ、GPTQ 或目标后端原生方案;
  6. 再尝试 3-bit、2-bit 或激活量化。

先建立一条稳定的 Pareto 曲线,比一上来追求“最低 bit”更有意义。

14.6 第六步:逐层定位敏感点

如果整体质量下降,不要只得出“4-bit 不行”。可以做层级消融:

  • 一次只量化一层;
  • 恢复某个模块为 FP16;
  • 比较 attention 和 MLP;
  • 比较前层、中层和后层;
  • 观察输出头、embedding、归一化附近;
  • 比较不同 group size;
  • 记录每层重建误差与任务指标变化。

常见结果是:并不是所有层都同样敏感。混合精度往往能用少量体积换回大量质量。

14.7 第七步:在真实后端 benchmark

量化完成后,必须在最终后端测试:

  • 模型是否能正确加载;
  • 是否触发预期低比特内核;
  • 是否出现隐式反量化到 FP16;
  • batch 1 与高 batch;
  • 短 prompt 与长 prompt;
  • prefill 与 decode;
  • 冷启动与热启动;
  • 峰值内存;
  • 多并发稳定性。

只看模型文件大小,无法判断系统收益。

14.8 第八步:做线上分布验证

离线集通过后,再用匿名化、合规处理的真实业务样本做影子评估或灰度测试。重点关注:

  • 长尾错误;
  • 格式失败;
  • 语义漂移;
  • 拒答变化;
  • 事实性下降;
  • 工具参数错误;
  • 特定语言退化;
  • 线上延迟尾部。

PTQ 的速度优势很大,但质量风险常隐藏在平均指标之外。


十五、真实项目中的 QAT 工作流

QAT 的成本更高,因此更需要有纪律的流程。不要在 PTQ 还没充分调优前,就直接投入昂贵训练。

15.1 第一步:证明 PTQ 确实不够

先回答:

  • 最强可用 PTQ 方法做过了吗?
  • 校准数据匹配真实业务吗?
  • group size、clipping、敏感层保留调过了吗?
  • 质量损失来自权重还是激活?
  • 后端是否存在格式转换问题?
  • 失败是否只集中在少数层或少数任务?

如果 PTQ 只是因为配置粗糙而失败,QAT 会白白增加复杂度。

15.2 第二步:冻结目标部署规格

QAT 前向必须尽量模拟最终部署。需要提前确定:

  • 位宽;
  • 对称或非对称;
  • per-tensor、per-channel 或 per-group;
  • group size;
  • clipping 规则;
  • zero-point;
  • 哪些算子量化;
  • 累积精度;
  • 权重打包限制;
  • 后端是否支持混合精度。

如果训练模拟的是 A 量化器,导出后却运行 B 量化器,QAT 学到的鲁棒性可能无法兑现。

15.3 第三步:从浮点检查点开始,而不是默认从零训练

多数项目可以先做:

  1. 加载高质量浮点模型;
  2. 插入 fake quant;
  3. 低学习率 warm-up;
  4. 短程微调;
  5. 观察质量恢复;
  6. 再决定是否扩大训练。

这能快速判断 QAT 是否值得。

15.4 第四步:选择训练参数范围

不一定要全参数更新。可以依次尝试:

  • 只训练 scale / clipping;
  • 只训练 LoRA 或低秩参数;
  • 只训练敏感层;
  • 训练 attention 或 MLP 的部分模块;
  • 全参数 QAT。

原则是用最小训练自由度解决问题。参数越多,恢复潜力越大,但训练成本、过拟合和稳定性风险也越高。

15.5 第五步:设计量化启用日程

直接从第一步开始对所有层施加强量化噪声,可能使训练不稳定。常见策略包括:

  • 先以浮点或较高位宽 warm-up;
  • 逐步启用 fake quant;
  • 先量化权重,再量化激活;
  • 先训练 observer,再冻结统计;
  • 后期冻结 scale 和 zero-point;
  • 位宽逐步降低;
  • 对敏感层延迟启用。

并非所有模型都需要复杂日程,但低比特和激活量化越激进,训练计划越重要。

15.6 第六步:监控的不只是训练损失

训练过程中同时看:

  • 浮点 shadow 权重上的损失;
  • fake-quant 前向损失;
  • 导出模拟后的验证指标;
  • scale 分布;
  • clipping 比例;
  • 饱和元素比例;
  • 梯度范数;
  • 敏感层误差;
  • 量化前后输出 KL 或重建误差;
  • 任务指标和长尾指标。

如果训练损失在降,而饱和比例不断上升,模型可能在“钻 fake quant 的空子”,导出后未必稳定。

15.7 第七步:必要时使用蒸馏

QAT 可以结合教师模型:

  • 教师使用浮点模型;
  • 学生使用 fake-quant 模型;
  • 同时优化标签损失与教师输出差异;
  • 必要时匹配中间层表示。

蒸馏能给低比特模型更密集的学习信号,特别适合标签有限或极低比特场景。但它会进一步增加训练复杂度。

15.8 第八步:真正 convert 后再评估

QAT 模型在 fake-quant 模式下表现好,不代表导出的整数模型一定一致。最终必须:

  1. 冻结量化参数;
  2. 执行 convert/export;
  3. 使用目标内核;
  4. 重新跑完整质量测试;
  5. 对比 fake-quant 与真实部署输出;
  6. 检查算子回退和精度混用。

这一环节叫 deployment parity,也就是训练模拟与真实部署一致性。它是 QAT 成败的最后一道关。

15.9 第九步:保留可回退方案

生产系统中可以同时保留:

  • FP16 基线;
  • 稳健的 PTQ 版本;
  • 激进 QAT 版本;
  • 不同上下文或任务的路由策略。

当 QAT 版本在某些长尾任务不稳定时,可以按场景回退,而不是把整个系统押在单一低比特模型上。


十六、一套可以直接使用的选型决策树

下面这套判断顺序比“能 PTQ 就 PTQ”更完整。

第一个问题:目标硬件到底支持什么?

只支持或擅长 FP16/BF16

量化可能主要用于节省存储,未必能加速。先确认后端是否有低比特权重内核。

支持 INT8,但不支持高效 INT4

优先尝试 INT8 PTQ、SmoothQuant 类 W8A8 或动态/静态 INT8。不要为了文件更小而选一个最终会回退的 INT4 格式。

支持高效 4-bit weight-only

优先 AWQ、GPTQ 或后端原生 4-bit PTQ,比较质量与内核吞吐。

必须 W4A4、W2A4 或更低

先做最强 PTQ,若质量不达标,QAT 的优先级显著上升。

第二个问题:你有没有训练数据和训练能力?

没有训练数据或无法复现训练流程

基本锁定 PTQ。可以改进校准集、采用更强 PTQ、混合精度和敏感层保护。

有少量领域数据,但没有大规模预训练资源

尝试量化感知微调、LoRA 化 QAT、蒸馏或只训练量化参数。

有完整训练数据和训练管线

可以系统比较 PTQ、短程 QAT、全参数 QAT,按收益决定成本。

第三个问题:PTQ 的质量差距有多大?

几乎无差

直接部署 PTQ。QAT 没有明显恢复空间。

有轻微下降,但业务可接受

优先调校准、group size、混合精度和敏感层。不要急着增加训练复杂度。

明显下降,且集中在少数层或任务

尝试混合精度、局部 QAT、LoRA QAT、蒸馏。

全面崩溃

检查配置是否错误:量化格式、tokenizer、导出、内核、激活范围。确认不是实现 bug 后,再考虑更高位宽或完整 QAT。

第四个问题:瓶颈是权重、KV Cache 还是计算?

权重加载与显存是瓶颈

Weight-only PTQ 往往最划算。

长上下文 KV Cache 是瓶颈

仅量化权重可能不够,应评估 KV Cache 量化、分页缓存、注意力实现和上下文策略。

大 batch prefill 计算是瓶颈

需要关注激活量化、低精度矩阵乘和硬件原生计算,而不只是模型文件大小。

第五个问题:模型是否会频繁更新?

每周或每天都换检查点

PTQ 自动化更有优势。每次都做完整 QAT 可能无法承受。

模型版本稳定、部署规模巨大

一次 QAT 成本可以被长期推理收益摊薄。即使只提升几个百分点质量或吞吐,也可能值得。

一句话决策版

可以把它压缩成下面这段:

先看硬件支持
  ↓
用最强、匹配后端的 PTQ 建立基线
  ↓
若质量达标:部署 PTQ
  ↓
若不达标:先调校准、粒度、混合精度和敏感层
  ↓
仍不达标且有数据:量化感知微调
  ↓
极低比特或严格指标:参数高效 QAT / 全参数 QAT / 蒸馏

十七、常见误区与避坑

误区 1:QAT 一定比 PTQ 好,所以有资源就直接 QAT

QAT 提供更大的适应空间,但不是自动胜利。训练配置不当、fake quant 与部署不一致、数据分布偏移或过拟合,都可能让 QAT 不如成熟 PTQ。

正确做法:先建立强 PTQ 基线,再衡量 QAT 的增量收益。

误区 2:PTQ 完全不需要数据

最朴素权重量化可以无数据,但激活量化、AWQ、GPTQ 和许多高质量 PTQ 都会使用校准输入。

正确说法:PTQ 不需要完整训练数据和训练标签,但可能需要少量代表性校准数据。

误区 3:只要用了校准数据,就不算 PTQ

校准正是 PTQ 的常见组成部分。是否使用数据不是 PTQ/QAT 的唯一分界。

误区 4:代码里出现 backward,就一定是 QAT

局部重建、量化尺度优化和 clipping 搜索也可能使用梯度,仍被归为 PTQ。

关键看是否让模型主体针对最终量化行为进行任务级适应。

误区 5:AWQ、GPTQ 是静态激活量化

它们常见形态是离线 weight-only PTQ。激活通常仍是 FP16/BF16,不应与经典 W8A8 静态量化混为一谈。

误区 6:GGUF 是一种和 GPTQ 并列的算法

GGUF 是模型容器格式。量化模型可以存进 GGUF,但格式本身不是某一个量化优化算法。

误区 7:QLoRA 就是 QAT

QLoRA 通过冻结 4-bit 基础模型并训练 LoRA 适配器降低微调显存。它可以与 QAT 思想结合,但不能自动等同于经典 QAT。

误区 8:4-bit 模型一定比 FP16 快四倍

位宽只决定部分存储与带宽成本。实际速度还受低比特内核、解包、反量化、batch、序列长度、KV Cache 和算子回退影响。

误区 9:量化后的文件能加载,就说明量化成功

能加载只说明格式兼容。你还需要检查质量、实际内核、速度、显存和长尾行为。

误区 10:校准数据越多越好

校准更依赖覆盖性。大量重复样本既增加耗时,也可能把量化参数过度偏向某一分布。

正确做法:优先覆盖真实场景、长度、语言、格式和难例。

误区 11:困惑度几乎不变,聊天质量就一定不变

Perplexity 是重要信号,但无法覆盖结构化输出、工具调用、代码、数学、安全和人类偏好。应做多维评估。

误区 12:QAT 能节省训练显存,因为模型已经低比特

经典 QAT 通常仍保留高精度主权重、梯度和优化器状态。fake quant 的主要目标是模拟推理误差,不是自动把训练变成纯 INT4。

误区 13:STE 是 round 的正确导数

STE 是替代梯度,是有偏近似,不是数学上的真实导数。它工程上有效,但需要接受训练不稳定与部署不一致的风险。

误区 14:所有 2-bit 实现都有四个有效级别

不一定。若使用窄范围对称量化并简单裁剪到 ([-1,1]),可能只有三值。阅读代码时要检查 qmin、qmax、zero-point 和实际格点。

误区 15:只看权重误差最小,就能选出最好量化方案

模型对不同方向的误差敏感度不同。任务损失、层输出重建、激活统计与二阶信息往往比单纯 (L_2) 权重误差更有意义。

误区 16:量化一次,就能适用于所有业务分布

校准集和线上分布变化会影响激活与敏感通道。模型从通用聊天切到代码、长上下文或特定语言时,需要重新验证,必要时重新校准。


十八、PTQ vs QAT 横向对比总表

维度 PTQ QAT
全称 Post-Training Quantization Quantization-Aware Training
核心时机 浮点模型训练完成后 训练或微调阶段
核心目标 在既有检查点附近找到低误差量化表示 让参数主动适应目标量化误差
是否一定无梯度 不一定,局部优化可用梯度 通常需要反向传播
是否更新模型主体 通常不做任务级更新 可更新全部、部分或低秩参数
数据需求 无数据或少量无标签校准数据 训练/微调数据,通常更多
标签需求 通常不需要 取决于训练目标,可有监督、无监督或蒸馏
时间成本 秒到小时,取决于模型与算法 小时到天,取决于规模与训练范围
工程门槛 较低 较高
适合位宽 INT8、4-bit 等主流区间尤其常见 极低比特、权重激活同时低比特更有价值
高比特精度 强 PTQ 往往已很好 可能略好,但边际收益有限
极低比特精度 更容易退化 通常更有恢复空间
对原训练流程依赖 中到高
对校准数据依赖 视方法而定 训练数据与验证数据更重要
典型风险 校准偏移、离群值、敏感层、后端不匹配 训练不稳、成本高、fake/real mismatch、过拟合
典型方法 GPTQ、AWQ、SmoothQuant、OmniQuant 经典 fake-quant QAT、LSQ、PACT、低秩 QAT
大模型常见策略 优先选择 PTQ 不达标后再做量化感知微调
最适合的使用者 只有检查点、追求快速部署的人 有训练数据、管线和严格目标的人

再补一张“容易混淆的名词表”:

名称 更准确的身份 通常与 PTQ/QAT 的关系
GPTQ 近似二阶、one-shot 权重量化算法 PTQ
AWQ 激活感知的 weight-only 量化算法 PTQ
SmoothQuant 面向 W8A8 的训练后平滑方法 PTQ
OmniQuant 优化量化参数的校准方法 PTQ,虽可用可微优化
GGUF 模型张量与元数据容器格式 可承载 PTQ 量化结果,本身非算法
llama.cpp quantize 量化与转换工具 常用于离线 PTQ 工作流
bitsandbytes 低精度算子与加载工具库 视具体功能而定
QLoRA 冻结 4-bit 基础模型、训练 LoRA 的高效微调方法 不自动等同于 QAT
Fake Quant 量化再反量化的训练模拟操作 QAT 核心组件
STE 离散操作的替代梯度估计 QAT 常用技术

十九、小结:先 PTQ,不迷信 PTQ;需要 QAT,也不必从头重训

现在可以回到开头那个问题:

同样是模型量化,为什么有的方法几分钟完成,有的却要重新训练好几天?

因为它们解决误差的方式不同。

PTQ 接受“模型已经训练完”这个前提,不再大规模改变模型,只利用权重统计、校准数据、误差补偿、等价变换或局部优化,寻找一个尽可能好的低比特表示。它把成本压得很低,因此特别适合大模型和第三方检查点。

QAT 则把量化误差放进训练前向,让模型在优化过程中亲自适应。权重可以移动,通道可以重新分工,scale 可以学习,敏感方向可以被任务损失直接保护。它因此更有机会在极低比特或严格指标下恢复质量,但需要数据、算力、训练管线和部署一致性。

全文最重要的结论可以压缩成十条:

  1. PTQ 的本质不是“完全不用数据”,而是“不做完整量化感知训练”。
  2. QAT 的本质不是“真的用整数训练”,而是训练时模拟最终量化误差。
  3. 权重比激活更容易量化,因为权重固定,激活依赖输入。
  4. 量化误差主要来自舍入和截断,离群值会恶化两者的权衡。
  5. AWQ 和 GPTQ 属于 PTQ,但会使用校准输入;它们常见形态是 weight-only。
  6. GGUF 是容器格式,不是与 AWQ、GPTQ 同层级的量化算法。
  7. QLoRA 是低显存微调方法,不应只因“4-bit 训练”就自动归为经典 QAT。
  8. STE 不是 round 的真实导数,而是让离散前向可训练的替代梯度。
  9. 低 bit 不自动带来等比例加速,最终效果取决于内核、硬件、KV Cache 和工作负载。
  10. 正确选型不是死记“能 PTQ 就别 QAT”,而是先用匹配硬件的强 PTQ 建立基线;质量仍不达标且有训练条件时,再逐级增加 QAT 强度。

最实用的一句话仍然可以保留,但要加上边界:

大模型部署先做 PTQ;PTQ 在真实后端和真实业务指标上不达标,再考虑量化感知微调。端侧极低比特、权重激活同时量化或严格精度场景,应更早把 QAT 纳入方案。


二十、附录:术语表与延伸阅读

20.1 术语表

Quantization,量化

用有限离散级别近似高精度数值的过程。

Dequantization,反量化

根据 scale 和 zero-point,把整数编码还原成近似浮点值。

Scale

量化网格的步长。scale 越小,格点越密,但可覆盖范围越窄。

Zero-point

浮点 0 在整数编码中的对应位置。非对称量化中常不为 0。

Calibration,校准

用代表性数据统计激活、敏感度、范围或重建误差,以确定量化参数。

Observer

训练或校准时收集 min/max、直方图、移动平均等统计信息的模块。

Fake Quantization,伪量化

前向执行量化再反量化,让数值承受量化误差,但保持浮点张量便于训练。

STE,Straight-Through Estimator

对不可导或离散操作使用替代梯度的技术。最简单版本在反向把量化节点近似成恒等映射。

Weight-only Quantization

只把权重压到低比特,激活通常保持 FP16/BF16 或其他较高精度。

Activation Quantization

把中间激活也映射到低比特。通常比权重量化更依赖输入分布和校准。

Per-tensor

整个张量共享量化参数。

Per-channel

每个通道使用独立量化参数。

Per-group

每一小组权重共享量化参数,是 LLM 低比特权重常见折中。

Group Size

每组包含的权重数量。更小通常更精细,但元数据和内核复杂度更高。

Clipping

把超出量化范围的数值压到边界。

Saturation,饱和

数值超出可表示范围并被截断到 qmin 或 qmax 的现象。

Mixed Precision,混合精度

不同层、通道或算子使用不同位宽,以在质量和成本间折中。

W4A16

权重 4-bit,激活 16-bit。常见于 LLM weight-only 推理。

W8A8

权重与激活均 8-bit,更有机会进入完整整数矩阵乘路径。

KV Cache

自回归 Transformer 为历史 token 保存的 Key/Value 状态。长上下文下可能占用大量显存。

Deployment Parity

训练或仿真量化行为与真实部署内核之间的一致性。

20.2 延伸阅读

[1] Elias Frantar, Saleh Ashkboos, Torsten Hoefler, Dan Alistarh. GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers. arXiv:2210.17323 / ICLR 2023.

[2] Ji Lin 等. AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration. arXiv:2306.00978.

[3] Guangxuan Xiao 等. SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models. arXiv:2211.10438.

[4] ggml-org/llama.cpp. tools/quantize/README.md,关于从高精度 GGUF 转换到量化格式的工具说明。

[5] Tim Dettmers, Artidoro Pagnoni, Ari Holtzman, Luke Zettlemoyer. QLoRA: Efficient Finetuning of Quantized LLMs. arXiv:2305.14314 / NeurIPS 2023.

[6] PyTorch. Quantization-Aware Training for Large Language Models with PyTorch. 介绍 torchao 与 torchtune 中的 LLM QAT 流程。

[7] Peiyu Liao 等. Low-Rank Quantization-Aware Training for LLMs. arXiv:2406.06385.

[8] Yoshua Bengio, Nicholas Léonard, Aaron Courville. Estimating or Propagating Gradients Through Stochastic Neurons for Conditional Computation. arXiv:1308.3432.

[9] Steven K. Esser 等. Learned Step Size Quantization. arXiv:1902.08153.

[10] Benoit Jacob 等. Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference. arXiv:1712.05877.

[11] Wenqi Shao 等. OmniQuant: Omnidirectionally Calibrated Quantization for Large Language Models. arXiv:2308.13137.

[12] Jungwook Choi 等. PACT: Parameterized Clipping Activation for Quantized Neural Networks. arXiv:1805.06085.


最后一张记忆卡

PTQ
= 已有模型 + 量化/校准/局部优化
= 快、便宜、适合大模型第一轮部署

QAT
= Fake Quant + 任务训练 + STE/替代梯度
= 让模型适应量化误差
= 低比特、激活量化、严格精度时更有价值

AWQ / GPTQ
= PTQ 的 weight-only 方法

SmoothQuant
= 面向 W8A8 的 PTQ 思路

GGUF
= 容器格式,不是量化算法

QLoRA
= 冻结 4-bit 基础模型训练 LoRA
= 不自动等于经典 QAT

选型
= 先看硬件 → 建立强 PTQ 基线 → 调校准与混合精度
→ 仍不达标且有训练条件 → QAT
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐