minimind/course/notes/18_lora_notes.md

6.6 KiB
Raw Blame History

Lesson 18 笔记:LoRA 原理与实现

目录


setattr(module, "lora", lora) 的含义

给 module 动态新增属性 lora,等价于 module.lora = lora。

为什么用 setattr 而不是直接赋值?

  • 属性名可以是变量,更灵活
  • 在循环里更通用

LoRA 代码里的作用:

setattr(module, "lora", lora)

在每个被改造的 Linear 层上额外挂一个 LoRA 子模块。

结构变成:

Linear layer:
    ├── weight (原始参数,冻结)
    ├── bias
    └── lora  ← 新增的模块

挂上去有什么用?

  1. 方便保存 checkpoint:model.state_dict() 会自动包含 layer.lora.A.weight、layer.lora.B.weight
  2. 方便 freeze/unfreeze:可以统一对 module.lora.requires_grad_(True)
  3. 方便 debug/inspect:可以直接 print(module.lora)
  4. 方便 LoRA merge:推理时 W = W + BA,需要访问 lora 参数

为什么不 setattr 会怎样?

仍然可以 module.forward = ...,但:

  • state_dict 不一定能找到
  • 不能统一 freeze
  • 不能 merge
  • 不好保存结构

一句话总结:

setattr(module, "lora", lora) 本质是在原始 PyTorch layer 上动态挂一个 LoRA 子模块,让它成为模型结构的一部分,便于训练、保存和管理。

为什么 LoRA 要"挂在 layer 上"?

因为 LoRA 本质是每个 Linear layer 的"附加模块",不是独立模型:

Transformer Block
   ├── Linear
   │     ├── W (冻结)
   │     └── LoRA (训练)

LoRA 生命周期工具链

理解为一整套"LoRA 插件系统":安装 → 训练 → 保存 → 加载 → 合并部署

总体流程图

        ┌──────────────┐
        │ apply_lora   │  ← 初始化模型(插插件)
        └──────┬───────┘
               ↓
        训练(只更新 LoRA)
               ↓
        ┌──────────────┐
        │ save_lora    │  ← 保存 LoRA 参数
        └──────┬───────┘
               ↓
        训练结束 / 断点
               ↓
        ┌──────────────┐
        │ load_lora    │  ← 加载 LoRA 继续训练/推理
        └──────┬───────┘
               ↓
        ┌──────────────┐
        │ merge_lora   │  ← 合并进 W(部署用)
        └──────────────┘

apply_lora(初始化时)

def apply_lora(model, rank=16):
    for name, module in model.named_modules():
        if isinstance(module, nn.Linear) and module.in_features == module.out_features:
            lora = LoRA(module.in_features, module.out_features, rank=rank).to(model.device)
            setattr(module, "lora", lora)
            original_forward = module.forward

            def forward_with_lora(x, layer1=original_forward, layer2=lora):
                return layer1(x) + layer2(x)

            module.forward = forward_with_lora

作用: 给模型每个 Linear 层外挂 LoRA 分支,改写 forward。

用在: 训练开始前


save_lora(训练中/后)

def save_lora(model, path):
    raw_model = getattr(model, '_orig_mod', model)
    state_dict = {}
    for name, module in raw_model.named_modules():
        if hasattr(module, 'lora'):
            clean_name = name[7:] if name.startswith("module.") else name
            lora_state = {f'{clean_name}.lora.{k}': v.cpu().half() for k, v in module.lora.state_dict().items()}
            state_dict.update(lora_state)
    torch.save(state_dict, path)

作用: 只保存 LoRA 参数(A/B矩阵),不保存原模型

用在: checkpoint 或训练结束保存 adapter


load_lora(恢复/推理前)

def load_lora(model, path):
    state_dict = torch.load(path, map_location=model.device)
    state_dict = {(k[7:] if k.startswith('module.') else k): v for k, v in state_dict.items()}

    for name, module in model.named_modules():
        if hasattr(module, 'lora'):
            lora_state = {k.replace(f'{name}.lora.', ''): v for k, v in state_dict.items() if f'{name}.lora.' in k}
            module.lora.load_state_dict(lora_state)

作用: 把 LoRA 权重重新塞回 model.lora

用在: 继续训练、推理前加载 adapter

前提: 必须先 apply_lora(model)


merge_lora(部署前)

def merge_lora(model, lora_path, save_path):
    load_lora(model, lora_path)
    raw_model = getattr(model, '_orig_mod', model)
    state_dict = {k: v.cpu().half() for k, v in raw_model.state_dict().items() if '.lora.' not in k}
    for name, module in raw_model.named_modules():
        if isinstance(module, nn.Linear) and '.lora.' not in name:
            state_dict[f'{name}.weight'] = module.weight.data.clone().cpu().half()
            if hasattr(module, 'lora'):
                state_dict[f'{name}.weight'] += (module.lora.B.weight.data @ module.lora.A.weight.data).cpu().half()
    torch.save(state_dict, save_path)

作用: 把 LoRA 融合进权重 W,生成纯模型

核心步骤:

  1. load_lora() 加载 LoRA
  2. W' = W + B @ A 合并权重
  3. 保存纯模型

用在: 推理部署前


四个函数角色分工

函数 作用 什么时候用
apply_lora 给模型插 LoRA 训练前
save_lora 存 LoRA 参数 训练中/结束
load_lora 恢复 LoRA 继续训练/推理
merge_lora 合并成完整模型 推理部署前

三种典型使用模式

模式1:训练 LoRA

apply_lora → train → save_lora

模式2:继续训练

apply_lora → load_lora → train

模式3:推理部署

apply_lora (可选) → load_lora → merge_lora → inference

一句话理解整个系统

  • apply_lora 是"装插件"
  • save/load_lora 是"存插件"
  • merge_lora 是"拆插件并写死进模型"