LoRA 微调实战笔记:从 Mac 到 Linux NVIDIA GPU

10/10/2026
6 min read
45
AITrainingSFTLora

一、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 微调的完整流程可以概括为:

auto
准备基础模型
    ↓
准备和清洗训练数据
    ↓
划分训练集、验证集和测试集
    ↓
配置 LoRA 参数
    ↓
加载模型与 Tokenizer
    ↓
执行监督微调 SFT
    ↓
保存 LoRA Adapter
    ↓
加载 Adapter 进行推理
    ↓
对比基础模型和微调模型

本笔记以一个防御性代码审查助手为例,让模型学习识别代码中的潜在安全问题,并输出修复建议。

建议先使用小模型跑通流程,再根据实际资源和评估结果升级模型规模。

三、准备训练数据

3.1 数据格式

本示例采用 JSONL 格式,每行保存一条独立的 JSON 数据,并使用 messages 表示对话内容。

创建项目目录:

auto
mkdir -p lora-lab/{data,scripts,mac,linux,outputs}
cd lora-lab

项目结构如下:

auto
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:

json
{"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:

python
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} 条数据,格式正确")

执行:

bash
python scripts/check_data.py

数据校验通过后,再开始训练。

四、Mac Apple Silicon 环境:使用 MLX-LM

Mac 上可以使用 MLX-LM 进行 LoRA 或 QLoRA 微调。该框架针对 Apple Silicon 进行了优化。

4.1 创建 Python 环境

检查架构:

bash
uname -m
python3 --version

Apple Silicon 通常输出 arm64。

创建虚拟环境:

auto
cd lora-lab

python3 -m venv .venv
source .venv/bin/activate

python -m pip install --upgrade pip

4.2 安装 MLX-LM

bash
python -m pip install "mlx-lm[train]"

检查命令是否可用:

bash
mlx_lm.lora --help

如果出现依赖错误,先检查 Python 版本、当前虚拟环境和 MLX-LM 的兼容性。

4.3 选择基础模型

入门时可以使用:

mlx-community/Qwen2.5-0.5B-Instruct-4bit

这是一个小型指令模型的 MLX 量化版本,适合用来验证数据、训练和推理流程。

第一次运行时,需要下载模型文件。

4.4 开始训练

确保 data/ 下存在训练集和验证集,执行:

auto
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:

auto
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)

运行:

auto
python mac/infer.py

推理流程包括加载基础模型、加载 Adapter、构造聊天模板和生成文本。

五、Linux NVIDIA GPU 环境:使用 PyTorch + TRL + PEFT

Linux 环境使用 Hugging Face 生态进行微调,适合 NVIDIA GPU 服务器。

5.1 检查 NVIDIA GPU

执行:

auto
nvidia-smi
python3 --version

nvidia-smi 应能够显示 GPU 型号、显存和驱动信息。

5.2 创建虚拟环境

auto
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/

安装完成后验证:

auto
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 安装微调依赖

auto
python -m pip install \
  transformers \
  datasets \
  peft \
  trl \
  accelerate \
  bitsandbytes

验证依赖:

auto
python -c "import transformers, datasets, peft, trl, bitsandbytes; print('依赖安装成功')"

5.5 编写训练代码

创建 linux/train.py:

auto
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=8LoRA 低秩矩阵的秩
lora_alpha=16LoRA 更新的缩放参数
lora_dropout=0.05LoRA 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 启动训练

在项目根目录运行:

auto
python scripts/check_data.py
python linux/train.py

训练完成后,Adapter 通常保存在:

auto
outputs/linux-lora/

该目录保存的是微调参数,而不一定是完整的基础模型权重。

5.8 加载 Adapter 进行推理

创建 linux/infer.py:

auto
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))

执行:

auto
python linux/infer.py

推理时需要同时使用基础模型和 Adapter。AutoPeftModelForCausalLM 会根据 Adapter 配置加载相应的基础模型与微调参数。

六、如何评估微调效果?

训练 Loss 下降不代表模型能力一定提升,需要使用独立测试集进行比较。

建议使用同一组测试问题,分别运行基础模型和微调模型,并记录以下指标:

指标评估内容
JSON 解析成功率输出格式是否正确
问题识别正确率是否识别真实存在的问题
修复建议正确率修复方法是否合理
误报率是否将正常代码错误判断为问题
推理耗时生成回答所需时间

JSON 格式检查示例

auto
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

可以尝试:

  1. 减小 per_device_train_batch_size。

  2. 减小 max_length,同时确认关键答案没有被截断。

  3. 使用 QLoRA。

  4. 开启梯度检查点。

  5. 使用更小的基础模型。

7.3 训练完成但输出没有改善

常见原因:

  • 训练数据太少。

  • 训练样本质量不高。

  • 模型过拟合。

  • 推理时没有正确加载 Adapter。

  • 评估集与训练集存在较大差异。

应优先检查数据质量、Adapter 加载情况和独立测试结果。

八、实战总结

LoRA 微调的关键步骤可以归纳为:

  1. 准备基础模型和训练环境。

  2. 构建高质量 JSONL 训练数据。

  3. 划分训练集、验证集和测试集。

  4. 配置 LoRA 参数和训练超参数。

  5. 使用 MLX-LM 或 PyTorch + TRL + PEFT 执行训练。

  6. 保存 Adapter,并在推理阶段加载。

  7. 使用独立测试集比较微调前后的效果。

Mac 方案适合利用 Apple Silicon 快速开展小规模实验;Linux NVIDIA GPU 方案则适合进一步开展量化训练、扩大模型规模和搭建标准训练流程。

最终目标不是单纯让训练程序运行成功,而是通过可靠的数据和评估,确认 LoRA 微调确实改善了目标任务的表现。

Thanks for reading!

Comments

Please sign in to join the conversation.

Loading content...