1. LoRA#
1.1 前言#
Adapters:
- 出现时间:Adapters方法最早出现,其初步形式可以追溯到2016年左右。
- 方法描述:Adapters通过在模型的每一层之间添加较小的、可训练的网络(称为adapter模块),而不是微调整个模型。这样可以显著减少训练时需要调整的参数数量。
- 应用:Adapters适用于那些希望在保持预训练模型结构不变的同时,对模型进行特定任务调整的场景。
- 缺陷:添加适配器层(adapters)的策略虽然参数少,但会在推理阶段引入延迟,特别是在大规模和对延迟敏感的生产环境中。
Prefix Tuning:
- 出现时间:Prefix Tuning是在Adapters之后出现的,大约是在2020年左右。
- 方法描述:在Prefix Tuning中,固定了大部分预训练模型的权重,仅在模型的输入部分添加一系列可训练的前缀向量(prefixes)。这些向量会和输入数据一起被送入模型,从而影响模型的行为。
- 应用:Prefix Tuning适用于需要对模型进行轻量级微调的场景,特别是当模型非常大,而可用于训练的资源有限时。
- 缺陷:直接优化输入层激活的方法(prefix)在训练参数方面存在非单调变化,且通过预留部分序列长度用于适应,降低了处理下游任务的序列长度。
Lora (Low-Rank Adaptation):
出现时间:Lora是最近几年(大约2021年)出现的方法。
方法描述:Lora通过向预训练模型的每一层的权重矩阵中添加低秩矩阵来实现微调。这种方法旨在通过改变权重的一个小子集来调整模型的行为,而不是修改整个权重矩阵。如下图所示:
应用:Lora适用于那些需要在不显著增加计算负担的情况下微调大型模型的场景。
小结:
- 三种方法都是为了解决大型预训练模型微调时存在的参数数量庞大、计算成本高等问题。
- Adapters通过添加额外的小型模块进行调整,Prefix Tuning通过修改输入的前缀向量来影响模型,而Lora通过对模型权重的低秩调整来实现微调。
1.2 LoRA的特点和原理#
LoRA方法冻结预训练模型的权重,并在Transformer架构的每一层注入可训练的秩分解矩阵,大大减少了下游任务的可训练参数数量。LoRA可以通过更少的训练参数和更高的训练吞吐量达到与全面微调相当或更好的效果,并且与适配器不同,没有额外的推理延迟。具体过程如下图所示:
这种方法通过在预训练权重矩阵上加入低秩矩阵B和A,从而实现对模型的微调,同时保持预训练权重冻结。这种设计在训练时只需优化低秩矩阵,大大减少了可训练参数的数量。具体计算公式如下图所示:

这个公式说明:训练 来替代训练 。这里 B 的维度为 ,,而 。
基于此,LoRA 可以成功地外推至全参微调:
- Adapters 和 prefix 都无法维持原有架构;
- 而 LoRA 只是增加了 ,可以维持原有架构。
- 论文中的说明:LoRA(低秩适应)走得更远,不要求在适应期间累积梯度更新到权重矩阵必须是满秩的。这意味着,当我们将LoRA应用于所有权重矩阵并训练所有偏差时,我们大致上通过将LoRA的秩r设置为预训练权重矩阵的秩,恢复了完全微调的表达能力。换句话说,随着我们增加可训练参数的数量,训练LoRA大致上收敛于训练原始模型,而基于适配器的方法收敛于一个多层感知机(MLP),基于前缀的方法则收敛于一个不能处理长输入序列的模型。
LoRA的特点如下:
- Base LLMs + 不同的 LoRA 支持不同的下游任务。就是定向预调大模型的意思
- 微调过程中需要相对很少的显存
- 数学原理:原参数矩阵,加上一个小的、简单的低秩矩阵(B*A升维之后,+W),来生成新的参数矩阵。
- 在 Transformer 的 multi-head attention 中使用 LoRA
- 微调后部署:Base + LoRA,不会增加推理时间
- 可以结合其它微调方法如QLoRA,降低精度,进一步减少计算量
LoRA的优势:
- 一个预训练模型可以共享并用于为不同任务构建许多小型LoRA模块。我们可以冻结共享模型,并通过替换图1中的矩阵A和B来高效地切换任务,这显著减少了存储需求和任务切换开销。
- LoRA使训练更加高效,并且当使用自适应优化器时,硬件进入门槛降低了高达3倍,因为我们不需要计算大多数参数的梯度或维护优化器状态。相反,我们只优化注入的、更小的低秩矩阵。
- 我们简单的线性设计允许我们在部署时将可训练矩阵与冻结权重合并,与全面微调模型相比,由于构造原因,不引入推理延迟。
- LoRA与许多先前的方法正交,可以与其中许多方法(如前缀调优 prefix-tuning)结合使用。
2. 模型量化#
2.1 什么是模型量化#
模型量化是指将神经网络中参数(权重)和激活值(activation)的数值精度从高位(如32位浮点数,FP32)降低为低位(如16位、8位、4位甚至2位整数),以减少存储开销、加速推理并降低能耗的技术。
其核心思想是:
用更少的比特来表示相近的数值,同时尽量保持模型性能不受显著影响。
量化过程一般分为两个阶段:
其中:
- :原始浮点值
- :量化后的整数
- :scale(缩放因子)
- :zero-point(零点偏移)
- :量化范围(由比特数决定)
可以对模型参数(weight)、激活值(activation)或者梯度(gradient)做量化。通常而言,模型的参数分布较为稳定,因此对参数 weight 做量化较为容易。
然而,模型的激活值往往存在异常值,直接对其做量化,会降低有效的量化格点数,导致精度损失严重,因此,激活值的量化需要更复杂的处理方法(如 SmoothQuant)。
模型量化可以看成模型的压缩/解压过程,也可以理解成模型加密/解密的过程。既然量化算法相当于一个压缩算法,自然我们需要关注:
- 压缩比,也就是说,一种量化方法能减少多少内存/显存占用?
- 压缩/解压缩的速度,这影响量化模型推理的速度,也是我们需要重点优化之处。
对于第一个关注点,当我们确定了量化精度(例如 int4),确定了量化方法,以及需要量化模型的哪些 layer,其内存和显存占用就基本确定下来了。大部分情况下,我们都只去量化 nn.Linear 层,目前几乎所有量化策略都是这么做的,而且量化模型的显存占用较少,因此我们几乎不会去考虑怎么进一步减少量化模型的体积。
对于第二个关注点,我们着重于模型 forward、backward 计算过程的解压缩速度。由于这些计算基本都在 GPU 上进行,所以我们就需要去优化 GPU 的 op 了。
2.2 数值编码方式#
FP16
FP16 是一种 浮点格式(不是整数量化),采用 IEEE 754 半精度标准:
| 位数 | 字段 | 含义 |
|---|---|---|
| 1 bit | 符号位(sign) | |
| 5 bits | 阶码位(exponent) | |
| 10 bits | 尾数位(mantissa) |
量化公式:
反量化公式(即从FP16还原FP32):
FP16 实际上是低位浮点存储,不需要显式 scale/z,硬件自动完成。
FP8
FP8 同样是浮点格式,主要有两种变体:
- E4M3:4位指数 + 3位尾数
- E5M2:5位指数 + 2位尾数
常见于 NVIDIA Hopper / TransformerEngine 中。
量化公式(以E4M3为例):
其中 ,。
反量化公式:
FP4
FP4 是一种实验性低比特浮点格式,常用于研究级或自定义硬件。
| 位数 | 含义 |
|---|---|
| 1 bit | sign |
| 2 bits | exponent |
| 1 bit | mantissa |
量化公式:
反量化公式:
FP4 的动态范围极小,因此常用于辅助存储或特定层。
INT8
量化公式:
反量化公式:
其中:
INT4
量化公式:
反量化公式:
其中:
2.3 常用的模型量化方法#
2.3.1 训练后量化 PTQ#
训练后量化(Post-Training Quantization)在模型训练完成后进行量化;速度快、无额外训练代价,但精度下降明显。
1. 训练后动态量化
将模型的权重提前量化为INT8,但激活值在推理过程中动态地量化为INT8。这种方法最简单,通常用于LSTM等模型。

代码如下:
# -*- coding: utf-8 -*-
import torch
class MyModel(torch.nn.Module):
def __init__(self):
super().__init__()
self.linear1 = torch.nn.Linear(3, 3, bias=False)
self.relu = torch.nn.ReLU()
self.linear2 = torch.nn.Linear(3, 1, bias=False)
def forward(self, inputs):
outputs = self.linear1(inputs)
outputs = self.relu(outputs)
outputs = self.linear2(outputs)
return outputs
# 构造训练数据
weights = torch.tensor([[1.1], [2.2], [3.3]])
torch.manual_seed(123)
training_features = torch.randn(12000, 3)
training_labels = training_features @ weights
# 构造测试数据
torch.manual_seed(123)
test_feature = torch.randn(1000, 3)
test_labels = test_feature @ weights
# 初始化模型与优化器
model = MyModel()
optimizer = torch.optim.Adam(model.parameters(), lr=0.1)
# 训练过程
for i in range(100):
preds = model(training_features)
loss = torch.nn.functional.mse_loss(preds, training_labels)
loss.backward()
optimizer.step()
optimizer.zero_grad()
# 测试 float32 模型
model.eval()
with torch.no_grad():
preds = model(test_feature)
mse = torch.nn.functional.mse_loss(preds, test_labels)
print(f"float32 model testing loss: {mse.item():.3f}")
# 动态量化(int8)
model_int8 = torch.ao.quantization.quantize_dynamic(model, {torch.nn.Linear}, dtype=torch.qint8)
with torch.no_grad():
preds = model_int8(test_feature)
mse = torch.nn.functional.mse_loss(preds, test_labels)
print(f"int8 model testing loss: {mse.item():.3f}")
# 查看参数
print("float32 model linear1 parameter:\n", model.linear1.weight)
print("int8 model linear1 parameter (int representation):\n", torch.int_repr(model_int8.linear1.weight()))
print("int8 model linear1 parameter (dequantized):\n", model_int8.linear1.weight())训练后动态量化的问题:
- 每一次推理每一层都要对输入统计量化参数,耗时。
- 每一层计算完都转换为fp32,存入显存,占用显存带宽
2. 训练后静态量化
在模型推理之前,通过一个**“校准”过程(输入一批有代表性的数据)来统计激活值的分布范围,确定一个固定的量化参数(缩放因子和零点)**。然后将权重和激活值都静态地量化为INT8。这是最常用、性能最好的后量化方法。
针对训练后动态量化的问题
- 用有代表性的输入数据跑一遍整个网络,通过统计得到每层大概的量化参数。
- 这一层的输出是下一层的输入。下一层还要量化,不如在这一层直接量化在传递给下一层。
训练后静态量化过程如下图所示:

代码如下:
import torch
class MyModel(torch.nn.Module):
def __init__(self):
super().__init__()
self.quant = torch.ao.quantization.QuantStub()
self.linear1 = torch.nn.Linear(3, 3, bias=False)
self.relu = torch.nn.ReLU()
self.linear2 = torch.nn.Linear(3, 1, bias=False)
self.dequant = torch.ao.quantization.DeQuantStub()
def forward(self, inputs):
q_inputs = self.quant(inputs)
outputs = self.linear1(q_inputs)
outputs = self.relu(outputs)
outputs = self.linear2(outputs)
f_outputs = self.dequant(outputs)
return f_outputs
# 构造训练数据
weights = torch.tensor([[1.1], [2.2], [3.3]])
torch.manual_seed(123)
training_features = torch.randn(12000, 3)
training_labels = training_features @ weights
# 构造测试数据
torch.manual_seed(123)
test_feature = torch.randn(1000, 3)
test_labels = test_feature @ weights
# 初始化模型与优化器
model = MyModel()
optimizer = torch.optim.Adam(model.parameters(), lr=0.1)
# 训练
for i in range(100):
preds = model(training_features)
loss = torch.nn.functional.mse_loss(preds, training_labels)
loss.backward()
optimizer.step()
optimizer.zero_grad()
# 测试 float32 模型
model.eval()
with torch.no_grad():
preds = model(test_feature)
mse = torch.nn.functional.mse_loss(preds, test_labels)
print(f"float32 model testing loss: {mse.item():.3f}")
# 设置量化配置(必须执行这步)
model.qconfig = torch.ao.quantization.get_default_qconfig("x86")
# 准备量化(插入 observer)
model_prepared = torch.ao.quantization.prepare(model)
# 用代表性数据校准 observer
model_prepared(test_feature)
# 转换成量化模型
model_int8 = torch.ao.quantization.convert(model_prepared)
# 测试量化模型
with torch.no_grad():
preds = model_int8(test_feature)
mse = torch.nn.functional.mse_loss(preds, test_labels)
print(f"int8 model testing loss: {mse.item():.3f}")
# 查看量化参数
print("float32 model linear1 weight:\n", model.linear1.weight)
print("int8 model linear1 weight (int representation):\n", torch.int_repr(model_int8.linear1.weight()))
print("int8 model linear1 weight (dequantized):\n", model_int8.linear1.weight())2.3.2 量化感知训练#
对训练好的模型,无论怎么量化,总是会有误差。量化感知训练(Quantization-Aware Training)是在网络训练过程中,模拟量化,让模型在训练过程中就能调整参数,让它更适合量化,提高量化后模型的精度。在 QAT 中,前向传播通过“假量化”模块模拟 INT8 运算,使模型在训练中感知量化误差;反向传播中,梯度通过 Straight-Through Estimator 回传到 FP32 权重,使权重在高精度空间中更新。最终在推理阶段才真正将权重与激活量化为 INT8。具体过程如下图所示:

代码如下:
# -*- coding: utf-8 -*-
import torch
class MyModel(torch.nn.Module):
def __init__(self):
super().__init__()
self.quant = torch.ao.quantization.QuantStub()
self.linear1 = torch.nn.Linear(3, 3, bias=False)
self.relu = torch.nn.ReLU()
self.linear2 = torch.nn.Linear(3, 1, bias=False)
self.dequant = torch.ao.quantization.DeQuantStub()
def forward(self, inputs):
q_inputs = self.quant(inputs)
outputs = self.linear1(q_inputs)
outputs = self.relu(outputs)
outputs = self.linear2(outputs)
f_outputs = self.dequant(outputs)
return f_outputs
# 构造数据
weights = torch.tensor([[1.1], [2.2], [3.3]])
torch.manual_seed(123)
training_features = torch.randn(12000, 3)
training_labels = training_features @ weights
torch.manual_seed(123)
test_feature = torch.randn(1000, 3)
test_labels = test_feature @ weights
model = MyModel()
# 设置量化配置(必须执行这步)
model.qconfig = torch.ao.quantization.get_default_qconfig("x86")
# 准备量化(插入 observer)
model_prepared = torch.ao.quantization.prepare_qat(model)
# 训练模型
optimizer = torch.optim.Adam(model.parameters(), lr=0.1)
for i in range(100):
preds = model_prepared(training_features)
loss = torch.nn.functional.mse_loss(preds, training_labels)
loss.backward()
optimizer.step()
optimizer.zero_grad()
# 测试浮点模型
model.eval()
with torch.no_grad():
preds = model_prepared(test_feature)
mse = torch.nn.functional.mse_loss(preds, test_labels)
print(f"float32 model testing loss: {mse.item():.3f}")
# 转换成量化模型
model_int8 = torch.ao.quantization.convert(model_prepared)
# 测试量化模型
with torch.no_grad():
preds = model_int8(test_feature)
mse = torch.nn.functional.mse_loss(preds, test_labels)
print(f"float32 model testing loss: {mse.item():.3f}")
# 查看量化参数
print("float32 model linear1 parameter:\n", model_prepared.linear1.weight)
print("int8 model linear1 parameter (int representation):\n", torch.int_repr(model_int8.linear1.weight()))
print("int8 model linear1 parameter (dequantized):\n", model_int8.linear1.weight())2.3.3 对称量化#
对称量化假设数据分布关于 0 近似对称(如权重分布),因此零点(zero-point)固定为 0。量化范围也关于 0 对称。
量化公式:
其中:
- :浮点数
- :量化后整数
- :缩放因子(scale)
- :量化区间上界(如 int8 → 127)
反量化公式:
缩放因子计算:
通常取对称范围:
示例(以 int8 为例)
若浮点权重范围是 ,则
某个浮点数 的量化结果:
反量化结果:
2.3.4 非对称量化#
非对称量化考虑到数据可能不以 0 为中心(例如 ReLU 激活输出通常为非负),因此引入**零点(Zero-Point, z)**以对齐量化整数的零值。
量化公式:
其中:
- :scale(比例因子)
- :zero-point(偏移量)
- :量化整数范围(如 0~255 对应 uint8)
反量化公式:
缩放因子和零点计算:
示例(以 uint8 为例)
假设激活范围为 ,
计算:
量化:
反量化:
2.3 常用的量化方法在LLM中不适用#
传统的量化方法(如对称/非对称定点量化,典型是 INT8 量化)在 LLM(如 GPT、LLaMA、BERT 大模型)上会遇到明显问题,主要原因如下:
- LLM 对精度极为敏感
- LLM 的参数规模动辄数百亿甚至上万亿,网络结构极深。
- 一点量化误差在层间传播会被放大,造成输出语义漂移。
- 特别是 注意力层(Attention)和归一化层(LayerNorm) 对精度变化极度敏感。 传统的 8-bit 量化方法往往会导致严重的性能下降(PPL上升、生成文本不连贯等)。
- 权重和激活分布高度不均匀
- CNN 的激活值通常集中在较小范围内,而 LLM 的激活值和权重分布呈 重尾(heavy-tailed)分布。
- 少数异常值(outlier)占据较大动态范围,导致普通线性量化(如 min-max)无法兼顾大部分数据的精度。
- 不同层、不同通道的分布差异大
- LLM 的不同 Transformer 层、甚至不同矩阵的列/行统计特性差异巨大。
- 传统统一比例的量化(per-tensor)会极大破坏部分层的数值关系。 → 因此需要更细粒度的量化(per-channel、per-group)甚至自适应量化策略。
2.4 用于量化LLM的方法#
近年来,研究者提出了多种专为 LLM 优化的量化技术,主要分为以下几类:
| 类别 | 方法代表 | 特点 |
|---|---|---|
| 1. Post-Training Quantization (PTQ) | GPTQ, AWQ, RPTQ, OmniQuant | 无需重新训练,通过统计或优化减少量化误差;部署简单,效率高。 |
| 2. Quantization-Aware Training (QAT) | QLoRA, SmoothQuant+再训练GPTQ, AWQ, RPTQ, OmniQuant | 在训练或微调过程中引入量化噪声模拟,提高模型对低精度的适应性。无需重新训练,通过统计或优化减少量化误差;部署简单,效率高。 |
| 3. 混合精度量化 (Hybrid Precision) | LLM.int8(), FP8, INT4+FP16 混合 | 对敏感层使用高精度(如 FP16),对鲁棒层使用低比特(如 INT4),平衡性能与精度。 |
下面介绍目前主流的方法:
GPTQ(Gradient Post-training Quantization)
- 原理:通过最小化量化误差对下游任务影响的梯度近似优化。
- 逐层量化,每层用二阶信息近似量化误差最小化。
- 支持 INT4/INT3,精度损失极小。
- 目前是 LLM 最常用的离线量化方案之一(如 LLaMA、OPT、Vicuna 等均有 GPTQ 版本)。
AWQ(Activation-aware Weight Quantization)
- 由 MIT 提出,主要解决激活值中 outlier 过大的问题。
- 方法:识别关键通道(outlier channel),保留其高精度表示,对其他部分低比特量化。
- 效果:在几乎无性能损失的前提下实现 INT4 推理。
- 支持大部分 Transformer 结构,实际推理速度快于 GPTQ。
SmoothQuant
- 由微软提出,用于 QAT 或半PTQ。
- 思路:通过平滑(smooth)激活与权重的比例,使得量化区间更均衡。
- 通常配合 INT8 推理,尤其在 CPU 或 GPU 部署上效果好。
- 可用于模型蒸馏或再训练阶段。
QLoRA(Quantized Low-Rank Adapter)
- 来自 HuggingFace,用于 量化后的微调(LoRA)。
- 核心思想:将预训练模型权重量化为 INT4,但微调时仅训练低秩适配器(LoRA 层)。
- 优点:显著减少显存占用,可在单张 24GB GPU 上微调 65B 模型。
- 缺点:只适合微调,不直接用于推理优化。
LLM.int8(8-bit Matrix Multiplication for LLMs)
由 Tim Dettmers 等人提出,专为大语言模型(LLM)设计的 8-bit 混合精度量化方法。
**核心思想:**在 Transformer 中,不同通道的激活分布差异较大,存在少量异常激活值(outliers),若直接量化会造成精度显著下降。LLM.int8 通过检测这些异常通道,仅将分布稳定的部分量化为 INT8,而将异常通道保留为 FP16 精度,从而实现 INT8 与 FP16 的混合计算,既降低显存占用又保持模型性能。
优点:
- 无需再训练(Post-Training Quantization)。
- 显存占用减少约 50%,几乎无精度损失。
- 已在 Hugging Face 的
BitsAndBytes库中广泛实现,部署方便。
缺点:
- 计算中仍包含 FP16 通道,速度提升有限。
- 依赖支持 INT8 加速的 GPU 硬件(如 A100、H100)。
- 主要用于推理阶段,不适用于进一步微调。
3.QLoRA#
3.1 背景与动机#
大型语言模型(LLMs)的微调通常需要大量内存,这限制了在资源受限环境中的应用。**如常规16位微调65B参数的LLaMA模型需要超过780GB的GPU内存(一张A100 GPU 的显存是64GB)。**现有方法如LoRA虽然减少参数,但仍有内存瓶颈。最近的量化方法能进一步减少LLMs的内存占用,但这些技术仅适用于推理,在训练期间会失效。要 在量化模型上做训练 / 微调 ,尤其要保留训练表现,是极具挑战的。
论文提出了QLoRA,一种高效的微调方法,它减少了内存的使用,足以在单个48GB GPU上微调65B参数的模型,同时保留了完整的16位微调任务性能。QLoRA通过一个冻结的、4位量化的预训练语言模型反向传播梯度到低秩适配器(LoRA)。这标志着LLM微调可访问性的重大转变:现在最大的公开可用模型可以在单个GPU上进行微调。
QLoRA引入了许多创新来节省内存而不牺牲性能:(a) 4位NormalFloat(NF4),一种对于正态分布权重理论上最优的新数据类型;(b) 双重量化,通过**量化量化常数(缩放因子)**来减少平均内存占用;(c) 分页优化器来管理内存峰值。
与QLoRA相关的技术如下:
- 块状 k-bit 量化(Block-wise k-bit Quantization):这是一种数据压缩技术,通过减少表示数据的比特数(如从32位浮点数到8位整数)来减少模型大小。为了有效使用低比特数据类型的整个范围,通常会通过最大绝对值归一化输入数据。然而,这种方法存在一个问题:如果输入数据中有极大或极小的异常值,一些量化区间将不会被充分利用。为了解决这个问题,可以将输入数据划分为块,每个块独立进行量化。如下图所示:

- LoRA(Low-Rank Adaptation) :LoRA 是一种常用的参数高效微调技术:不直接修改基模型权重,而是在每个线性层插入两个小矩阵(低秩矩阵)去拟合增量,并只训练这些适配器(冻结原权重)。这样可以大幅减少要训练的参数数量。如下图所示:

其中 是一个 标量缩放参数
是为了补偿量化引入的尺度差异;
它是一个可学习的标量;
能够在微调时自动调整 LoRA 的影响强度;
保证在低精度量化下模型依然稳定、有效。
- 参数高效微调的内存需求(Memory Requirement of Parameter-Efficient Finetuning):这部分讨论了在训练期间,使用低秩适配器(LoRA)时的内存需求。
- 每个Transformer module,都加上一个 LoRA module;
- 在不同地方加上 LoRA 模块,会带来不同效果;
- LoRA的内存占用很小,因此可以使用更多的适配器来提高性能,而不会显著增加总体内存使用。然而,训练时最大的内存开销来自激活梯度,而不是LoRA参数本身。
这些概念是实现QLoRA方法的基础,通过它们,论文展示了如何在保持性能的同时显著减少大型语言模型微调过程中的内存需求。
3.2 核心要点#
QLoRA 在逻辑上由以下步骤组成:
1. 量化预训练模型:将基模型权重量化为 4-bit 表示(采用 NF4 类型 + 双重量化)
- 加入 LoRA Adapter:在每层的线性/投影矩阵上插入 LoRA(低秩)参数,并冻结原始基模型权重
- 前向/反向计算时 dequantize → 16-bit 计算:量化存储 + 在需要时恢复高精度
- 把梯度仅传回给 LoRA 参数(通过 dequantize 路径传播)
- 使用分页优化器 (paged optimizer) 管理内存峰值(尤其 gradient checkpointing 情况下)
作者强调:为了在量化微调中恢复与 16-bit 相当的性能,必须在所有适当的线性层都加 LoRA,而不能只选部分层。
3.3 QLoRA微调#
通过提出的两种技术实现了高保真度的4位微调——4位NormalFloat(NF4)量化和双重量化。此外,论文引入了分页优化器,以防止在梯度检查点期间内存峰值导致内存不足错误,这种错误传统上使得在单台机器上对大型模型进行微调变得困难。
QLoRA有一种低精度的存储数据类型,通常是4位,和一个通常为BFloat16的计算数据类型。在实践中,这意味着每当使用QLORA权重张量时,我们将其解量化为BFloat16,然后以16位进行矩阵乘法。
值得注意的是:NF4 并不是一种硬件支持的数据类型(不像 INT4 / FP16 那样),而是一种“非线性量化映射方式”。它只是把符合正态分布的权重映射到 16 个非均匀分布的浮点查表值上,以减少量化误差。因此,在推理计算(矩阵乘法)时不能直接在 INT4 上计算,必须先反量化(dequantize)回浮点数BF16再参与计算。
3.3.1 NF4#
NF4是一种量化数据类型,特别适用于正态分布的数据。它通过量化过程将数据的精度降低到4位,从而减少数据存储和计算所需的内存和带宽。
NormalFloat(NF)数据类型基于分位数量化,这是一种理论上最优的数据类型,确保每个量化区间从输入张量中分配到相等数量的值。分位数量化通过估计输入张量的经验累积分布函数来工作。
分位数量化的主要限制是分位数估计过程昂贵。因此,使用快速分位数近似算法,例如SRAM分位数,来估计它们。由于这些分位数估计算法的近似性质,该数据类型对离群值的量化误差较大,而这些离群值通常是最重要的值(混合精度模型载入)。当输入张量来自一个固定到量化常数的分布时,可以避免昂贵的分位数估计和近似误差。在这种情况下,输入张量具有相同的分位数,使得精确的分位数估计在计算上是可行的。
由于预训练神经网络权重通常具有以零为中心的正态分布,标准差为 ,我们可以通过缩放 将所有权重转换为单一固定分布,使得分布正好适合我们数据类型的范围。对于我们的数据类型,我们设置任意范围[−1, 1]。因此,数据类型和神经网络权重的分位数都需要标准化到这个范围内。
对于标准偏差 在 范围内的零均值正态分布的信息理论上最优数据类型的计算如下:
- 估计理论上的 分布的 分位数,以获得正态分布的 位分位数量化数据类型;
- 将此数据类型的值标准化到 范围内;
- 通过绝对最大重缩放将输入权重张量量化并标准化到 范围内。
一旦权重范围和数据类型范围匹配,我们就可以像往常一样进行量化。步骤(3)相当于将权重张量的标准偏差重缩放以匹配 k位数 据类型的标准偏差。更正式地,我们如下估计数据类型的 个值 :
当 为标准正态分布 的分位数函数时,对称的 位量化的一个问题是这种方法不能精确表示零,而零在神经网络中很重要(例如填充、零初始化)。为了确保0的离散零点,并使用所有 位表示 位数据类型,我们通过估计两个范围的分位数 来创建一个非对称数据类型:
负部分:使用 个量化值覆盖负值范围(对于k=4,有8个值)。这些值对应概率范围[0, 0.5],分位数点从 或类似方式计算,但论文未详细说明具体概率值。
正部分:使用 个量化值覆盖正值范围(对于k=4,有9个值)。这些值对应概率范围[0.5, 1]。
合并和去重:将负部分和正部分的量化值集合合并,由于零在两者中都出现(作为边界),移除一个重复的零。最终得到 个值(对于k=4,16个值)。
我们将这种具有每个量化区间内预期值数量相等的结果数据类型称为 位NormalFloat(NFk),因为这种数据类型对于以零为中心的正态分布数据在信息论上是最优的。
由于论文中的方法需要计算 个分位数,然后取平均,这可能会在边界处出现无穷大的问题(因为正态分布的分位数在p=0或1时是无穷大)。所以在实现中通常选择一个偏移量(offset=0.9677083)来避免计算极端分位数。这实际上是一种近似,它通过选择一组关键分位数来覆盖大部分概率范围,然后通过归一化到[-1,1]来得到量化值。这种方法可能在实际应用中表现良好,且计算简单。具体代码如下:
# -*- codeing = utf-8 -*-
import torch
from scipy.stats import norm
def create_normal_map(offset=0.9677083):
# 正数部分
v1 = norm.ppf(torch.linspace(offset, 0.5, 9)[:-1].tolist()) # 正数部分
v1 = v1.tolist()
# 中间部分(0)
v2 = [0]
# 负数部分
v3 = (-norm.ppf(torch.linspace(offset, 0.5, 8)[:-1].tolist())).tolist() # 负数部分
# 合并正数、零、负数部分
v = v1 + v2 + v3
# 转为 torch.Tensor 并排序(从负到正)
values = torch.tensor(v)
values = values.sort().values
# 归一化到 [-1, 1]
values /= values.max()
return values
# 运行查看结果
nf4_values = create_normal_map()
print("NF4 查表分位点:")
print(nf4_values)计算得到分位数如下:
NF4_quant_levels = [ -1.0, -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 ]
3.3.2 双重量化#
双重量化是对量化常数进行二次量化,以进一步节省内存。尽管精确的4位量化需要较小的块大小,但它也带来了相当大的内存开销。例如,使用32位常数和64的块大小对W进行量化时,量化常数平均每个参数增加了 位。双重量化有助于减少量化常数的内存占用。
更具体地说,双重量化将第一次量化的量化常数 作为第二次量化的输入。这一步产生了量化后的量化常数 和第二级量化常数 。我们对第二次量化使用8位浮点数和256的块大小,因为观察到对于8位量化,性能没有降低。**由于 是正数,我们在量化前从c2中减去平均值,使值围绕零居中,并利用对称量化。**平均来说,对于64的块大小,这种量化方式将每个参数的内存占用从 位降低到 位,降低了0.373位。如下图所示:

3.3.3 分页优化器#
论文使用了NVIDIA的统一内存特性,在GPU偶尔内存不足时,能够自动在CPU和GPU之间进行页面到页面的传输,确保GPU处理过程中无错误。这个特性类似于CPU RAM和磁盘之间的常规内存分页。我们使用这个特性来为优化器状态分配分页内存,当GPU内存不足时,这些状态会自动被转移到CPU RAM中,并在优化器更新步骤中需要内存时再次分页回GPU内存。
3.3.4 QLoRA#
使用上述组件,论文定义了单个线性层中的QLoRA,在量化基础模型中配备单个LoRA适配器。其中使用 NF4 作为 W,使用 FP8 作为 c2。使用 64 的块大小来提高 W 的量化精度,并使用 256 的块大小来节省 c2 的内存。
Adapter 布局:在模型的每个适合线性层(例如注意力中的查询/键/值/输出投影矩阵、MLP 层的线性层等)加入 LoRA adapter,并冻结原始量化基模型的权重。
前向 / 反向流程
- 前向时:把量化的权重 dequantize → 与输入相乘 → 加上 adapter 的输出 → 构成最终输出
- 反向传播时:梯度从损失流回 adapter 参数(LoRA),而原量化权重保持不更新
- 在梯度传播过程中,梯度不会传给主量化权重,而是直接进入 LoRA 参数,这样保证量化误差不会破坏主模型。
**总结:**QLoRA有一个存储数据类型(通常是4位的NormalFloat)和一个计算数据类型(16位的BrainFloat)。我们将存储数据类型解量化为计算数据类型,以执行前向和后向传递,但我们只为使用16位BrainFloat的LoRA参数计算权重梯度。
3.4 论文后续部分#
第四章及之后的内容主要介绍了QLoRA的实验验证与性能分析。作者在多个大型语言模型(如 LLaMA-7B、13B、33B、65B)上进行了广泛实验,比较了QLoRA 与全参数微调、LoRA、Adapter、Prefix-tuning 等方法的性能与资源消耗。结果表明,QLoRA 仅需 4-bit 量化权重即可在几乎不损失性能的情况下完成高效微调,显著降低显存占用(最多节省约 75% 资源)。在多项基准(如 Alpaca、Vicuna、OpenAssistant 及多任务评测集)上,QLoRA 微调的模型达到了与全参数微调相当甚至更优的效果。此外,作者还介绍了 QLoRA 的可扩展性、稳定性分析以及开源实现细节(PEFT框架集成)。
论文还分析了训练模型的趋势。首先,结果发现数据质量比数据集大小更重要,例如,一个9k样本数据集(OASST1)在聊天机器人性能上超过了一个450k样本数据集(FLAN v2,抽样子集)即使两者都旨在支持指令遵循泛化。其次,论文展示了在Massive Multitask Language Understanding (MMLU)基准测试上的强劲表现并不意味着在Vicuna聊天机器人基准测试上同样表现强劲,反之亦然——换句话说,对于给定任务,数据集的适用性比大小更重要。
最后在 结论部分,论文总结了 QLoRA 在节省计算资源与保持模型性能方面的突出优势,并展望了未来在大规模分布式微调、混合精度优化、以及自动量化策略方面的进一步研究方向。
4. 知识蒸馏#
4.1 研究背景与动机#
在一般情况下,我们不会去区分训练和部署使用的模型,但是训练和部署之间存在着一定的不一致性:
在训练过程中,我们需要使用复杂的模型,大量的计算资源,以便从非常大、高度冗余的数据集中提取出信息。在实验中,效果最好的模型往往规模很大,甚至由多个模型集成得到。而大模型不方便部署到服务中去,常见的瓶颈如下:
- 推断速度慢
- 对部署资源要求高(内存,显存等)
随着深度学习的发展,模型结构越来越复杂、参数量庞大,计算与存储开销显著增加。这种复杂模型(如ResNet、Transformer、YOLO系列大型模型)在准确率上表现优异,但难以部署在计算资源受限的设备(如移动端、嵌入式系统)上。 因此,研究者提出了**知识蒸馏(KD)**方法,其核心思想是:
利用一个性能较高但庞大的“教师模型”(Teacher Network)指导一个较小的“学生模型”(Student Network)学习,以实现模型压缩与性能保持的平衡。
该思想最早由 Hinton 等人(2015) 在论文 “Distilling the Knowledge in a Neural Network” 中提出,被认为是知识蒸馏领域的奠基性工作。
4.2 核心思想#
知识蒸馏通过引入“软标签(Soft Labels)”传递教师网络的“暗知识(Dark Knowledge)”。
- 教师网络在训练后输出的是每个类别的概率分布;
- 学生网络通过模仿这种概率分布,不仅学习到最终分类结果(硬标签),还学习到类别间的相似关系。
这使学生模型在训练中能获得比硬标签更多的信息,从而提升泛化能力。
4.3 主要组成部分#
- 教师网络(Teacher Network)
- 通常为预训练好的高性能模型;
- 参数量大、计算量高;
- 输出用于生成软标签,作为学生学习的指导信号。
- 学生网络(Student Network)
- 结构轻量,待训练;
- 目标是学习教师网络的知识表示;
- 可与教师网络结构相同或不同。
- 蒸馏目标与损失函数
在蒸馏过程中,通常采用两种监督信号:
- 硬标签(Hard Label):来自数据集的真实标签,使用普通的交叉熵损失;
- 软标签(Soft Label):来自教师模型的输出分布,经过温度平滑处理后用于蒸馏损失计算。
综合损失函数为:
其中:
- :温度参数,用于平滑教师网络输出;
- :加权系数,控制两部分损失的平衡。
在 softmax 前将教师输出 logits 除以温度 :
越高,softmax输出的概率分布越趋于平滑,其分布的熵越大,负标签携带的信息会被相对地放大,模型训练将更加关注负标签。
温度 的特点:
- 原始的softmax函数是 时的特例, 时,概率分布比原始更“陡峭”, 时,概率分布比原始更“平缓”。
- 温度越高,softmax上各个值的分布就越平均(思考极端情况: (i) , 此时softmax的值是平均分布的;(ii) ,此时softmax的值就相当于 , 即最大的概率处的值趋近于1,而其他值趋近于0)
- 不管温度T怎么取值,Soft target都有忽略相对较小的 携带的信息的倾向
温度的高低改变的是学生网络训练过程中对负标签的关注程度: 温度较低时,对负标签的关注,尤其是那些显著低于平均值的负标签的关注较少;而温度较高时,负标签相关的值会相对增大,学生网络会相对多地关注到负标签。
实际上,负标签中包含一定的信息,尤其是那些值显著高于平均值的负标签。但由于教师网络的训练过程决定了负标签部分比较noisy,并且负标签的值越低,其信息就越不可靠。因此温度的选取需要通过实验决定,本质上就是在下面两件事之中取舍:
- 从有部分信息量的负标签中学习 –> 温度要高一些
- 防止受负标签中噪声的影响 –>温度要低一些
总的来说, 的选择和学生网络的大小有关,学生网络参数量比较小的时候,相对比较低的温度就可以了(因为参数量小的模型不能捕捉到所有知识,所以可以适当忽略掉一些负标签的信息)
4.4 蒸馏过程与损失函数#
第一步是训练教师网络;
第二步是在高温 下,蒸馏教师网络的知识到学生网络,整体流程如下图所示
训练教师网络的过程很简单,下面详细讲讲第二步:高温蒸馏的过程。高温蒸馏过程的目标函数由distill loss(对应soft target)和student loss(对应hard target)加权得到。示意图如上。
- : 教师网络的logits。
- : 学生网络的logits。
- : 教师网络的在温度 下的softmax输出在第 类上的值。
- : 学生网络的在温度 下的softmax输出在第 类上的值。
- : 在第 类上的ground truth值,, 正标签取1,负标签取0。
- : 总标签数量。
教师网络和学生网络同时输入 training set (这里可以直接复用训练教师网络用到的training set),用教师网络产生的softmax distribution (with high temperature) 来作为soft target,学生网络在相同温度 条件下的softmax输出和soft target的cross entropy就是Loss函数的第一部分
,其中 ,
学生网络在 的条件下的softmax输出和ground truth的cross entropy就是Loss函数的第二部分 。
,其中
第二部分Loss 的必要性其实很好理解:教师网路也有一定的错误率,使用ground truth可以有效降低错误被传播给学生网络的可能。打个比方,老师虽然学识远远超过学生,但是他仍然有出错的可能,而这时候如果学生在老师的教授之外,可以同时参考到标准答案,就可以有效地降低被老师偶尔的错误“带偏”的可能性。
讨论: 为什么要用 来缩放 ?
实验发现第二部分所占比重比较小的时候,能产生最好的结果,这是一个经验的结论。一个可能的原因是,由于soft target产生的gradient与hard target产生的gradient之间有与 相关的比值。
简而言之:在知识蒸馏中, soft loss(distillation loss)所带来的梯度,其大小相比hard loss会因温度 而缩小一个 的量级。因此,为了让两部分损失的梯度规模在训练中可比,我们把soft loss乘上 。
4.5 主要优点#
模型压缩:减少参数与计算量,适用于移动端与嵌入式系统。
性能保持:学生模型精度接近甚至超过教师模型。
泛化增强:通过学习教师的隐含知识,模型对不确定样本更鲁棒。
迁移能力强:可与剪枝、量化、神经网络架构搜索等方法结合。
4.6 知识蒸馏在LLM中的应用#
在大语言模型(LLM)领域,知识蒸馏主要用于将超大规模模型(如 GPT、LLaMA 等)的语言理解与生成能力迁移到小型模型中,以实现更高的推理效率和端侧部署能力。其蒸馏过程通常包括以下几个步骤:
- 教师输出生成:教师模型根据给定的 Prompt 生成高质量文本响应,作为训练数据的参考输出。
- 教师与学生对齐输入:教师和学生模型使用相同的输入序列(通常为「Prompt + 教师输出」拼接后的完整序列)。
- 概率分布蒸馏:在每个时间步(token-level),计算教师与学生预测分布之间的差异(例如使用 KL 散度)。
- 综合损失优化:综合使用软标签损失(Soft Loss)和硬标签损失(Hard Loss)进行训练。
其中, 为交叉熵损失, 为基于教师概率分布的 KL 散度损失, 为蒸馏温度系数。
生成长度不一致问题与对齐策略
由于 LLM 是自回归生成模型,不同输入或模型生成的文本长度往往不同(教师与学生生成的 token 数量可能不一致),若直接计算分布差异会导致 token 无法对齐。为解决这一问题,常用以下两种策略:
截断对齐(Truncation Alignment) 当教师与学生生成长度不一致时,取二者生成序列的最短长度 ,仅在前 个 token 上计算损失。 这种方法简单有效,但可能忽略教师输出中后续有价值的信息。
强制对齐(Teacher Forcing Alignment) 在训练时,固定输入序列为「Prompt + 教师输出」,让学生模型强制学习教师在每个位置的预测分布,而不让学生自由生成。 这种方式保证教师与学生在时间步上严格对齐,是当前主流 LLM 蒸馏(如 DistilBERT、TinyLLaMA、Alpaca 等)采用的方案。 其蒸馏损失定义为:
该方法能显著提高训练稳定性,并避免长度差异带来的对齐误差。
参考:
论文精读:LoRa: Low-Rank Adaptation of Large Language Models
大模型PEFT的LORA算法 lora_rank, lora_alpha
模型量化:量化基础 对称量化 非对称量化 极大值量化 零点量化
论文精读:QLoRA: Efficient Finetuning of Quantized LLMs