LLM Intro(八):理解量化、AWQ 与 QLoRA

LoRA 减少了需要更新的参数,但基础模型依然占用大量内存。量化进一步追问:保存一个权重,是否一定需要 16 或 32 个 bit?如果允许可控的数值误差,就可以用更少的离散值表示它,减少存储和数据搬运。

代价在于,离散表示会改变计算结果,而且压缩格式必须得到实际算子支持。理解量化,既要知道一个数怎样被编码,也要知道误差怎样影响整层输出。

浮点精度与整数编码

浮点数由符号、指数和有效数字部分组成。指数影响可表示的数量级,有效数字影响给定数量级附近的分辨能力。FP16 和 BF16 都占 16 bit,却在指数范围与有效数字精度之间作了不同分配,因此“同样是 16 bit”不代表数值行为一样。

整数没有浮点数的指数结构。把 [0.1, 0.2, 0.9] 直接转换成整数,会把三个数都截成 0。量化并非简单类型转换,而是建立浮点值与整数编码之间的映射,再保存恢复数值所需的尺度信息。

scale 和 zero-point 如何工作

常见的仿射量化写作:

q=clip⁡(round⁡(x/s)+z,qmin,qmax),x^=s(q−z).q=\operatorname{clip}\left(\operatorname{round}(x/s)+z, q_{min},q_{max}\right),\qquad \hat x=s(q-z).

s>0s>0 是 scale,决定相邻编码之间的间隔;zz 是 zero-point,让整数 zz 对应实数 0;clip 把结果限制在编码范围内。量化得到 qq,反量化得到近似值 x^\hat x,一般不能恢复原来的全部信息。

采用对称量化时,可以让 z=0z=0,范围围绕零展开。例如使用整数 [-127,127] 表示实数 [-1,1],则 s=1/127s=1/127。数值 0.30.3 对应编码 38,恢复后约为 0.2992。若使用非对称量化,可以针对偏向正值或负值的分布调整区间,提高编码范围的利用率,但需要处理零点偏移。

下面用普通 Python 演示对称映射。返回的整数列表只是编码示意,并没有真的把 Python 对象压缩成 8 bit:

def quantize_symmetric(values, bits=8):
    if bits < 2 or not values:
        raise ValueError("Use at least two bits and a nonempty input.")
    qmax = 2 ** (bits - 1) - 1
    peak = max(abs(x) for x in values)
    scale = peak / qmax if peak else 1.0
    codes = [max(-qmax, min(qmax, round(x / scale))) for x in values]
    restored = [code * scale for code in codes]
    return codes, scale, restored

codes, scale, restored = quantize_symmetric([-1.0, 0.0, 0.3, 1.0])
print(codes)     # [-127, 0, 38, 127]
print(restored)  # [-1.0, 0.0, 0.2992125984251969, 1.0]

全零输入单独处理,是为了避免 scale 变成零。这里假设输入有限;实际实现还要明确舍入规则、布局和异常值处理。

舍入与截断是两种不同误差

量化中的舍入误差与超出表示范围时的截断误差

即使数值落在表示范围内,它也未必刚好位于离散网格上,取最近编码会带来舍入误差。若数值超过表示范围,就会被压到边界,产生截断误差。

扩大范围能容纳离群值,但在编码数量固定时,网格间距也随之增大,让大量常见数值的舍入误差变大。缩小范围能更细致地表达主要分布,却会牺牲两端。因此选择范围不是简单地“越大越好”,需要考虑最终输出误差。

为什么要分 channel、分 group

如果整个权重张量共用一个 scale,少数特别大的数可能决定所有权重的分辨率。把张量分成更小的区域,分别计算 scale,就能适应局部的数值分布。

按 tensor、channel 和 group 选择量化参数的方式

Per-tensor 为整个张量共享参数;per-channel 按某个指定轴分配参数;group-wise 则把指定维度上相邻的一组元素放在一起。这里的“channel”必须结合权重布局解释,不能只看矩阵的横竖方向。

分组越细,通常越容易控制误差,但需要保存更多 scale、zero-point,也可能增加内核处理成本。若每组有 gg 个 4-bit 权重并附带一个 16-bit scale,仅这两项的平均存储就是 4+16/g4+16/g bit/权重;还有零点或其他元数据时要继续计入。所以模型标注“4-bit”,并不意味着每个参数连同附加信息恰好只占 4 bit。

PTQ:在训练完成后确定量化参数

Post-Training Quantization(PTQ)从已经训练好的模型出发,选择量化范围、尺度或补偿方式,然后导出适合推理的权重。需要激活统计的方法通常会先运行一小批校准数据;这些数据可以不带人工标签,但应覆盖目标使用场景、长度与输入格式。

还要区分量化对象。Weight-only 主要压缩权重,输入激活和累加可能仍使用较高精度;同时量化权重与激活,则对算子和数值稳定性提出更多要求。类似 W4A16 的记号描述这两个部分的位宽,并没有单独说明 scale 格式、分组大小或累加精度。

校准完成后,不能只看权重均方误差。对 Y=WXY=WX,量化误差 ΔW\Delta W 会产生:

ΔY=ΔWX.\Delta Y=\Delta W X.

某个权重的误差即使不大,若总与幅度很大的激活相乘,也可能显著影响输出。这正是激活分布值得参与权重量化决策的原因。

AWQ 为什么关注显著通道

保护少量显著权重对量化后模型效果的影响

少量重要通道可能对误差特别敏感,但重要性不能只靠权重本身的绝对值判断。AWQ 使用校准输入的激活信息识别这种敏感性,并通过通道缩放改善低位宽权重量化。AWQ 论文讨论了这种方法及其推理实现。

仍使用 Y=WXY=WX 的列向量约定,取一个对角缩放矩阵 SS:

Y=(WS)(S−1X).Y=(WS)(S^{-1}X).

浮点运算中,这只是等价变换;但对 WSWS 量化后,误差在乘回 S−1XS^{-1}X 时会发生变化。选择合适的通道尺度,可以给敏感通道更有利的量化条件,同时平衡其他通道的误差。

“保护重要权重”并不等于部署时必须把这部分权重逐个留成 FP16。图中的对比用于说明敏感性,AWQ 的缩放方法则把这种观察转化成适合低位宽权重的表示。它仍然会有量化误差,收益取决于模型、校准数据、位宽与分组设置。

QLoRA:低精度存储基础权重,高精度训练适配器

QLoRA 解决的是另一种需求:在较少显存下做微调。它把冻结的基础权重以低位宽形式保存,在计算时按需要反量化,同时训练较高精度的 LoRA 参数:

Y=dequant⁡(Wq)X+αrBAX.Y=\operatorname{dequant}(W_q)X+\frac{\alpha}{r}BAX.

基础权重不更新,优化器主要维护适配器的状态。但梯度仍需要经过基础模型的运算传播,所以这并不是把整个基础模型从计算图中删掉。反量化也可以按块在算子内完成,不一定先永久生成一份完整浮点权重。

QLoRA 使用的 NF4 是一种非均匀 4-bit 编码:16 个编码对应一组预设代表值,再配合分组尺度。它针对近似正态的权重分布设计,让较密集的区域得到更细的表示;它既不是普通均匀 INT4,也不意味着所有模型都能无损量化。存储采用 NF4,不代表乘法、累加或适配器更新也都在 4 bit 下进行。QLoRA 论文给出了该表示及训练方法。

实际接入时,先用量化后端加载冻结基础模型,设置独立的计算 dtype,再进行低位宽训练准备、注入 LoRA,最后使用与普通 SFT 一致的样本和标签。训练准备包括冻结基础参数、按实现需要处理敏感层的精度和梯度路径。仅设置 load_in_4bit,还不能代替后续这些步骤。PEFT 的 量化训练说明展示了 BitsAndBytesConfig、prepare_model_for_kbit_training 与适配器注入之间的衔接。

从理论压缩率到实际收益

70 亿个权重按每个 4 bit 计算,纯编码约为 3.5 GB。运行时仍有尺度信息、未量化模块、激活、缓存和工作区;QLoRA 训练还包含适配器及其状态。因此这只是权重编码的估计,不是显存需求承诺。

量化也不保证更快。如果缺少匹配内核,反量化和布局转换可能抵消搬运量减少的收益。选择格式时,需要同时确认硬件、推理引擎或训练后端是否支持,并在实际 batch、序列长度和任务上测量质量、峰值显存、延迟与吞吐量。

若要比较普通 LoRA 与 QLoRA,保持数据、目标模块、训练预算和评测方式一致,才容易看清低精度基础权重带来的影响。若目标是部署 PTQ 模型,则更关心在可接受质量下降下,整个服务是否确实节省了资源。


系列目录 · 上一篇:用 SFT 与 LoRA 微调语言模型 · 下一篇:RAG 的检索、重排与生成

本文采用 CC BY-NC-SA 4.0 协议