01

微调全景图

Fine-Tuning Landscape

全参数微调 (Full Fine-Tuning) 需更新模型所有参数,成本高昂;参数高效微调 (PEFT) 仅训练极少量附加参数,在接近全量微调性能的同时大幅降低显存与存储开销。

方法对比 — 可训练参数占比
Full Fine-Tuning
更新模型 100% 参数。性能上限最高,但需为每个任务保存完整模型副本,显存开销极大。
LoRA
低秩矩阵分解 W = W₀ + BA,仅训练 0.1%~1% 参数,可热插拔切换任务。
QLoRA
4-bit 量化基座 + FP16 LoRA 适配器,可在单张 48GB GPU 上微调 65B 模型。
Adapter
在 Transformer 层间插入瓶颈网络 d→r→d,参数量小但引入额外推理延迟。
Prefix Tuning
在每层 attention 的 K/V 前拼接可学习前缀向量,不修改模型权重,参数量极小。
Prompt Tuning
仅在输入层添加可学习 soft prompt,参数量最小,但容量有限、适合大模型。
python peft_overview.py复制
# PEFT — 参数高效微调概述
from peft import LoraConfig, get_peft_model, TaskType

# 1. 定义 LoRA 配置
config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    r=8,                       # 秩
    lora_alpha=16,              # 缩放因子
    lora_dropout=0.05,
    target_modules=["q_proj", "v_proj"],  # 目标层
)

# 2. 包装基础模型 — 冻结原始权重,仅训练适配器
model = get_peft_model(base_model, config)
model.print_trainable_parameters()
# trainable params: 1,572,864 || all params: 675,235,840 || trainable%: 0.23%
02

LoRA 低秩适配

LoRA: Low-Rank Adaptation

LoRA 假设权重更新具有低秩结构,将增量矩阵分解为两个小矩阵的乘积 W = W₀ + BA,其中 B∈ℝ^(d×r)、A∈ℝ^(r×d),秩 r 远小于 d,使可训练参数从 d² 降至 2dr。

矩阵分解可视化 — W = W₀ + B × A
内在维度假说
预训练模型具有低 "内在维度",权重的有效更新可以用远低于原始维度的低秩矩阵近似。
秩的选择
通常 r=4~64 即可。秩越大表达力越强但参数增多,收益递减。
缩放因子 α
实际更新为 ΔW = (α/r) · BA,α 用于调节适配器强度,可不重新调参即切换 r。
python lora_layer.py复制
import torch
import torch.nn as nn

class LoRALinear(nn.Module):
    def __init__(self, in_features, out_features, r=8, alpha=16):
        super().__init__()
        self.W0 = nn.Linear(in_features, out_features, bias=False)
        self.W0.weight.requires_grad = False          # 冻结原始权重

        self.B = nn.Parameter(torch.zeros(out_features, r))    # d_out × r
        self.A = nn.Parameter(torch.randn(r, in_features) * 0.01)  # r × d_in
        self.scaling = alpha / r

    def forward(self, x):
        # W = W₀ + (α/r) · B·A
        return self.W0(x) + self.scaling * (x @ self.A.T) @ self.B.T
03

QLoRA 量化微调

QLoRA: Quantized LoRA

QLoRA 将基座模型权重量化为 4-bit NF4,同时以 FP16 训练 LoRA 适配器,配合双重量化和分页优化器,在几乎不损失性能的前提下将显存占用降至全量微调的数分之一。

显存对比 — Full FT vs LoRA vs QLoRA
NF4 量化
Normal Float 4-bit:基于正态分布信息论最优的 4-bit 数据类型,量化误差最小。
双重量化
对量化常数本身再次量化,每个参数额外节省 0.37 bit,进一步降低显存。
分页优化器
利用 NVIDIA 统一内存,在显存不足时自动将优化器状态分页到 CPU 内存,避免 OOM。
python qlora_setup.py复制
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
from peft import LoraConfig, prepare_model_for_kbit_training

# 1. 4-bit NF4 量化配置
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",               # NormalFloat4
    bnb_4bit_compute_dtype=torch.float16,
    bnb_4bit_use_double_quant=True,           # 双重量化
)

# 2. 加载 4-bit 量化模型
model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-2-7B",
    quantization_config=bnb_config,
    device_map="auto",
)

# 3. 准备 k-bit 训练 + 应用 LoRA
model = prepare_model_for_kbit_training(model)
model = get_peft_model(model, LoraConfig(r=64, lora_alpha=16,
    target_modules=["q_proj","k_proj","v_proj","o_proj"]))
04

Adapter 层

Adapter Layers

Adapter 在 Transformer 每层内部插入瓶颈结构:先降维 (d→r)、经非线性激活、再升维 (r→d),仅训练瓶颈参数。推理时可通过移除 Adapter 恢复原模型,实现零延迟部署。

瓶颈结构 — down-project → activation → up-project
Adapter (Houlsby)
在每层 attention 和 FFN 后各插入一个 Adapter,参数量约为模型的 0.5%~8%。
AdapterFusion
先为每个任务单独训练 Adapter,再用 Fusion 层学习加权组合,实现多任务知识共享。
Compacter
用低秩参数化矩阵 (PHM) 进一步压缩 Adapter 参数,参数量可降至 0.05%。
python adapter_module.py复制
import torch.nn as nn

class Adapter(nn.Module):
    def __init__(self, d_model=768, bottleneck=64):
        super().__init__()
        self.down = nn.Linear(d_model, bottleneck)        # 降维 d → r
        self.act  = nn.GELU()                            # 非线性激活
        self.up   = nn.Linear(bottleneck, d_model)        # 升维 r → d
        self.scale = nn.Parameter(torch.ones(1))         # 残差缩放

    def forward(self, h):
        # 残差连接:h + scale · up(act(down(h)))
        return h + self.scale * self.up(self.act(self.down(h)))
05

Prefix Tuning

Prefix Tuning

Prefix Tuning 在每层 attention 的 Key 和 Value 前拼接 l 个可学习前缀向量,通过 MLP 重参数化稳定训练。模型权重完全冻结,仅训练前缀参数。

前缀注入 — 可学习前缀向量进入 Attention
Prefix Tuning
在每层注入前缀,用 MLP 重参数化 Pθ = MLP(P) 稳定训练,参数量 ~0.1%。
Prompt Tuning
仅在输入嵌入层添加 soft prompt,不涉及深层,参数量最小但适合超大模型。
P-tuning v2
Prefix Tuning 的通用化版本,在所有层添加前缀,对小模型也有效。
python prefix_tuning.py复制
class PrefixTuning(nn.Module):
    def __init__(self, num_layers=12, num_heads=12,
                 prefix_len=20, d_head=64):
        super().__init__()
        self.prefix_len = prefix_len
        d_kv = num_heads * d_head

        # 可学习前缀参数(重参数化)
        self.prefix_keys = nn.Parameter(torch.randn(num_layers, prefix_len, d_kv))
        self.prefix_vals = nn.Parameter(torch.randn(num_layers, prefix_len, d_kv))

    def forward(self, layer_idx, K, V):
        # 在第 layer_idx 层的 K/V 前拼接前缀
        pk = self.prefix_keys[layer_idx]    # [prefix_len, d_kv]
        pv = self.prefix_vals[layer_idx]
        K = torch.cat([pk.unsqueeze(0), K], dim=1)  # 拼接
        V = torch.cat([pv.unsqueeze(0), V], dim=1)
        return K, V
06

指令微调 SFT

Supervised Fine-Tuning

指令微调使用 "指令-回复" 对训练模型遵循人类指令。关键技巧是损失掩码:仅对回复部分计算损失,避免模型学习复述指令,提升指令遵循能力。

损失掩码 — 仅在回复 token 上计算损失
Alpaca 格式
instruction / input / output 三字段,Stanford Alpaca 风格。
ChatML 格式
<|im_start|>user/assistant<|im_end|> 角色标记,OpenAI 兼容。
Vicuna 格式
USER: / ASSISTANT: 前缀,多轮对话分隔。
python sft_training.py复制
def sft_loss(logits, labels, attention_mask):
    # 损失掩码:仅在回复 token 上计算损失
    # labels 中 instruction 部分设为 -100 (忽略)
    shift_logits = logits[..., :-1, :].contiguous()
    shift_labels = labels[..., 1:].contiguous()

    loss_fct = nn.CrossEntropyLoss(ignore_index=-100)
    loss = loss_fct(
        shift_logits.view(-1, shift_logits.size(-1)),
        shift_labels.view(-1),
    )
    return loss

# 构造带掩码的标签
def build_labels(input_ids, response_start):
    labels = input_ids.clone()
    labels[:, :response_start] = -100   # 掩码指令部分
    return labels
07

分布式训练

Distributed Training

大模型训练需要分布式策略。数据并行 (DP) 复制完整模型到每张 GPU;张量并行 (TP) 切分矩阵到不同 GPU;流水线并行 (PP) 按层切分;FSDP/ZeRO 则切分优化器状态、梯度与参数。

并行策略对比 — GPU 显存与通信
DDP
数据并行,每 GPU 持有完整模型副本,反向传播后 AllReduce 同步梯度。简单高效。
FSDP
Fully Sharded Data Parallel,切分参数/梯度/优化器状态,按需 AllGather 还原。
ZeRO Stage 1/2/3
Stage1 切优化器状态,Stage2 加梯度,Stage3 再加参数,逐级降显存、增通信。
Tensor Parallel
将权重矩阵按列/行切分到多 GPU,层内通信量大但无需存储完整副本。
python ddp_training.py复制
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP

def setup_ddp():
    dist.init_process_group(backend="nccl")
    torch.cuda.set_device(dist.get_rank())

def train_step(model, optimizer, batch):
    # model 是 DDP 包装后的模型
    inputs, labels = batch
    outputs = model(inputs)                    # 前向(含自动同步准备)
    loss = loss_fn(outputs, labels)

    optimizer.zero_grad()
    loss.backward()                          # 反向 → AllReduce 同步梯度
    optimizer.step()                           # 各 GPU 独立更新(梯度相同)
    return loss.item()

# FSDP 等价 — 只需替换包装器
# from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
# model = FSDP(model, sharding_strategy=ShardingStrategy.FULL_SHARD)
08

灾难性遗忘

Catastrophic Forgetting

微调新任务时模型会遗忘旧任务知识。通过经验回放、弹性权重巩固 (EWC)、L2 正则化等方法可缓解遗忘;LoRA 因冻结基座权重,天然具有抗遗忘优势。

知识保留曲线 — 旧任务准确率随微调步数衰减
经验回放 Replay
在训练集中混入旧任务数据,简单有效但需保存旧数据,可能违反隐私。
EWC 弹性权重巩固
基于 Fisher 信息矩阵约束重要参数变化:L = L_task + λ Σ F_i(θ_i-θ*_i)²。
L2 正则化
向原始权重施加 L2 惩罚,简单但对所有参数一视同仁,效果弱于 EWC。
LoRA 抗遗忘
基座权重完全冻结,新任务仅训练适配器,旧任务知识不会被动摇。
python ewc_penalty.py复制
class EWC:
    def __init__(self, model, fisher=None, params_star=None):
        self.fisher = fisher              # Fisher 信息矩阵
        self.params_star = params_star    # 旧任务最优参数

    def penalty(self, model):
        # L_ewc = Σ F_i · (θ_i - θ*_i)²
        loss = 0
        for n, p in model.named_parameters():
            if n in self.fisher:
                loss += (self.fisher[n] * (p - self.params_star[n]) ** 2).sum()
        return loss

# 训练时:总损失 = 任务损失 + λ · EWC惩罚
# total_loss = task_loss + lambda_ewc * ewc.penalty(model)