Skip to content

Latest commit

 

History

12 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

lora-lab · 从零实现 LoRA / QLoRA / SFT

不看 PEFT 源码,亲手写出 LoRA 的数学、QLoRA 的 NF4 存储、SFT 的 prompt 掩码与训练循环——然后在 CPU 上用真实训练数据对比 LoRA SFT vs 全参 SFT vs 不微调。

tests Python License: MIT

📊 实测对比(本机 CPU · 2 层 × 128d 字符级 GPT · 复现:uv run python train.py)

在通用语料(40 篇短文)上预训练后,用 20 条 Q: => A: 指令对做 SFT:

方案 可训练参数 SFT 验证损失 相对 baseline
不微调(baseline) — 6.82 —
LoRA SFT(r=8) 9.85% 5.91 -13.3% ✅
全参 SFT 100% 6.07 -11.0%

生成效果(prompt:Q: what is lora,贪心解码):

baseline: Q: what is loravel keepthet at ber ocom l ry tokeng ma
LoRA SFT: Q: what is loraA: lora adds small trainable matrices to

两个诚实的观察:① LoRA 以不到 1/10 的可训练参数赢了全参微调——20 条样本的小数据上,全参更新更容易过拟合(这正是 QLoRA 论文的核心论点之一);② 模型是字符级玩具规模,"答对"来自对训练对的记忆,本仓库证明的是机制与数值契约,不是涌现能力。

🎯 真模型实测:Qwen2.5-1.5B QLoRA(RTX 3090 24GB)

同一套机制(NF4 基座 + fp32 LoRA + prompt 掩码 + 余弦调度)在真模型上的完整复现(results/qwen-qlora-3090.json,复现:examples/train_qwen_gpu.py --samples 3000 --epochs 2):

项目 数值
模型 / 数据 Qwen2.5-1.5B-Instruct × Belle 数学指令 3000 条
训练配置 r=16, α=32, bs=4×grad_accum=4, 2 epochs, 356 steps
验证损失 1.1876 → 0.8386(-29.4%)
训练耗时 / 显存 15.3 分钟 / 峰值 21.7 GB(GPU 100% 利用率)
适配器体积 ~26 MB(vs 全参微调要存 3 GB)

训练损失轨迹(每 20 步记录):1.00 → 0.68 → … → 0.64(完整曲线在 results 文件里)。3090 24GB 跑 1.5B QLoRA 显存余量充足,同配置可上 7B(权重 ~5GB)。

规模对照:同数据同管线,1.5B vs 7B(3090)

模型 训练配置 显存峰值 验证损失 降幅 耗时
Qwen2.5-1.5B-Instruct r=16 · bs4 21.7 GB 1.1876 → 0.8386 -29.4% 15.3 min
Qwen2.5-7B-Instruct r=16 · bs2 · 梯度检查点 13.3 GB 1.5022 → 0.7707 -48.7% ~55 min

同一套管线只改了模型名和 batch size(2 vs 4)。7B 的基座更强(before loss 1.50 vs 1.19 是"更差的起点"——它对这套小样本的分布更保守),微调后反而降得更深。数据:results/qwen-7b-3090.json。

微调前后生成对比(examples/eval_generations.py,同一道题贪心解码):

Q: 计算 (123 + 456) × 2 − 78,写出步骤。 微调前:学术 LaTeX 腔,\( (123 + 456) \times 2 - 78 \)…(列公式为主) 微调后:干净的逐步计算——123 + 456 = 579 → 579 × 2 = 1158 → 1158 − 78 = 1080(Belle 语料的格式风格被成功注入)

完整 8 题对照在 results/generations_compare.json:微调主要改变输出风格与步骤结构(分步、竖式化、去 LaTeX),这正是 SFT 注入领域格式的直接证据。

✨ 从零实现了什么

模块 内容 验证方式
lora.py LoRALinear:冻结基座 + (α/r)·BA 残差路径;B 零初始化(起步等价于基座);Kaiming 初始化 A(对齐 PEFT);merge() 把 ΔW 折叠回权重 测试:初始输出与基座逐位一致、前向数学恒等式、merge 后输出一致、可训练参数数 = r·(d_in+d_out)、只有 LoRA 参数有梯度
qlora.py NF4Linear:NF4 16 电平查表(数值转录自论文)+ fp32 块缩放 absmax;inject_qlora 在 NF4 基座上叠加 fp32 LoRA 测试:uint4 索引存储 <1 byte/权重、前向误差 <20%(4bit 教科书界)、QLoRA 适配器能反传梯度
data.py 字符级 tokenizer、语料 packing(EOS 拼接切块)、SFT prompt 掩码(prompt 位置 label=-100,只学答案) 测试:token 守恒、prompt 位恰好被掩码、确定性切分
trainer.py 150 行训练器:AdamW、线性 warmup + 余弦衰减到 10%、梯度累积、梯度裁剪、验证评估、采样生成 测试:调度曲线关键点、裁剪范数、训练损失下降
train.py 端到端实验:预训练 → 三种 SFT 对比 → 生成样本 上表的实测数据
examples/train_qwen_gpu.py 生产级配方:transformers + PEFT + bitsandbytes 对 Qwen 做 QLoRA(需 GPU,概念与本仓一一对应) 文档说明

🚀 快速开始

uv sync                      # torch + pytest
uv run pytest                # 17 passed
uv run python train.py       # ~10s CPU,复现上表

# 有 CUDA GPU?真模型 QLoRA(RTX 3090 实测 15 分钟 / 21.7GB)
uv sync --extra gpu
uv run python examples/train_qwen_gpu.py --smoke        # 5 分钟冒烟
uv run python examples/train_qwen_gpu.py --samples 3000 --epochs 2

作为库使用(任何 nn.Module 都能注入):

import torch.nn as nn
from lora_lab.lora import inject_lora, mark_only_lora_trainable, trainable_ratio

model = load_your_model()          # 任意含 nn.Linear 的模型
inject_lora(model, ("q_proj", "v_proj"), r=16, alpha=32)
mark_only_lora_trainable(model)
print(f"{trainable_ratio(model):.1%} params trainable")  # e.g. 0.4%
# 训练后一行合并,推理零额外延迟:
# merged = module.merge()

🧮 LoRA 一页纸(原理速览)

  • 为什么低秩有效:微调时权重更新 ΔW 的本征维度远小于参数量——任务信息是低秩的
  • 为什么 B=0 初始化:保证训练起点 = 预训练模型,不破坏已有能力
  • α/r 缩放:把更新幅度与解耦的秩超参分离,调 r 不需要重调学习率
  • QLoRA 省的是什么:NF4 存基座(~4.5 bit/权重 vs 16bit),优化器状态和梯度只存在于 LoRA 参数上
  • prompt 掩码为什么重要:不掩码 = 模型花容量学"复述问题";掩码 = 全部梯度流向答案生成

❓ 常见问题(FAQ)

Q1:QLoRA 和全参微调到底差在哪?为什么值得用? 全参微调要为所有权重保存梯度和 AdamW 优化器状态(约 2-3 倍参数量的额外显存);QLoRA 把基座冻结成 NF4(约 4.5 bit/权重),梯度和优化器状态只存在于低秩 fp32 适配器上。实测 1.5B 模型适配器只有 ~26 MB(全参要存 3 GB),7B 在 3090 24GB 上显存峰值仅 13.3 GB。

Q2:7B 为什么只要 ~55 分钟? 因为反向传播只更新 LoRA 参数(r=16),基座权重只做前向;加上 bs=2 × 梯度累积和梯度检查点,3000 条 × 2 epochs 共约 700 步。同数据下 1.5B(bs=4)只要 15.3 分钟。

Q3:能换其他基座模型吗? 可以。examples/train_qwen_gpu.py --model <HF模型名> 接受任意 transformers 支持的 causal LM(需 bitsandbytes NF4 + PEFT 支持);本仓从零实现的 lora_lab.lora.inject_lora 则能注入任何含 nn.Linear 的模型,README 快速开始里有示例。

Q4:既然"不看 PEFT 源码",为什么 examples 里用了 PEFT? 两条线各司其职:lora_lab/ 是从零实现的教学核心(数学、NF4、prompt 掩码,17 个测试逐项验证数值契约);examples/train_qwen_gpu.py 是生产级配方(transformers + PEFT + bitsandbytes),概念与本仓实现一一对应,用来在真模型上复现同一套机制。

Q5:为什么 CPU 小实验里 LoRA(-13.3%)反而赢了全参(-11.0%)? 20 条样本的小数据上,全参更新更容易过拟合——这正是 QLoRA 论文的核心论点之一。但注意模型是字符级玩具规模,"答对"来自对训练对的记忆,本实验证明的是机制与数值契约,不是涌现能力。

Q6:所有数字怎么复现? CPU 对比表:uv run python train.py(~10s);测试:uv run pytest(17 passed);1.5B QLoRA:examples/train_qwen_gpu.py --samples 3000 --epochs 2(3090 实测 15.3 min / 21.7 GB);7B:同命令加 --model Qwen/Qwen2.5-7B-Instruct --batch-size 2。结果 JSON 在 results/。

⚠️ 局限与已知问题(Limitations)

  • 仅单卡验证:所有真模型实验只在单张 RTX 3090 24GB 上跑过,未在 A100/H100/4090 或多卡(DDP/DeepSpeed)环境复现,显存与耗时不代表其他硬件的表现。
  • 模型规模与任务单一:真模型上限 7B,数据只有 Belle 数学指令 3000 条单一任务;未验证更大模型、更长序列、多语言/多任务下的表现,验证损失基于单一固定切分。
  • 仅覆盖文本 SFT:只涉及 causal LM 的 LoRA/QLoRA/SFT;不含多模态、embedding 模型微调,也没有 DPO/RLHF 等对齐训练阶段。
  • 教学实现非生产级:lora_lab/ 的 NF4 是教学简化版,没有 bitsandbytes 的分页优化器、双重量化等工程优化;生产训练请走 examples/ 的 PEFT + bitsandbytes 路线。
  • 未做系统消融与稳定性分析:超参以 r=16 为主,没有对 r/α/学习率做系统扫描;结果为单次运行、未报告多种子方差(7B 训练损失曲线波动明显,见 results/qwen-7b-3090.json 的 history)。
  • 文本生成评估是定性的:微调前后对比(examples/eval_generations.py)为 8 题贪心解码人工对照,没有 BLEU/ROUGE 等自动指标。

License

MIT

About

从零实现 LoRA/QLoRA/SFT:低秩适配数学、NF4 存储、prompt 掩码数据管线、150 行训练器,CPU 实测 LoRA vs 全参微调对比

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages