参考资料
LoRA 把微调所需的训练参数从 175B 压缩到 0.1% 以下,QLoRA 则在此基础上叠加上 4-bit 量化和分页优化器,让 65B 参数的 Llama 能在单张 48GB 显卡上完成全参数微调。显存占用从 780GB 降到 48GB,压缩比超过 16 倍。
全量微调的瓶颈
LLaMA-65B 的全量微调需要约 780GB 显存。这个数字来自三个部分的叠加:
- 模型权重:65B 参数 × 2 字节 (BF16) = 130GB
- 优化器状态(Adam):2 份动量 + 1 份二阶矩,每份 4 字节 → 65B × 12 = 780GB
- 梯度:65B × 4 字节 = 260GB
即使使用 ZeRO-3 这类分布式优化,也需要至少 10 张 A100。这使得中小团队几乎无法触碰大模型的微调。
LoRA:低秩适配
LoRA 的核心假设是:微调过程中权重的实际变化量 是低秩的[1]。与其更新全部参数,不如冻结原权重 ,只训练两个低秩矩阵的乘积:
其中 ,,秩 。通常 取 8 或 16。
以 LLaMA-65B 的 q_proj 层为例,输入维度 8192,输出维度 8192。全量更新需要 8192² ≈ 6700 万参数,而 LoRA (r=16) 只需要 8192×16×2 = 26.2 万参数,压缩比 256:1。
训练时的内存结构:
| 组件 | 全量微调 | LoRA (r=16) |
|---|---|---|
| 模型权重 | 130 GB | 130 GB |
| 可训练参数 | 0 GB | 0.2 GB |
| 梯度 | 260 GB | 0.4 GB |
| 优化器状态 | 780 GB | 0.6 GB |
| 合计 | 1170 GB | 131 GB |
LoRA 把训练内存从 1170GB 压到 131GB,单张 A100 就能跑。但模型权重本身的 130GB 仍然是个硬门槛。
量化基础:从 Int4 开始
在深入 QLoRA 之前,先理解什么是量化。量化的本质是用更少的 bit 来表示数值,从而节省存储空间。
为什么需要量化
LLaMA-65B 的每个参数默认用 16-bit 浮点数 (BF16) 存储,占用 2 字节。130GB 显存中,模型权重独占 130GB。如果能用 4-bit 表示每个参数,显存占用就能降到 32GB——单张 RTX 4090 都能装下。
线性 Int4 量化:最直观的方案
4-bit 能表示 个不同的值。最简单的思路是线性映射:把浮点数范围均匀切成 16 份。
假设我们有一组权重:
# 原始权重(BF16,每个 2 字节)
weights = [0.5, -0.3, 1.2, -0.8, 0.1, -0.4, 0.9, -0.6]
# 第 1 步:确定取值范围
w_min, w_max = min(weights), max(weights) # -0.8, 1.2
# 第 2 步:归一化到 [0, 15] 的整数范围
# 公式:q = round((x - min) / (max - min) * 15)
quantized = []
for w in weights:
normalized = (w - w_min) / (w_max - w_min) # 映射到 [0, 1]
q = int(round(normalized * 15)) # 映射到 [0, 15]
quantized.append(q)
# quantized = [8, 5, 15, 0, 6, 4, 13, 2]
# 现在每个权重只需要 4 个 bit 存储
存储时,我们保存:
- 量化常数
w_min和w_max(各 32-bit,共 8 字节) - 8 个 4-bit 量化值(共 32 bit = 4 字节)
总计 12 字节,原始需要 16 字节,压缩比 4:3。
反量化:计算时还原精度
前向传播需要浮点数计算,所以要反量化:
def dequantize(q, w_min, w_max):
"""把 4-bit 整数还原为浮点数"""
return w_min + (q / 15.0) * (w_max - w_min)
# 还原后的值
restored = [dequantize(q, w_min, w_max) for q in quantized]
# [0.52, -0.32, 1.2, -0.8, 0.08, -0.4, 0.92, -0.56]
注意还原值和原始值有细微差异(如 0.5 变成 0.52),这就是量化误差。
线性量化的局限
大模型权重服从零均值正态分布:大部分值集中在 0 附近,极端值很少。线性量化的 16 个值均匀分布,导致:
- 0 附近的数据点太多,被塞进有限的几个桶里,精度损失大
- 两端的极端值很少,但线性量化却分配了同样多的桶位,浪费存储
假设权重分布是 ,-0.8 到 0.8 之间的值占 99%,但线性量化的 16 个值均匀分布在 [-0.8, 1.2] 整个范围。0 附近(-0.1 到 0.1)的数以亿计的权重,只能映射到 2-3 个量化值上。
这就是 QLoRA 要解决的问题:用非均匀分布的量化值,把更多的精度分配给概率密度更高的区域。
QLoRA:两阶段量化
QLoRA 在 LoRA 基础上做了三件事:4-bit Normal Float (NF4) 量化、双重量化、分页优化器[2]。其中前两者的组合让 65B 模型仅需 35-40GB 显存。
4-bit Normal Float:信息论最优量化
NF4 的核心改进是非均匀量化:量化值的分布匹配权重分布的概率密度。对于服从 的权重,NF4 的 16 个值是标准正态分布的 16 个分位数:
Normal Float (NF4) 采用信息论最优的分位数量化。假设权重服从 ,NF4 的 16 个量化值是标准正态分布的 16 个分位数:
# NF4 的 16 个量化值(对称分布,只展示正半部分)
nf4_values = [
-1.0, # -∞ ~ -0.67σ 的分位数边界
-0.6961928009986877,
-0.5250730514526367,
-0.39491748809814453,
-0.28444138169288635,
-0.18477343022823334,
-0.09105003625154495,
0.0,
0.07958029955625534,
0.16093020141124725,
0.24611230194568634,
0.33791524171829224,
0.44070982933044434,
0.5626170039176941,
0.7229568362236023,
1.0,
]
每个 4-bit 值索引到这 16 个预定义浮点数之一。存储时只存索引 (0-15),计算时再查表还原。
分块量化:用一个例子理解
QLoRA 不会一次性量化整个权重矩阵,而是分块处理(默认块大小 64)。这样做有两个原因:
- 局部统计特性:不同层的权重分布方差不同,分块量化能更好地适应
- 量化常数:每块需要一个缩放因子
absmax来还原原始数值范围
假设我们有一个简化版的权重列表(只取 8 个值演示):
# 原始权重(BF16 存储,每个 2 字节)
weights = [0.5, -0.3, 1.2, -0.8, 0.1, -0.4, 0.9, -0.6]
# 第 1 步:找到绝对值最大值
absmax = max(abs(w) for w in weights) # 1.2
# 第 2 步:归一化到 [-1, 1]
normalized = [w / absmax for w in weights]
# [0.4167, -0.25, 1.0, -0.6667, 0.0833, -0.3333, 0.75, -0.5]
# 第 3 步:量化到 4-bit(找最近的 NF4 值)
def quantize_to_nf4(x):
"""找到最近的 NF4 量化值并返回索引"""
closest_idx = min(range(16), key=lambda i: abs(nf4_values[i] - x))
return closest_idx
quantized_indices = [quantize_to_nf4(x) for x in normalized]
# [9, 6, 15, 3, 7, 5, 12, 4] <- 每个只需 4 个 bit 存储
# 存储结构:
# - 量化常数 absmax: 32-bit float (4 字节)
# - 8 个 4-bit 索引: 32 bit = 4 字节
# 总计 8 字节,原始需要 16 字节,压缩比 2:1
双重量化:压缩量化常数
上面的例子中,每 64 个权重就需要一个 32-bit 的 absmax。对于 65B 模型,量化常数本身就要占用约 500MB。
QLoRA 的第二层量化把这些 absmax 也量化了:
# 第一层量化:每块一个 absmax
block_absmax_values = [1.2, 0.8, 1.5, 0.6, ...] # 大量 32-bit 浮点数
# 第 2.1 步:把 absmax 也分块(每 256 个 absmax 一块)
# 找出这块 absmax 的最大值作为二级量化常数
second_block = block_absmax_values[:256]
second_absmax = max(second_block) # 比如 1.8
# 第 2.2 步:把这 256 个 absmax 量化为 8-bit
# 8-bit 可以表示 0-255,映射到 [0, second_absmax]
quantized_absmax = [
int((x / second_absmax) * 255) for x in second_block
]
# 每个 absmax 从 32-bit 降到 8-bit,额外节省 4x
双重量化后,量化常数的存储开销从约 500MB 降到约 125MB。
反量化计算
前向传播时需要还原 FP16/BF16 精度:
def dequantize(quantized_idx, absmax, second_absmax, quantized_absmax_idx):
"""从 4-bit 索引还原到浮点数"""
# 第 1 步:还原一级 absmax(从 8-bit)
block_absmax = (quantized_absmax_idx / 255.0) * second_absmax
# 第 2 步:还原权重值
nf4_value = nf4_values[quantized_idx]
weight = nf4_value * block_absmax
return weight
这个反量化在前向传播时动态进行,权重本身始终保持 4-bit 存储。
分页优化器:平滑显存峰值
Adam 优化器在更新步骤中需要同时保留:当前梯度、一阶动量、二阶动量。这三者在某些时刻会同时存在于显存中,形成峰值。
Paged Optimizers 利用 NVIDIA 的统一内存机制,把暂时不用的优化器状态分页换出到 CPU 内存,需要时再异步加载。这带来了约 1-2% 的训练速度损失,但消除了显存峰值,让训练能在更小的 GPU 上稳定运行。
完整显存对比
| 配置 | 全量微调 | LoRA | QLoRA |
|---|---|---|---|
| 模型权重 | 130 GB (BF16) | 130 GB | 32 GB (4-bit) |
| 可训练参数 | - | 0.2 GB | 0.2 GB |
| 梯度 | 260 GB | 0.4 GB | 0.4 GB |
| 优化器状态 | 780 GB | 0.6 GB | 0.6 GB |
| 激活值 (4K 长度) | ~50 GB | ~50 GB | ~50 GB |
| 合计 (65B 模型) | ~1220 GB | ~181 GB | ~48 GB |
| 所需 GPU | 16× A100 | 3× A100 | 1× A6000 |
QLoRA 让 65B 模型的微调门槛从 16 张 A100 降到单张消费级显卡。
训练代码
QLoRA 的典型配置(使用 transformers + peft):
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
# 4-bit 量化配置
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True, # 启用双重量化
bnb_4bit_quant_type="nf4", # Normal Float 4
)
# 加载量化模型
model = AutoModelForCausalModel.from_pretrained(
"meta-llama/Llama-2-65b-hf",
quantization_config=bnb_config,
device_map="auto",
)
# 为量化模型准备训练
model = prepare_model_for_kbit_training(model)
# LoRA 配置
lora_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "v_proj", "k_proj", "o_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora_config)
# 启用分页优化器
training_args = TrainingArguments(
per_device_train_batch_size=1,
gradient_accumulation_steps=4,
optim="paged_adamw_8bit",
# ...
)qlora_train.py
关键配置:bnb_4bit_use_double_quant=True 启用双重量化,optim="paged_adamw_8bit" 启用分页优化器。
局限与权衡
QLoRA 的 4-bit 量化会引入约 0.1-0.5% 的精度损失(取决于任务)。在大多数下游微调任务中,这种损失可以忽略不计。但如果你在极度敏感的任务(如数学推理)上追求 SOTA,可能需要考虑:
- 混合精度训练:某些层保持 FP16(如
lm_head、embed_tokens) - 更大的 LoRA 秩:r=64 或 128,牺牲参数效率换取表达能力
QLoRA 的真正价值在于降低门槛。它让个人研究者和中小团队能够在消费级硬件上实验 65B+ 模型,而不必依赖昂贵的云计算集群。