LoRA 微调实战笔记:从 Mac 到 Linux NVIDIA GPU
一、LoRA 是什么?
LoRA(Low-Rank Adaptation,低秩适配)是一种参数高效微调(PEFT)方法。它通过冻结预训练模型的大部分参数,只训练新增的低秩矩阵,让模型学习特定任务。
传统全量微调需要更新模型的大量参数,而 LoRA 主要更新少量新增参数,因此能够降低训练所需的显存、计算量和存储成本。
1.1 核心原理
假设原始权重矩阵为 \(W\),LoRA 不直接更新整个矩阵,而是学习一个增量矩阵:
\[ W'=W+\Delta W \]
其中:
\[ \Delta W=BA \]
-
\(W\):预训练模型的原始权重。
-
\(A、B\):新增的可训练矩阵。
-
\(\Delta W\):LoRA 学习到的权重变化。
-
\(W'\):应用 LoRA 后的有效权重。
通过低秩分解,LoRA 可以减少需要训练的参数数量。
1.2 LoRA 与 QLoRA
| 方式 | 基础模型 | 训练参数 |
|---|---|---|
| Full Fine-Tuning | 通常使用完整模型权重 | 更新大部分或全部参数 |
| LoRA | 通常以普通或较低精度加载 | 主要更新 LoRA 参数 |
| QLoRA | 使用量化后的基础模型 | 主要更新 LoRA 参数 |
QLoRA 通常使用 4-bit 量化加载基础模型,以降低显存占用,再训练 LoRA Adapter。
需要注意,量化并不会消除所有训练开销,激活值、序列长度和其他训练状态仍然需要显存。
二、微调项目的整体流程
LoRA 微调的完整流程可以概括为:
准备基础模型
↓
准备和清洗训练数据
↓
划分训练集、验证集和测试集
↓
配置 LoRA 参数
↓
加载模型与 Tokenizer
↓
执行监督微调 SFT
↓
保存 LoRA Adapter
↓
加载 Adapter 进行推理
↓
对比基础模型和微调模型
本笔记以一个防御性代码审查助手为例,让模型学习识别代码中的潜在安全问题,并输出修复建议。
建议先使用小模型跑通流程,再根据实际资源和评估结果升级模型规模。
三、准备训练数据
3.1 数据格式
本示例采用 JSONL 格式,每行保存一条独立的 JSON 数据,并使用 messages 表示对话内容。
创建项目目录:
mkdir -p lora-lab/{data,scripts,mac,linux,outputs}
cd lora-lab
项目结构如下:
lora-lab/
├── data/
│ ├── train.jsonl
│ ├── valid.jsonl
│ └── test.jsonl
├── scripts/
│ └── check_data.py
├── mac/
│ └── infer.py
├── linux/
│ ├── train.py
│ └── infer.py
└── outputs/
3.2 训练数据示例
创建 data/train.jsonl:
{"messages":[{"role":"system","content":"你是一名防御性代码审查助手。分析代码中的安全风险,解释原因并给出修复建议。只分析代码,不执行代码。输出 JSON,字段为 issue、severity、reason、fix。"},{"role":"user","content":"审查代码:query = \"SELECT * FROM users WHERE name = '\" + username + \"'\""},{"role":"assistant","content":"{\"issue\":\"SQL 查询使用字符串拼接\",\"severity\":\"high\",\"reason\":\"用户输入可能改变 SQL 查询的结构。\",\"fix\":\"使用参数化查询,将 SQL 语句与用户输入分离。\"}"}]}
{"messages":[{"role":"system","content":"你是一名防御性代码审查助手。分析代码中的安全风险,解释原因并给出修复建议。只分析代码,不执行代码。输出 JSON,字段为 issue、severity、reason、fix。"},{"role":"user","content":"审查代码:const token = 'example-placeholder';"},{"role":"assistant","content":"{\"issue\":\"凭据硬编码在源码中\",\"severity\":\"medium\",\"reason\":\"真实凭据可能通过代码仓库或构建产物泄露。\",\"fix\":\"从环境变量或密钥管理服务读取凭据;如果真实凭据已泄露,应及时轮换。\"}"}]}
{"messages":[{"role":"system","content":"你是一名防御性代码审查助手。分析代码中的安全风险,解释原因并给出修复建议。只分析代码,不执行代码。输出 JSON,字段为 issue、severity、reason、fix。"},{"role":"user","content":"审查代码:app.get('/profile', (req, res) => { res.send('<h1>' + req.query.name + '</h1>') })"},{"role":"assistant","content":"{\"issue\":\"用户输入直接拼接到 HTML\",\"severity\":\"medium\",\"reason\":\"未经适当处理的输入可能被浏览器解释为 HTML。\",\"fix\":\"优先使用具有自动转义功能的模板系统;确实需要 HTML 时,使用经过审查的清理方案。\"}"}]}
上面的数据仅用于演示格式和训练流程,不足以训练出可靠的生产模型。
3.3 训练集、验证集和测试集
-
train.jsonl:用于更新模型参数。 -
valid.jsonl:用于观察训练效果、调整超参数。 -
test.jsonl:用于训练结束后的独立评估。
验证集和测试集应尽量与训练集独立,避免重复或高度相似的样本造成评估结果虚高。
3.4 检查数据格式
创建 scripts/check_data.py:
import jsonfrom pathlib
import Path
for filename in ("train.jsonl", "valid.jsonl", "test.jsonl"):
path = Path("data") / filename
if not path.exists():
raise FileNotFoundError(path)
count = 0
with path.open(encoding="utf-8") as file:
for line_number, line in enumerate(file, start=1): if not line.strip(): raise ValueError( f"{filename}:{line_number} 存在空行" ) sample = json.loads(line) messages = sample.get("messages") if not isinstance(messages, list) or not messages: raise ValueError( f"{filename}:{line_number} 缺少 messages" ) for message in messages: if message.get("role") not in ( "system", "user", "assistant" ): raise ValueError( f"{filename}:{line_number} role 不合法" ) if not isinstance(message.get("content"), str): raise ValueError( f"{filename}:{line_number} content 不合法" ) count += 1 print(f"{filename}: {count} 条数据,格式正确")
执行:
python scripts/check_data.py
数据校验通过后,再开始训练。
四、Mac Apple Silicon 环境:使用 MLX-LM
Mac 上可以使用 MLX-LM 进行 LoRA 或 QLoRA 微调。该框架针对 Apple Silicon 进行了优化。
4.1 创建 Python 环境
检查架构:
uname -m
python3 --version
Apple Silicon 通常输出 arm64。
创建虚拟环境:
cd lora-lab
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
4.2 安装 MLX-LM
python -m pip install "mlx-lm[train]"
检查命令是否可用:
mlx_lm.lora --help
如果出现依赖错误,先检查 Python 版本、当前虚拟环境和 MLX-LM 的兼容性。
4.3 选择基础模型
入门时可以使用:
mlx-community/Qwen2.5-0.5B-Instruct-4bit
这是一个小型指令模型的 MLX 量化版本,适合用来验证数据、训练和推理流程。
第一次运行时,需要下载模型文件。
4.4 开始训练
确保 data/ 下存在训练集和验证集,执行:
mlx_lm.lora \
--model mlx-community/Qwen2.5-0.5B-Instruct-4bit \
--train \
--data ./data \
--adapter-path ./outputs/mac-lora \
--iters 100 \
--batch-size 1 \
--learning-rate 1e-5
主要参数:
| 参数 | 作用 |
|---|---|
| --model | 指定基础模型 |
| --train | 开启训练 |
| --data | 指定数据目录 |
| --adapter-path | 指定 Adapter 输出目录 |
| --iters | 训练迭代次数 |
| --batch-size | 每个微批次的样本数 |
| --learning-rate | 学习率 |
这里的 100 次迭代用于验证流程,并不代表模型一定训练充分。具体参数以本机 mlx_lm.lora --help 和对应版本的官方文档为准。
4.5 加载微调模型进行推理
创建 mac/infer.py:
from mlx_lm import load, generateMODEL = "mlx-community/Qwen2.5-0.5B-Instruct-4bit"ADAPTER = "./outputs/mac-lora"model, tokenizer = load( MODEL, adapter_path=ADAPTER,)messages = [ { "role": "system", "content": ( "你是一名防御性代码审查助手。" "识别安全问题并给出修复建议。" "只分析代码,不执行代码。" "输出 JSON。" ), }, { "role": "user", "content": ( "审查代码:" "const query = \"SELECT * FROM users WHERE id = \" + userId" ), },]prompt = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True,)result = generate( model, tokenizer, prompt=prompt, max_tokens=256,)print(result)
运行:
python mac/infer.py
推理流程包括加载基础模型、加载 Adapter、构造聊天模板和生成文本。
五、Linux NVIDIA GPU 环境:使用 PyTorch + TRL + PEFT
Linux 环境使用 Hugging Face 生态进行微调,适合 NVIDIA GPU 服务器。
5.1 检查 NVIDIA GPU
执行:
nvidia-smi
python3 --version
nvidia-smi 应能够显示 GPU 型号、显存和驱动信息。
5.2 创建虚拟环境
cd lora-lab
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
5.3 安装 PyTorch
从 PyTorch 官方安装页面选择适合当前系统和 CUDA 环境的安装命令:
https://pytorch.org/get-started/locally/
安装完成后验证:
python -c "import torch; print(torch.__version__); print(torch.cuda.is_available()); print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'GPU 不可用')"
确认 torch.cuda.is_available() 返回 True,再继续安装微调依赖。
5.4 安装微调依赖
python -m pip install \
transformers \
datasets \
peft \
trl \
accelerate \
bitsandbytes
验证依赖:
python -c "import transformers, datasets, peft, trl, bitsandbytes; print('依赖安装成功')"
5.5 编写训练代码
创建 linux/train.py:
import torchif tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_tokentokenizer.padding_side = "right"# 2. 配置 4-bit 量化quant_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_use_double_quant=True, bnb_4bit_compute_dtype=compute_dtype,)# 3. 加载基础模型model = AutoModelForCausalLM.from_pretrained( MODEL_NAME, quantization_config=quant_config, torch_dtype=compute_dtype, device_map={"": 0},)model.config.use_cache = False# 4. 加载训练集和验证集dataset = load_dataset( "json", data_files={ "train": f"{DATA_DIR}/train.jsonl", "validation": f"{DATA_DIR}/valid.jsonl", },)# 5. 配置 LoRAlora_config = LoraConfig( task_type="CAUSAL_LM", r=8, lora_alpha=16, lora_dropout=0.05, target_modules=[ "q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj", ],)# 6. 配置训练参数training_config = SFTConfig( output_dir=OUTPUT_DIR, num_train_epochs=3, per_device_train_batch_size=1, per_device_eval_batch_size=1, gradient_accumulation_steps=4, learning_rate=1e-4, logging_steps=1, eval_strategy="epoch", save_strategy="epoch", save_total_limit=2, max_length=1024, packing=False, gradient_checkpointing=True, bf16=use_bf16, fp16=not use_bf16, report_to="none", seed=42,)# 7. 创建训练器
这份代码展示了常见的 QLoRA + SFT 训练方式。不同版本的 Transformers 和 TRL 可能存在参数差异,遇到接口报错时应先检查实际安装版本。
5.6 关键训练参数
| 参数 | 含义 |
|---|---|
| r=8 | LoRA 低秩矩阵的秩 |
| lora_alpha=16 | LoRA 更新的缩放参数 |
| lora_dropout=0.05 | LoRA Dropout |
| learning_rate=1e-4 | 学习率 |
| num_train_epochs=3 | 遍历训练集三轮 |
| per_device_train_batch_size=1 | 每张 GPU 每个微批次处理一条样本 |
| gradient_accumulation_steps=4 | 每四个微批次累积梯度后更新参数 |
| max_length=1024 | 训练序列的最大长度 |
在单卡训练中,示例配置对应的有效批量大小为:
\[ B_{\text{effective}} = B_{\text{device}}\times G\times D \]
\[ B_{\text{effective}}=1\times4\times1=4 \]
其中 \(G\) 是梯度累积步数,\(D\) 是 GPU 数量。
梯度累积可以减少每个微批次的显存需求,但不能完全消除单次前向和反向传播所需的显存。
5.7 启动训练
在项目根目录运行:
python scripts/check_data.py
python linux/train.py
训练完成后,Adapter 通常保存在:
outputs/linux-lora/
该目录保存的是微调参数,而不一定是完整的基础模型权重。
5.8 加载 Adapter 进行推理
创建 linux/infer.py:
import torchfrom peft import AutoPeftModelForCausalLMfrom transformers import AutoTokenizerADAPTER_PATH = "./outputs/linux-lora"if not torch.cuda.is_available(): raise RuntimeError("没有可用的 CUDA GPU")dtype = ( torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16)tokenizer = AutoTokenizer.from_pretrained(ADAPTER_PATH)model = AutoPeftModelForCausalLM.from_pretrained( ADAPTER_PATH, torch_dtype=dtype, device_map="auto",)model.eval()messages = [ { "role": "system", "content": ( "你是一名防御性代码审查助手。" "识别安全问题并给出修复建议。" "只分析代码,不执行代码。" "输出 JSON。" ), }, { "role": "user", "content": ( "审查代码:" "const query = \"SELECT * FROM users WHERE id = \" + userId" ), },]prompt = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True,)inputs = tokenizer( prompt, return_tensors="pt",).to(model.device)with torch.inference_mode(): outputs = model.generate( **inputs, max_new_tokens=256, do_sample=False, pad_token_id=tokenizer.pad_token_id, )new_tokens = outputs[0][inputs["input_ids"].shape[1]:]print(tokenizer.decode(new_tokens, skip_special_tokens=True))
执行:
python linux/infer.py
推理时需要同时使用基础模型和 Adapter。AutoPeftModelForCausalLM 会根据 Adapter 配置加载相应的基础模型与微调参数。
六、如何评估微调效果?
训练 Loss 下降不代表模型能力一定提升,需要使用独立测试集进行比较。
建议使用同一组测试问题,分别运行基础模型和微调模型,并记录以下指标:
| 指标 | 评估内容 |
|---|---|
| JSON 解析成功率 | 输出格式是否正确 |
| 问题识别正确率 | 是否识别真实存在的问题 |
| 修复建议正确率 | 修复方法是否合理 |
| 误报率 | 是否将正常代码错误判断为问题 |
| 推理耗时 | 生成回答所需时间 |
JSON 格式检查示例
import jsondef is_valid_json(text): try: json.loads(text) return True except json.JSONDecodeError: return False
该函数只能检查 JSON 语法,不能证明模型给出的分析结论正确。实际评估还需要检查字段、内容和修复建议。
七、常见问题排查
7.1 Loss 出现 NaN
可能原因:
-
学习率过高。
-
数据存在异常或标签不正确。
-
混合精度计算不稳定。
-
Loss 掩码或训练配置错误。
排查时应先检查数据和标签,再检查学习率、精度及梯度稳定性。
7.2 CUDA Out of Memory
可以尝试:
-
减小
per_device_train_batch_size。 -
减小
max_length,同时确认关键答案没有被截断。 -
使用 QLoRA。
-
开启梯度检查点。
-
使用更小的基础模型。
7.3 训练完成但输出没有改善
常见原因:
-
训练数据太少。
-
训练样本质量不高。
-
模型过拟合。
-
推理时没有正确加载 Adapter。
-
评估集与训练集存在较大差异。
应优先检查数据质量、Adapter 加载情况和独立测试结果。
八、实战总结
LoRA 微调的关键步骤可以归纳为:
-
准备基础模型和训练环境。
-
构建高质量 JSONL 训练数据。
-
划分训练集、验证集和测试集。
-
配置 LoRA 参数和训练超参数。
-
使用 MLX-LM 或 PyTorch + TRL + PEFT 执行训练。
-
保存 Adapter,并在推理阶段加载。
-
使用独立测试集比较微调前后的效果。
Mac 方案适合利用 Apple Silicon 快速开展小规模实验;Linux NVIDIA GPU 方案则适合进一步开展量化训练、扩大模型规模和搭建标准训练流程。
最终目标不是单纯让训练程序运行成功,而是通过可靠的数据和评估,确认 LoRA 微调确实改善了目标任务的表现。
Thanks for reading!
Related Articles
Comments
Please sign in to join the conversation.
Loading content...