系列 · 迁移学习 · 第 9 篇

迁移学习(九):参数高效微调

从低秩适配的数学原理出发,系统讲解 LoRA、Adapter、Prefix-Tuning、Prompt-Tuning、BitFit、QLoRA 等参数高效微调方法,附 LoRA 从零实现与方法选型指南。

单张 GPU 如何微调 1750 亿参数的模型?只需更新 0.1% 的参数即可——参数高效微调(Parameter-Efficient Fine-Tuning,PEFT)使这成为可能。在大多数基准测试中,其效果几乎与全量微调持平。本文将从数学原理出发,推导 LoRA、Adapter、Prefix-Tuning、Prompt-Tuning、BitFit 和 QLoRA 的设计逻辑,并用一张图帮助你选择合适的方法。

迁移学习(九):参数高效微调 — 章节概览图


你将学到什么#

  • 为什么低秩假设在权重更新中成立
  • LoRA 的推导过程、初始化方法、缩放技巧及权重合并策略
  • Adapter 的瓶颈结构设计和插入位置选择
  • Prefix-Tuning、Prompt-Tuning 和 P-Tuning v2 的差异
  • QLoRA 如何通过 4-bit 量化在单张 GPU 上运行 65B 模型
  • 基于 GLUE 数据的方法对比与选型指南

前置知识#

  • Transformer 架构(Attention、FFN、残差连接与 LayerNorm)
  • 矩阵分解基础(秩、SVD)
  • 迁移学习基础(第 1 至第 6 篇)

全量微调的难题#

$$\boldsymbol{\theta}^* = \arg\min_{\boldsymbol{\theta}} \mathcal{L}(\boldsymbol{\theta})$$

对于 GPT-3(175B 参数),这意味着大约 700 GB 的 FP32 权重,再加上梯度和优化器状态——每个任务都需要一份完整副本。即使模型能装下,每个任务的存储和服务成本也极其高昂:100 个客户就意味着 100 份 700 GB 的 checkpoint。

$$\boldsymbol{\theta}^* = \boldsymbol{\theta}_0 + \Delta\boldsymbol{\theta}, \qquad |\Delta\boldsymbol{\theta}| \ll |\boldsymbol{\theta}_0|.$$

冻结 $\boldsymbol{\theta}_0$ ,仅训练并分发一个极小的任务特定增量 $\Delta\boldsymbol{\theta}$ 。预训练权重成为共享主干,而适配部分则变成轻量级的增量,可以按需存储、版本管理和动态路由。

方法可训练参数占比存储节省
全量微调100%0%
LoRA0.1% - 1%99% - 99.9%
Adapter0.5% - 2%98% - 99.5%
Prefix-Tuning~0.1%~99.9%
Prompt-Tuning<0.01%>99.99%
BitFit~0.1%~99.9%

LoRA:低秩适配#

LoRA 在 SST-2 / MNLI / CoLA 上的 rank 敏感度与参数成本 Pareto 曲线。

迁移学习(九):参数高效微调 — 章节小结图

LoRA 低秩分解:W = W_0 + (alpha/r) * B * A

核心思想一句话概括#

$$ \mathbf{W}' = \mathbf{W}_0 + \frac{\alpha}{r}\, \mathbf{B}\mathbf{A}, \qquad \mathbf{A} \in \mathbb{R}^{r \times d_{\text{in}}},\; \mathbf{B} \in \mathbb{R}^{d_{\text{out}} \times r},\; r \ll \min(d_{\text{in}}, d_{\text{out}}). $$

冻结的 $\mathbf{W}_0$ 包含 $d_{\text{in}} d_{\text{out}}$ 个参数,而可训练部分只有 $r(d_{\text{in}} + d_{\text{out}})$ 个。例如当 $d=4096, r=8$ 时,新增参数仅占原矩阵的 0.39%

为什么低秩假设成立?#

有两个实证结果支持该假设:

  1. 本征维度(Aghajanyan et al., 2020):微调轨迹实际上位于权重空间的一个低维子空间中。仅几百个参数就足以在许多任务上匹配全量微调的效果。
  2. 预训练矩阵的奇异值谱:注意力投影矩阵的奇异值快速衰减,少数方向承载了大部分能量。而针对新任务所需的更新甚至更加集中。

换句话说,预训练模型已经掌握了几乎所有知识;适配只需旋转一小片参数即可。

初始化、缩放与前向计算#

LoRA 在实践中依赖几个关键细节:

  • 初始化$\mathbf{A} \sim \mathcal{N}(0, \sigma^2)$ (Kaiming 风格方差),$\mathbf{B} = \mathbf{0}$ 。这保证了第 0 步时 $\Delta\mathbf{W} = \mathbf{0}$ ,训练从预训练行为精确开始。
  • 前向计算:直接计算 $\mathbf{h} = \mathbf{W}_0 \mathbf{x} + \tfrac{\alpha}{r}\,\mathbf{B}(\mathbf{A}\mathbf{x})$切勿显式构造 $\mathbf{B}\mathbf{A}$ ——它是一个 $d_{\text{out}}\times d_{\text{in}}$ 的稠密矩阵,正是我们要避免的。
  • 缩放:因子 $\alpha/r$ 将步长与秩解耦:即使 $r$ 翻倍,有效学习率也不会翻倍,因此 $\alpha$ 可在不同秩设置下保持不变。
  • 合并:推理时可折叠适配器:$\mathbf{W}_{\text{merged}} = \mathbf{W}_0 + (\alpha/r)\,\mathbf{B}\mathbf{A}$ 。这带来 零延迟开销——你只需交付一个权重矩阵。
  • 应用位置:实证表明,将 LoRA 应用于 queryvalue 投影优于仅用于 attention 或 MLP,且在远低于“所有线性层”成本的情况下达到相近效果。

参数量差距有多大?#

不同方法的可训练参数量(对数刻度)

以 175B 模型为基础,全量微调与 LoRA($r=8$ )在可训练参数上的差距约为 五个数量级——但如文末 GLUE 图表所示,得分几乎无差别。


Adapter:Block 中的瓶颈模块#

Adapter 在 Transformer Block 中的位置

$$ \text{Adapter}(\mathbf{h}) = \mathbf{h} + \mathbf{W}_{\text{up}}\, \sigma(\mathbf{W}_{\text{down}}\, \mathbf{h}), \qquad \mathbf{W}_{\text{down}} \in \mathbb{R}^{m \times d},\; \mathbf{W}_{\text{up}} \in \mathbb{R}^{d \times m},\; m \ll d. $$

“先降维再升维”的技巧与 LoRA 相同,但作为额外层而非对现有层的增量。将 $\mathbf{W}_{\text{up}}$ 初始化为接近零,同样使模块初始表现为恒等映射。

Adapter vs LoRA——一个有用的对比:

AdapterLoRA
修改内容新增模块现有权重重参数化
推理延迟有(额外串行层)无(可合并至 $W_0$
单任务存储每层约 $2md$每层约 $r(d_\text{in}+d_\text{out})$
最佳适用编码器模型(BERT)生成模型(GPT 系列)

有两个后续改进值得关注。Pfeiffer Adapters 每个 Block 仅保留一个 Adapter(FFN 后),以一半参数恢复大部分性能。Parallel Adapters(He et al., 2021)将瓶颈计算与 FFN 并行执行,避免 GPU 上的串行依赖,显著缩小与 LoRA 的延迟差距。


Prefix-Tuning#

Prefix-Tuning:每层都加可学习的虚拟 token

$$\bigl[\mathbf{P}_1, \ldots, \mathbf{P}_m,\; \mathbf{x}_1, \ldots, \mathbf{x}_n\bigr] \to \text{Transformer}.$$

仅训练前缀矩阵,总参数量为 $m \times d \times L \times 2$$L$ 层的 key + value)。两个实践要点:

  • 直接优化前缀不稳定。原论文通过一个小 MLP 训练前缀,推理时丢弃 MLP。
  • 信息瓶颈真实存在:20 个 token 的前缀容量有限,在困难任务上比 LoRA 低 0.5–1 分。

概念上,前缀从第一层起引导注意力,影响模型对后续每个真实 token 的“关注点”。它在精神上最接近软提示,但作用于注意力机制内部而非嵌入表。


Prompt-Tuning vs P-Tuning v2#

Prompt-Tuning 只调输入层;P-Tuning v2 每层都调

Prompt-Tuning(Lester et al., 2021)是一种极致简化:仅在输入层调整 $m$ 个软 prompt 向量。可训练参数仅为 $m \times d$ ——通常几千个。

但有个前提:它只在超大模型上真正有效。Lester et al. 表明,与全量微调的差距随规模单调缩小;低于 ~1B 参数时损失严重,但在 10B+ 时基本消失。直觉是:百亿参数模型有足够容量“解读”任意连续 prompt,而小模型需要更局部的编辑能力。

P-Tuning v2(Liu et al., 2022)融合了 Prompt-Tuning 与 Prefix-Tuning:在每一层添加可学习 prompt,而非仅输入层。结果在多数任务上(包括小模型)匹配全量微调,且可训练参数仍约为 0.1%。

BitFit#

最简方法:仅微调偏置项(Zaken et al., 2021)。在 BERT 中约占 0.08% 参数,却在小数据 GLUE 任务上与全量微调竞争。为何有效?偏置调整了每个非线性层的输入分布,足以重定向激活路径,而无需改变线性映射本身。作为基线几乎免费;作为严肃方法,略逊于 LoRA。


QLoRA:量化 + LoRA#

QLoRA:4-bit 基座 + 分页优化器 + bf16 LoRA 适配器

QLoRA(Dettmers et al., 2023)是一项工程突破,使 65B 模型能在单张 48 GB GPU 上微调。它融合三项技术:

  1. NF4(4-bit NormalFloat)量化:冻结基座权重。码本在预训练权重近似零均值高斯分布的假设下信息论最优——该假设基本成立。
  2. 双重量化:对每块的缩放常数再次量化,每参数再省 ~0.4 bit。
  3. 分页优化器:通过统一内存在 CPU 与 GPU 间分页调度 Adam 状态,避免长序列 OOM。

关键在于,梯度仍流经量化 matmul 到上方的 bf16 LoRA 适配器。冻结基座无需 FP16 权重;仅小适配器需要。右图显示:65B 模型全量微调需 ~700 GB;QLoRA 仅需 ~50 GB。

QLoRA 在 Vicuna 基准上性能下降 <2%,却运行在便宜数个数量级的硬件上。

实现:从零开始实现 LoRA#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F

class LoRALayer(nn.Module):
    """W' = W_0 + (alpha / r) * B @ A.

    基础权重冻结,只有 A 和 B 参与梯度更新。
    """

    def __init__(self, in_features: int, out_features: int,
                 rank: int = 8, alpha: float = 16.0,
                 dropout: float = 0.0):
        super().__init__()
        self.in_features, self.out_features = in_features, out_features
        self.rank, self.alpha = rank, alpha
        self.scaling = alpha / rank

        # 冻结的预训练权重(稍后加载)
        self.weight = nn.Parameter(
            torch.empty(out_features, in_features), requires_grad=False)
        self.bias = nn.Parameter(
            torch.zeros(out_features), requires_grad=False)

        # 可训练的低秩因子
        self.lora_A = nn.Parameter(torch.empty(rank, in_features))
        self.lora_B = nn.Parameter(torch.zeros(out_features, rank))
        nn.init.kaiming_uniform_(self.lora_A, a=np.sqrt(5))

        self.dropout = nn.Dropout(dropout) if dropout > 0 else nn.Identity()
        self._merged = False

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        base = F.linear(x, self.weight, self.bias)
        if self._merged:
            return base
        # 关键点:绝不显式计算 B @ A
        delta = (self.dropout(x) @ self.lora_A.T) @ self.lora_B.T
        return base + self.scaling * delta

    @torch.no_grad()
    def merge_weights(self) -> None:
        """将 LoRA 更新合并到基础权重中,推理时无额外开销。"""
        if self._merged:
            return
        self.weight.add_(self.scaling * (self.lora_B @ self.lora_A))
        self.lora_A.zero_()
        self.lora_B.zero_()
        self._merged = True

def apply_lora(module: nn.Module, rank: int = 8, alpha: float = 16.0,
               targets: tuple[str, ...] = ("query", "value")) -> None:
    """递归替换指定的 nn.Linear 层为 LoRALayer。"""
    for name, child in module.named_children():
        if isinstance(child, nn.Linear) and any(t in name for t in targets):
            lora = LoRALayer(child.in_features, child.out_features, rank, alpha)
            lora.weight.data.copy_(child.weight.data)
            if child.bias is not None:
                lora.bias.data.copy_(child.bias.data)
            setattr(module, name, lora)
        else:
            apply_lora(child, rank, alpha, targets)

实现中有三点值得注意:

  1. 前向传播永不构建 $\mathbf{B}\mathbf{A}$ ——先左乘 $\mathbf{A}$ ,生成宽度为 $r$ 的中间结果。
  2. merge_weights 是一次性原地操作。合并后,该层行为与普通 nn.Linear 完全一致,服务时延迟不变。
  3. apply_lora 遍历模块树,仅替换名为 "query" / "value" 的层——这是 LoRA 论文的标准建议。

方法对比:GLUE 一图看懂#

PEFT 方法在 GLUE 上的效率-准确率关系

气泡图总结了 RoBERTa-base 在 GLUE 上的设计空间:

  • LoRA $r=8$ (0.24% 可训练参数) 几乎与全量微调持平(差距 <0.1 分)。
  • Adapter 质量匹配 LoRA,但参数量约 4 倍,且推理延迟不可忽略。
  • Prefix-Tuning 以小幅精度损失换取极低参数量,适合需存储数百任务适配器的场景。
  • Prompt-Tuning 仅在基座远大于 RoBERTa-base 时有竞争力;小模型上落后 3–4 分。
  • BitFit 作为极简基线,效果出人意料。

方法选型指南#

场景首选原因
GPT 式解码器微调LoRA(q,v)无延迟,易合并,支持 70B+
BERT 式多任务分类Adapter(Pfeiffer)单任务存储小,工具链成熟
超大基座(>10B),多任务Prompt-Tuning存储开销可忽略,随规模优雅扩展
需零推理开销LoRA + 合并最终模型为单矩阵
小样本 / 数千样本Prefix-Tuning 或 BitFit归纳偏置强,不易过拟合
单卡 24–48 GB,30B+ 模型QLoRANF4 + 分页 Adam 解锁该规模

如何选择 LoRA 的秩:从 $r = 8$ 开始。若验证集落后 >0.5 分,升至 16;若内存受限,降至 4。更大基座模型往往容忍(甚至偏好)更小的 $r$ ——适配的本征维度不随模型规模增长。


DoRA、OFT 与后 LoRA 浪潮#

LoRA 在 2022–2024 年主导领域,因其简单且效果稳定。2024–2025 年的新工作从三个正交方向优化该思路,部分已具生产价值。

DoRA:分解权重,再对方向应用 LoRA#

$$ W' = (m + \Delta m) \cdot \frac{W + \Delta V}{\|W + \Delta V\|}. $$

在 Llama-2-7B 推理基准上,DoRA 在相同参数量下缩小 LoRA 与全量微调差距约一半。代价:每适配矩阵增加一个 Linear(out_dim, 1) 和一次归一化——推理时可融合回 $W$ ,开销可忽略。

OFT:保持几何特性的正交微调#

Orthogonal Fine-Tuning(Qiu et al., 2023)以正交旋转替代加法更新:$W' = R \cdot W$ ,其中 $R$ 由小反对称矩阵的 Cayley 变换参数化。因 $R$ 正交,$W$ 的奇异值被精确保留。这对基座校准敏感的场景至关重要——文生图微调是典型应用,LoRA 易漂移风格预算,而 OFT 保持其完整。缺点是:相同秩下,OFT 每步计算成本高于 LoRA。

VeRA、IA³ 与极致压缩前沿#

VeRA(Kopiczko et al., 2024)在所有层共享同一随机低秩对 $(A, B)$ ,仅学每层缩放向量。在 GLUE 上,参数量比 LoRA 少 10 倍,精度近乎持平——适用于存储数千客户适配器且磁盘为瓶颈的场景。IA³ 更进一步,每 Transformer 层仅学三个缩放向量(key、value、FFN 各一),适配器需小于 100 KB 时是最佳选择。

坦白说:2025 年通用 LLM 微调,LoRA 仍是默认首选。若小秩下精度损失过大,选 DoRA;图像生成任务选 OFT;仅当适配器存储成瓶颈时,才考虑 VeRA。

大规模多 LoRA 服务#

LoRA 的架构优势——适配器是可加至冻结基座的小矩阵——仅在实际利用时转化为服务优势。我见过三种有效的生产模式。

热插拔适配器缓存#

基座模型常驻 GPU 内存,按请求切换 LoRA 矩阵。7B 模型 rank 16 时,每适配器约 16 MB;CPU 内存可存数百个,PCIe 传输 <5 ms。适合多租户 SaaS(每客户自有微调)。框架如 vLLM 的 --enable-lora、S-LoRA 或 Punica 均支持,内存布局略有差异。

批处理异构请求#

难点在于同一批次含不同 LoRA 请求。朴素方案串行处理,浪费批处理优势。S-LoRA(Sheng et al., 2023)通过将适配器分页至统一缓冲区,并用定制内核按请求 gather 对应 $A$$B$ 解决此问题。在 A100 上,“单 LoRA” 与 “单批次 100 LoRA” 的吞吐差距从 8× 缩至 <1.5×。若需并发服务多个适配器,此模式值得借鉴。

何时合并适配器回基座#

若单一适配器占流量 95%+,正确做法是将其合并回基座权重$W' = W + BA$ )并服务单合并模型。虽失多租户优势,但获单模型服务简洁性及小幅延迟收益(跳过适配器前向)。我们在某客户助手项目中做过此权衡:单一企业角色占 98% QPS,合并后 p99 延迟降 12%,精度无损。

决策树:若适配器 QPS 相近,保持分离并用 S-LoRA 式批处理;若一者主导,合并之;若有数百低 QPS 适配器,采用 CPU 缓存热插拔。

Adapter 推理延迟瓶颈分析#

前文提到 Adapter 会增加推理延迟,这虽属实,但量化其影响至关重要——因为串行与并行放置的权衡高度依赖 batch size,这也是 LoRA 最终胜出生产部署的关键原因。

串行 vs 并行放置#

$$ h_{\text{out}} = h_{\text{in}} + \text{Adapter}(\text{FFN}(h_{\text{in}})). $$

Adapter 的前向传播必须等待 FFN 完成。在 GPU 上这是硬性依赖——两次连续的 CUDA 启动各自带来 kernel 启动开销和 SM 调度停顿。

$$ h_{\text{out}} = h_{\text{in}} + \text{FFN}(h_{\text{in}}) + \text{Adapter}(h_{\text{in}}). $$

在 SM 资源充足的硬件上,两条分支可重叠执行,Adapter 的实际耗时趋近于零。但在某些任务上精度略低,因为 Adapter 不再接收 FFN 变换后的激活值。

微基准测试#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import time
import torch
import torch.nn as nn

d, m = 1024, 64
ffn = nn.Sequential(nn.Linear(d, 4 * d), nn.GELU(), nn.Linear(4 * d, d)).cuda()
adapter = nn.Sequential(nn.Linear(d, m), nn.GELU(), nn.Linear(m, d)).cuda()

def serial_forward(x):
    return x + adapter(ffn(x))

def parallel_forward(x):
    return x + ffn(x) + adapter(x)

def bench(fn, x, n=200):
    for _ in range(20): fn(x)               # 预热
    torch.cuda.synchronize()
    t0 = time.perf_counter()
    for _ in range(n): fn(x)
    torch.cuda.synchronize()
    return (time.perf_counter() - t0) / n * 1e3  # 毫秒

for bs in [1, 8, 32, 128]:
    x = torch.randn(bs, 128, d, device="cuda")
    base = bench(lambda z: z + ffn(z), x)
    ser = bench(serial_forward, x)
    par = bench(parallel_forward, x)
    print(f"bs={bs:3d}  base={base:.3f}  serial={ser:.3f} (+{(ser/base-1)*100:.1f}%)"
          f"  parallel={par:.3f} (+{(par/base-1)*100:.1f}%)")

在 A100(FP32 权重)上结果大致如下:

batchbase (ms)serial开销parallel开销
10.410.48+18%0.43+6%
80.430.49+14%0.44+4%
320.620.68+9%0.63+2%
1281.851.93+4%1.88+1.5%

呈现两大规律:首先,相对开销随 batch size 增大而下降——kernel 启动成本被更多计算摊薄;其次,并行在所有 batch size 下均优于串行,且在小 batch 时优势最明显。

实践启示#

对于推理服务,并行 Adapter 严格优于串行。对于训练,串行在某些任务上可能高出 0.2–0.5 分——当 Adapter 的作用是精炼 FFN 输出而非注入正交信号时(如 NLI 任务最为典型)。因此选择成为精度与延迟间的微调旋钮。

但两者在推理时均逊于 LoRA。一旦合并,LoRA 不引入任何新运算;该层前向传播与普通线性层完全一致。这正是后续章节所依托的结构性优势:参数高效性同时带来运行时高效性。

多 Adapter 服务备注#

当并发服务多个 Adapter 时,延迟情况再次变化。串行 Adapter 导致请求间分支发散——不同客户需要不同的瓶颈权重,无法共享 kernel 启动。并行 Adapter 虽有同样问题,但至少能与 FFN 重叠,部分隐藏发散成本。LoRA 在此场景亦无额外开销:无论逻辑上应用哪个 Adapter,合并路径始终是单次矩阵乘法(前提是内存中保留各合并副本)。若因 Adapter 数量过多无法保留副本,本文前述的 S-LoRA gather kernel 可恢复大部分吞吐量。Adapter 则无类似解决方案。


NF4 量化原理剖析#

QLoRA 的核心突破是在单张 48 GB GPU 上微调 65B 模型。其关键技术是冻结基座权重的 NF4 码本。有必要理解它为何显著优于均匀 int4。

均匀 int4 为何浪费比特#

均匀 int4 将值域 $[-w_{\max}, w_{\max}]$ 等分为 16 个间隔。若权重分布均匀,此方案最优。但实际并非如此。预训练 Transformer 权重近似零均值高斯分布且方差小——多数权重聚集在零附近,仅有细长尾部。等间距码本将一半码字分配给极少权重所在的区域,另一半又过于粗糙无法精细表示密集中心区。结果导致零附近(影响最大处)量化误差显著。

NormalFloat 构造原理#

$$ q_i = \Phi^{-1}\!\left(\frac{i}{16}\right), \qquad i = 1, \ldots, 15, $$

并以区间中点(或条件均值)作为 16 个码本值。Dettmers 等人(2023)稍作调整,确保恰好有一个码字落在零点——因预训练权重经剪枝类正则化后确有零模态。

$$ \{-1.00, -0.70, -0.53, -0.39, -0.28, -0.18, -0.09, 0.00, 0.08, 0.16, 0.25, 0.34, 0.44, 0.56, 0.72, 1.00\}. $$

注意其非对称间隔:零附近(高斯质量集中区)码字密集,尾部稀疏。

从零实现#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import torch
from torch.distributions import Normal

def build_nf4_codebook() -> torch.Tensor:
    """16 个码字,按标准正态 CDF 等概率划分,锚定于 0 和 ±1。"""
    normal = Normal(0.0, 1.0)
    # 非对称划分:8 个负值,1 个零,7 个正值(Dettmers 约定)。
    neg = normal.icdf(torch.linspace(0.5 / 8, 0.5, 8))
    pos = normal.icdf(torch.linspace(0.5, 1 - 0.5 / 7, 7))[1:]
    levels = torch.cat([neg, torch.tensor([0.0]), pos])
    levels = levels / levels.abs().max()           # 归一化至 [-1, 1]
    return levels.sort().values

NF4 = build_nf4_codebook()

def quantize_nf4(W: torch.Tensor, blocksize: int = 64):
    """每块绝对值最大值缩放,最近邻码本舍入。"""
    W_flat = W.flatten()
    pad = (-W_flat.numel()) % blocksize
    W_flat = torch.cat([W_flat, W_flat.new_zeros(pad)])
    blocks = W_flat.view(-1, blocksize)
    scale = blocks.abs().amax(dim=1, keepdim=True).clamp(min=1e-8)
    normed = blocks / scale                                    # 归一化至 [-1, 1]
    # 为每个元素查找最近邻码本索引。
    dist = (normed.unsqueeze(-1) - NF4.to(W).view(1, 1, -1)).abs()
    idx = dist.argmin(dim=-1).to(torch.uint8)                  # 4-bit 索引
    return idx, scale.squeeze(1), W.shape, pad

def dequantize_nf4(idx, scale, shape, pad):
    levels = NF4.to(scale).gather(0, idx.long().flatten()).view_as(idx)
    out = (levels * scale.unsqueeze(1)).flatten()
    if pad: out = out[:-pad]
    return out.view(shape)

# 反向传播使用直通估计:dequant 的梯度视为恒等映射。
class NF4Linear(torch.autograd.Function):
    @staticmethod
    def forward(ctx, x, idx, scale, shape, pad):
        W = dequantize_nf4(idx, scale, shape, pad)
        ctx.save_for_backward(x, W)
        return x @ W.T
    @staticmethod
    def backward(ctx, gy):
        x, W = ctx.saved_tensors
        return gy @ W, None, None, None, None

在 Llama-7B 注意力投影规模的随机高斯矩阵上快速验证重建误差:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
W = torch.randn(4096, 4096)
idx, scale, shape, pad = quantize_nf4(W)
W_hat = dequantize_nf4(idx, scale, shape, pad)
mse_nf4 = ((W - W_hat) ** 2).mean().item()

# 均匀 int4 基线。
s = W.abs().max() / 7
W_uni = (W / s).round().clamp(-8, 7) * s
mse_uniform = ((W - W_uni) ** 2).mean().item()
print(f"NF4 MSE = {mse_nf4:.4f}    uniform int4 MSE = {mse_uniform:.4f}")

典型结果:NF4 约 0.04,均匀 int4 约 0.11 —— 后者误差近三倍。在真实 Llama 权重上差距更大,因其比拟合高斯更集中于零点。

双重量化#

NF4 为每 64 元素块存储一个 FP32 绝对值最大值。此开销为 $32 / 64 = 0.5$ 比特/参数——当权重本身仅 4 比特时不可忽略。双重量化将这些绝对值最大值进一步量化为 int8,并为每 256 块组使用二级 FP32 缩放因子。额外误差可忽略(绝对值最大值分布本身良好),但节省约 0.4 比特/参数,即 7B 模型节省约 0.4 GB。65B 模型节省数 GB——这正是能否塞进 48 GB 显存的关键。

为何能在单 GPU 运行 65B#

计算如下:65B 参数经 NF4 + 双重量化后约为 $65 \cdot 10^9 \cdot 4.5 \,\text{bits} / 8 = 36.6$ GB(冻结基座)。加上 bf16 LoRA Adapter($r=64$ 时约 200 MB)、仅 Adapter 的 Adam 状态(约 0.8 GB)及最长序列的激活值,总显存低于 48 GB。若用 FP16 基座权重(仅此就需 130 GB),则完全不可行。

QLoRA 精度得以保持的原因也在于此:梯度信号仍通过忠实的反量化基座权重传递,因此 LoRA Adapter 学习的是对近似真实模型的修正,而非损坏模型。

直通估计器的隐藏之处#

dequantize_nf4 的反向传播在技术上不可导——argmin 和整数索引几乎处处梯度为零。上述 NF4Linear.backward 通过将反量化权重视为参数,直接传播 $\partial \mathcal{L} / \partial W$ 来规避此问题。这就是直通估计器(STE),在 QLoRA 场景下正确的原因很微妙:冻结基座权重从不接收梯度更新,因此 STE 偏差不会累积。唯一重要的梯度来自量化矩阵乘法之上的 LoRA Adapter,而这些梯度传递完全精确。若尝试将梯度反传至量化权重本身(如量化感知训练),则需谨慎重新设计 STE。

常见问题#

与全量微调相比,性能损失多少?#

10B 参数模型,多数基准上损失 <1 分(常在噪声内)。<1B 参数时,常见 2–5 分差距,尤以小数据集为甚。

LoRA 应使用何种学习率?#

比全量微调高 1–2 个数量级。可训练参数初始近零,可承受更大步长而不发散。典型调度:$\text{lr} = 1\text{e-}4$$5\text{e-}4$ ,配合余弦衰减。

PEFT 能否与量化结合?#

可以——这正是 QLoRA。NF4 + 双重量化 + bf16 LoRA 可在单 48 GB GPU 上微调 65B 模型,性能下降 <2%。

是否需将 LoRA 应用于所有线性层?#

非必需。原论文发现 query + value 已捕获大部分增益;加入 key 和 output 仅带来边际提升,参数量却翻倍。对纯解码器 LLM,有时在 MLP up/down 投影上添加对长上下文任务有帮助。

Adapter 与 LoRA 推理时孰优?#

LoRA 合并后无额外开销:模型为单权重矩阵,推理与全量微调一致。Adapter 因额外串行子层,每 block 增加 1–3% 延迟;并行 Adapter 可降至 <1%。

参考文献#

  • Hu, E. J., et al. (2021). LoRA: Low-Rank Adaptation of Large Language Models. ICLR.
  • Aghajanyan, A., Gupta, S., & Zettlemoyer, L. (2020). Intrinsic Dimensionality Explains the Effectiveness of Language Model Fine-Tuning. ACL.
  • Houlsby, N., et al. (2019). Parameter-Efficient Transfer Learning for NLP. ICML.
  • Pfeiffer, J., et al. (2021). AdapterFusion: Non-Destructive Task Composition for Transfer Learning. EACL.
  • He, J., et al. (2021). Towards a Unified View of Parameter-Efficient Transfer Learning. ICLR.
  • Li, X. L., & Liang, P. (2021). Prefix-Tuning: Optimizing Continuous Prompts for Generation. ACL.
  • Lester, B., Al-Rfou, R., & Constant, N. (2021). The Power of Scale for Parameter-Efficient Prompt Tuning. EMNLP.
  • Liu, X., et al. (2022). P-Tuning v2: Prompt Tuning Can Be Comparable to Fine-Tuning Universally Across Scales and Tasks. ACL.
  • Zaken, E. B., Ravfogel, S., & Goldberg, Y. (2021). BitFit: Simple Parameter-Efficient Fine-Tuning for Transformer-Based Masked Language-Models. ACL.
  • Dettmers, T., et al. (2023). QLoRA: Efficient Finetuning of Quantized LLMs. NeurIPS.
本系列

迁移学习 12 篇

  1. 01 迁移学习(一):基础与核心概念
  2. 02 迁移学习(二):预训练与微调
  3. 03 迁移学习(三):域适应
  4. 04 迁移学习(四):小样本学习
  5. 05 迁移学习(五):知识蒸馏
  6. 06 迁移学习(六):多任务学习
  7. 07 迁移学习(七):零样本学习
  8. 08 迁移学习(八):多模态迁移
  9. 09 迁移学习(九):参数高效微调 当前
  10. 10 迁移学习(十):持续学习
  11. 11 迁移学习(十一):跨语言迁移
  12. 12 迁移学习(十二):工业应用与最佳实践

读有所得?

GitHub 关注我 → 新文周更

GitHub