Qwen LoRA 微调实验
建议学时
4 学时。
| 课时 | 内容 | 产出 |
|---|---|---|
| 1 | 环境、数据、tokenizer 和 chat template 检查 | 数据检查记录 |
| 2 | 运行 LoRA/QLoRA smoke test | 训练日志 |
| 3 | 对比微调前后输出 | 评估表 |
| 4 | 判断是否进入合并、量化和部署验证 | 微调决策结论 |
本实验对应理论章节:
学习目标
完成本实验后,学习者应能:
- 准备一个公开安全的小型 instruction 数据集。
- 检查
messages格式、system prompt 和输出格式。 - 使用 LoRA/QLoRA 方式完成一次极小规模 Qwen 微调 smoke test。
- 保存训练日志、adapter 路径和失败记录。
- 用固定 prompt 比较微调前后输出。
- 判断微调结果是否值得继续做量化和端侧部署验证。
实验边界
本实验是教学 smoke test,不是生产训练方案。
不要承诺模型能力提升。
不要把 adapter、checkpoint、模型权重、训练缓存提交到 Git。
实验目标是让读者按步骤跑通:
- 数据格式。
- 训练配置。
- 训练命令。
- 日志记录。
- 输出对比。
- 部署判断。
推荐硬件
| 环境 | 推荐用途 |
|---|---|
| Ubuntu Server + NVIDIA GPU | 推荐训练环境 |
| 本地 Mac/CPU | 只适合读代码、检查数据、极小模型实验 |
| Jetson | 不推荐作为第一训练环境,可用于训练后部署验证 |
| 云 GPU | 适合没有本地 GPU 的学员 |
Jetson 更适合做部署验证,而不是作为本课程第一训练设备。
原因是训练显存、温度、功耗和包兼容问题会显著增加学习成本。
目录结构
建议训练相关文件放在课程仓库外部:
mkdir -p ~/edge-ai-lab/finetune/{data,outputs,logs,configs}
课程仓库里只保留教学脚本和模板:
labs/finetuning/
sample_sft_data.jsonl
train_lora_smoke.py
lora_config.example.yaml
finetuning-results-template.md
Step 0:确认执行位置
以下命令默认从课程仓库根目录执行。
pwd
test -f labs/finetuning/train_lora_smoke.py
test -f labs/finetuning/sample_sft_data.jsonl
如果 test 命令没有输出,表示文件存在。
如果不在课程仓库根目录,先进入仓库再继续。
Step 1:准备 Python 环境
以下命令是教学示例。具体版本以课堂环境和目标 GPU 为准。
mkdir -p ~/edge-ai-lab/finetune/{data,outputs,logs,configs}
python3 -m venv ~/edge-ai-lab/finetune/.venv
source ~/edge-ai-lab/finetune/.venv/bin/activate
python -m pip install --upgrade pip
安装训练依赖:
pip install "torch" "transformers" "datasets" "accelerate" "peft" "trl" "bitsandbytes"
如果 bitsandbytes 在本机不可用,可以先使用非 QLoRA 的 LoRA smoke test,或换云 GPU/Ubuntu CUDA 环境。
Step 2:检查环境
python - <<'PY'
import platform
import torch
print("python:", platform.python_version())
print("torch:", torch.__version__)
print("cuda available:", torch.cuda.is_available())
if torch.cuda.is_available():
print("gpu:", torch.cuda.get_device_name(0))
PY
如果 cuda available 是 False,仍可尝试极小模型或 CPU 路径,但应在实验记录中写清楚限制。
也可以记录 GPU 当前状态:
nvidia-smi
如果没有 NVIDIA GPU 或命令不存在,把失败原因写进日志,不要伪造 GPU 结果。
Step 3:复制样例数据
cp labs/finetuning/sample_sft_data.jsonl ~/edge-ai-lab/finetune/data/sample_sft_data.jsonl
cp labs/finetuning/lora_config.example.yaml ~/edge-ai-lab/finetune/configs/lora_config.example.yaml
检查前几行:
head -n 3 ~/edge-ai-lab/finetune/data/sample_sft_data.jsonl
每行应是一个 JSON 对象,并包含 messages。
Step 4:检查数据格式
可以先用 Python 验证 JSONL 是否可解析:
python - <<'PY'
import json
from pathlib import Path
path = Path.home() / "edge-ai-lab/finetune/data/sample_sft_data.jsonl"
for i, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
row = json.loads(line)
assert "messages" in row, f"line {i} missing messages"
roles = [m["role"] for m in row["messages"]]
assert roles[-1] == "assistant", f"line {i} last role should be assistant"
print("ok")
PY
数据检查表:
| 检查项 | 结果 | 备注 |
|---|---|---|
| JSONL 可解析 | 待填 | 待填 |
| 每行有 messages | 待填 | 待填 |
| role 顺序正确 | 待填 | 待填 |
| assistant 输出格式稳定 | 待填 | 待填 |
| 无隐私和敏感信息 | 待填 | 待填 |
| 训练/评估可切分 | 待填 | 待填 |
Step 5:检查 chat template
训练前先打印一条样本经过 tokenizer 后的文本。
python labs/finetuning/train_lora_smoke.py \
--model Qwen/Qwen2.5-0.5B-Instruct \
--data ~/edge-ai-lab/finetune/data/sample_sft_data.jsonl \
--output ~/edge-ai-lab/finetune/outputs/template-check \
--print-sample
检查点:
| 检查项 | 结果 |
|---|---|
| system 指令出现在模板中 | 待填 |
| user/assistant 边界清楚 | 待填 |
| assistant 答案没有被截断 | 待填 |
| 与后续推理 prompt 格式一致 | 待填 |
如果这里就不符合预期,先修数据或 tokenizer 配置,不要进入训练。
Step 6:运行训练 smoke test
先不要直接跑长训练。
使用极小步数验证流程:
python labs/finetuning/train_lora_smoke.py \
--model Qwen/Qwen2.5-0.5B-Instruct \
--data ~/edge-ai-lab/finetune/data/sample_sft_data.jsonl \
--output ~/edge-ai-lab/finetune/outputs/qwen-lora-smoke \
--max-steps 5 \
--max-seq-length 512 \
2>&1 | tee ~/edge-ai-lab/finetune/logs/qwen-lora-smoke.log
如果机器显存不足,可以先降低:
--max-seq-length- batch size
- 模型尺寸
如果依赖无法安装,记录失败原因,不要跳过日志。
训练后检查 adapter 是否保存:
test -d ~/edge-ai-lab/finetune/outputs/qwen-lora-smoke/adapter
find ~/edge-ai-lab/finetune/outputs/qwen-lora-smoke/adapter -maxdepth 1 -type f
tail -n 20 ~/edge-ai-lab/finetune/logs/qwen-lora-smoke.log
Step 7:记录训练日志
训练日志至少记录:
| 字段 | 值 |
|---|---|
| base model | 待填 |
| dataset path | 待填 |
| sample count | 待填 |
| max steps | 待填 |
| max seq length | 待填 |
| LoRA rank | 待填 |
| learning rate | 待填 |
| peak VRAM/RAM | 待填 |
| adapter path | 待填 |
| log path | 待填 |
不要只写“训练成功”。
要保留能复查的命令和日志路径。
Step 8:固定 prompt 对比
选择 3-5 个固定 prompt。
至少包含:
- 训练集中相似任务。
- 训练集中没有出现但同类型任务。
- JSON 输出任务。
- 量化/部署领域解释任务。
记录表:
| Prompt ID | Prompt | 基座输出 | 微调后输出 | 是否更符合任务 | 问题 |
|---|---|---|---|---|---|
| P1 | 待填 | 待填 | 待填 | 待填 | 待填 |
| P2 | 待填 | 待填 | 待填 | 待填 | 待填 |
| P3 | 待填 | 待填 | 待填 | 待填 | 待填 |
可以用下面脚本做基座和 adapter 的最小推理对比。
先测试基座:
python - <<'PY'
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
model_name = "Qwen/Qwen2.5-0.5B-Instruct"
prompt = "请输出 JSON,总结 Q4 量化的两个风险。"
messages = [
{"role": "system", "content": "你是端侧模型部署课程助教。回答要简洁、可操作。"},
{"role": "user", "content": prompt},
]
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
inputs = tokenizer.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
return_tensors="pt",
)
model = AutoModelForCausalLM.from_pretrained(
model_name,
trust_remote_code=True,
device_map="auto",
)
outputs = model.generate(
inputs.to(model.device),
max_new_tokens=128,
do_sample=False,
)
print(tokenizer.decode(outputs[0][inputs.shape[-1]:], skip_special_tokens=True))
PY
再测试 adapter:
python - <<'PY'
import torch
from pathlib import Path
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
model_name = "Qwen/Qwen2.5-0.5B-Instruct"
adapter_path = Path("~/edge-ai-lab/finetune/outputs/qwen-lora-smoke/adapter").expanduser()
prompt = "请输出 JSON,总结 Q4 量化的两个风险。"
messages = [
{"role": "system", "content": "你是端侧模型部署课程助教。回答要简洁、可操作。"},
{"role": "user", "content": prompt},
]
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
inputs = tokenizer.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
return_tensors="pt",
)
base = AutoModelForCausalLM.from_pretrained(
model_name,
trust_remote_code=True,
device_map="auto",
)
model = PeftModel.from_pretrained(base, str(adapter_path))
outputs = model.generate(
inputs.to(model.device),
max_new_tokens=128,
do_sample=False,
)
print(tokenizer.decode(outputs[0][inputs.shape[-1]:], skip_special_tokens=True))
PY
这两个命令可能下载模型并占用较多内存。失败时记录错误,不要把失败删除。
Step 9:判断是否继续
完成 smoke test 后,不要直接进入大规模训练。
先判断:
| 问题 | 如果答案是“否” |
|---|---|
| 数据格式是否稳定? | 先修数据 |
| 微调后目标格式是否改善? | 先调数据和 prompt |
| 基座能力是否足够? | 考虑换模型 |
| 显存和训练时间是否可接受? | 调小模型或用 QLoRA |
| 是否会影响端侧部署? | 继续量化和 profiling |
只有当微调确实改善目标任务,才进入下一步:
失败排查
无法下载模型
- 检查网络和 Hugging Face 访问。
- 检查模型名称是否正确。
- 如果模型需要授权,按模型页面要求处理。
CUDA 或 bitsandbytes 不可用
- 检查
nvidia-smi。 - 检查 PyTorch CUDA 版本。
- 先运行非 QLoRA LoRA smoke test。
- 换 Ubuntu CUDA 环境或云 GPU。
OOM
- 降低
max_seq_length。 - 降低 batch size。
- 使用 gradient accumulation。
- 换更小模型。
- 尝试 QLoRA。
loss 很快降低但输出没有改善
- 训练集太小或重复。
- prompt 与部署 prompt 不一致。
- 输出格式样例不稳定。
- 评估 prompt 太少。
微调后 JSON 更差
- 训练样本里 JSON 不合法。
- assistant 输出有多余解释。
- system prompt 不一致。
- 评估时采样温度过高。
验收结果
| 产物 | 验收标准 |
|---|---|
| 环境检查 | Python、Torch、CUDA 或限制说明已记录 |
| 数据检查表 | 已完成并说明问题 |
| chat template 检查 | 至少打印 1 条样本并确认格式 |
| 训练日志 | 有命令、step、loss 或失败原因 |
| adapter/checkpoint | 保存到仓库外路径 |
| 对比输出 | 至少 3 个 prompt |
| 继续/停止判断 | 能说明是否值得后续量化部署 |
作业
- 把样例数据扩展到 20 条,保持同一输出格式。
- 打印 1 条样本的 chat template 文本,说明角色边界是否正确。
- 跑一次 5-step smoke test,保存日志。
- 用 3 个 prompt 比较基座和微调后输出。
- 写一段结论:微调是否值得进入量化部署阶段。