4、双重量化(Double Quantization):动机与原理、量化常数再量化、内存节省分析
4.1 为什么要搞双重量化?
聊到QLoRA,双重量化是个绕不开的点。说实话,我第一次看到这个名词的时候,心里想的是:「量化就量化呗,还双重,是不是搞复杂了?」
后来真正动手做项目才发现——嗯,这里确实有门道。
咱们先回顾一下常规的量化过程。你在做NF4量化时,每个权重块(比如64个参数一组)都会产生一个量化常数(scale factor)。这个常数本身是FP32精度的。你想想看,如果模型有几十亿参数,分成的块数可能上百万甚至上千万,那这些量化常数加起来,占用的显存可不是小数目。
我遇到过一位同事,他做7B模型的QLoRA微调,发现显存占用比预期高了将近2GB。排查了半天,最后发现——就是这些量化常数在「偷内存」。
双重量化的思路很简单:对量化常数再做一次量化。说白了,就是把那些FP32的scale factor,也用低精度格式存起来。
4.2 量化常数再量化的具体做法
具体怎么操作呢?我习惯把它拆成两步来看:
- 第一重量化:对模型权重做NF4量化,产生一组FP32的量化常数(每个块一个)。
- 第二重量化:把这组FP32的量化常数,再打包成FP8或者INT8格式。
你可能会问:「量化常数本身范围很宽,再量化会不会丢精度?」
好问题。实际上,量化常数的分布非常集中——它们通常都在一个相对窄的范围内。我在项目中实测过,大部分scale factor集中在0.1到10之间。这个范围用FP8表示,几乎不会有精度损失。
具体实现上,QLoRA的做法是:
# 伪代码示意
# 第一重:NF4量化权重
quantized_weights, scale_factors_fp32 = nf4_quantize(weights, block_size=64)
# 第二重:对scale factors做FP8量化
scale_factors_fp8 = fp8_quantize(scale_factors_fp32, block_size=256)
# 最终存储:quantized_weights + scale_factors_fp8
注意这里的block_size=256,意思是每256个FP32的scale factor再分成一组,每组产生一个二级量化常数。这个二级常数仍然是FP32,但数量已经大大减少了。
4.3 内存节省分析
咱们来算一笔账。假设一个70亿参数的模型,用NF4量化,块大小64:
| 项目 | 计算方式 | 内存占用 |
|---|---|---|
| 原始权重(FP16) | 7B × 2 bytes | 14 GB |
| NF4量化后权重 | 7B × 0.5 bytes | 3.5 GB |
| 第一重量化常数(FP32) | 7B / 64 × 4 bytes | 437.5 MB |
| 第二重量化后常数(FP8) | 7B / 64 / 256 × 1 byte | ~1.7 MB |
看到了吗?437.5 MB 降到了 1.7 MB,节省了超过99%的常数存储空间。这就是双重量化的威力。
整体来看,双重量化让QLoRA在显存效率上达到了极致。原本需要14GB的模型,现在只需要大约3.5GB(权重)+ 少量常数 + LoRA参数。这就是为什么你可以在单张24GB显卡上微调70B模型的原因。
4.4 双重量化的知识结构图
下面我用一张图来总结双重量化的核心逻辑:
从这张图可以看得很清楚:双重量化本质上就是对量化常数再做一次量化。左边是常规的NF4量化路径,右边是额外的量化常数压缩路径。两条路径最终合并,得到极致压缩的模型表示。
好了,关于双重量化的动机、原理和内存节省分析,我就讲到这里。核心就一句话:量化常数也是内存,能省则省。