项目 / Projects
Unsloth 高效微调实战:用 Qwen3.5-2B 跑通 Alpaca 微调(有监督微调)
用 16-bit LoRA + TRL SFTTrainer 在 Qwen3.5-2B 上跑通 Alpaca 指令微调,并在工程上做了可复现、跨平台与量化评估的改进;4090 实测 PPL 从 6.76 降到 3.08(↓54%)。
背景与目标
大语言模型全参微调需要更新全部权重,显存与算力开销极大:2B 模型全参 bf16 训练,仅优化器状态(AdamW 的 m/v)就能吃掉约 16GB,单卡基本跑不动。痛点是中小团队 / 单卡 / Colab 用户想「用自己的一条指令数据把模型调教成会按格式回答」,却被显存和训练时长挡在门外。
本篇要解决的问题:用最低成本把 Qwen3.5-2B 这个开源小模型,通过 Alpaca 指令数据教会它「理解 instruction + input → 产出 output」的对话 / 指令范式,且能在 8GB 起步的显卡上跑起来。
我们选择 Unsloth 作为微调底座,和它对比 LLaMA-Factory、XTuner 后的定位一致——三件套分工不同,而 Unsloth 的卖点正好命中上述痛点:
| 维度 | LLaMA-Factory | XTuner | Unsloth(本文) |
|---|---|---|---|
| 上手方式 | WebUI / CLI | 配置文件驱动 | Notebook / 少量 Python |
| 最大优势 | 功能全家桶 | 可深度定制 | 速度 + 显存 |
| 适合人群 | 业务快速落地 | 算法工程师 | 单卡 / Colab / 快速验证 |
Unsloth 官方数据:微调速度约快 2~5 倍,显存可降 50%~80%;2B 的 bf16 LoRA 约 5GB 显存即可起步。本工程实测:RTX 4060(8GB)纯文本 SFT 跑满约 7.4GB;RTX 4090(24GB)batch=16 仅约 9.3GB——与「2~5 倍 / 省显存」定位一致。
模型:Qwen/Qwen3.5-2B,2B 量级、开源、中文友好、社区生态全。数据:yahma/alpaca-cleaned,Alpaca-52k 清理版,约 5 万条 instruction/input/output 三元组,是 SFT 入门的标杆数据集。方式:16-bit LoRA + HuggingFace TRL 的 SFTTrainer。
复现前必须先记住两个硬坑:
- Qwen3.5 不推荐 4-bit QLoRA(量化误差偏大)→ 本工程走 bf16 / 16-bit LoRA,不用 4-bit。
- 需要 transformers v5(旧版直接报错)→ 我们锁定
transformers==5.5.0。
复现方案与环境
文章用 conda + AutoDL。我们落地为两条等价路径,关键点都写进了 requirements.txt:
# 1) 建环境(Python 3.11,与已验证链路一致)
conda create -n unsloth python=3.11 -y
conda activate unsloth
# 2) 装依赖(一条命令,torch CUDA 源 + triton 平台分支已内置)
pip install -r requirements.txt
requirements.txt 首行固化了两件事,避免再踩坑:
--extra-index-url https://download.pytorch.org/whl/cu126→ 让torch==2.11.0+cu126能从 PyTorch 官方源解析(PyPI 上只有无后缀版本)。triton-windows==3.7.1.post27; platform_system == "Windows"/triton==3.6.0; platform_system != "Windows"→ 跨平台自动选对 triton(Linux 上必须等于 torch 依赖的 3.6.0)。
模型与数据集用 HuggingFace 源下载(文章用 ModelScope + load_dataset),代码在 scripts/01_download.py,路径统一收口到 common.py:
PROJECT_ROOT = Path(__file__).resolve().parent.parent # scripts/ 的上一级
MODEL_BASE = PROJECT_ROOT / "models" / "Qwen" / "Qwen3.5-2B"
DATASET_DIR = PROJECT_ROOT / "datasets" / "yahma" / "alpaca-cleaned"
关键设计:PROJECT_ROOT 靠 __file__ 定位,不依赖工作目录(cwd)。所以无论在本地还是 /wh/unsloth/ 路径下执行,模型 / 数据 / 输出都能稳定落在项目根——训练日志里 lora_model/ 自动落到 /wh/unsloth/lora_model/ 就是这个机制在生效。
主线工程结构:环境安装 → 下载模型/数据 → LoRA 微调 → 推理验证 → 评估 → 保存/导出(GGUF / vLLM),对应 scripts/01~06 + common.py。
微调实现要点
加载模型(对应 scripts/03_train.py):dtype=None 让 Unsloth 自动选 bfloat16(4090 是 Ampere+,实测 Bfloat16 = TRUE),load_in_16bit=True + load_in_4bit=False 走 16-bit LoRA,避开 4-bit 量化误差坑。
挂上 LoRA:r=16, lora_alpha=16, lora_dropout=0, bias="none", use_gradient_checkpointing="unsloth", random_state=3407。要点是只训约 0.49% 参数——这就是 LoRA「高效」的本质:冻结基座,只更新低秩适配矩阵。use_gradient_checkpointing="unsloth" 前向不存中间激活、反向重算,是 4090 batch=16 才 9.3GB 的主因。
数据格式化(对应 common.py 的 format_alpaca):按模板拼 instruction / input / output,末尾必须加 EOS_TOKEN,否则生成容易停不下来。训练前 dataset.map(format_alpaca, batched=True) 得到 text 字段供 SFTTrainer 使用。
SFTTrainer 训练:忠实复现文章默认值(packing=False、max_steps=60 仅演示),正式训练改用 --num_epochs(本工程用了 3)。相对文章的工程化改进(不影响复现):seed/data_seed=3407 显式传入使训练可复现;输出目录默认锚定项目根;packing 做成可选参数(--packing True 可提速 2~3×,用短样本拼满 2048 窗口减少 padding 浪费)。
注意:文章演示用
max_steps=60。正式训练应改num_epochs(如本工程的 3)。packing=True时每个 step 处理多条打包样本,epoch 按 packed 序列计,所以 3 epoch 只跑 609 步就完成——这是正常且更快的,不是参数错位。
推理验证
文章用 FastLanguageModel.for_inference(model) 启用 2× 推理加速。本工程 04_inference.py 加载 lora_model/,按同模板组装输入,用 TextStreamer 流式生成;但未用 for_inference(),改用 model.eval()——原因:for_inference 的融合 kernel 在 RTX 4060(sm_89)首次编译会触发 ptxas 崩溃,4090 上虽可开,但为跨卡稳健统一用 eval() 路径。
保存与导出
scripts/05_export.py 由 --only 选择:lora / merged / gguf / all,覆盖文章给的三种出口:
| 用途 | 本项目命令 |
|---|---|
| 只留 LoRA 适配器 | 训练结束自动存 lora_model/ |
| 合并 16-bit(给 vLLM) | python scripts/05_export.py --only merged |
| 给 Ollama / llama.cpp | python scripts/05_export.py --only gguf --quant q5_k_m |
导出目录踩坑点:save_pretrained_gguf 会先生成中间全精度目录 qwen35_2b_alpaca_gguf/,再把最终量化结果放到 qwen35_2b_alpaca_gguf_gguf/。跑「merged + gguf」两步会看到三个目录——qwen35_2b_alpaca_merged/(给 vLLM,有用)、qwen35_2b_alpaca_gguf/(中间产物,可删,省约 4GB)、qwen35_2b_alpaca_gguf_gguf/(最终 GGUF,给 Ollama,有用)。
评估:微调到底有没有效
文章未给评估脚本。本工程补了 06_eval.py:用 CrossEntropyLoss(reduction="none") 对真实 token 求平均 loss,再算困惑度 PPL = exp(avg_loss),公平对比「基座 vs LoRA」。4090 实测(2000 样本 / seq 1024 / 同 token 数 398717,配对对照):
| 模式 | avg_loss | PPL |
|---|---|---|
| BASE(基座) | 1.9117 | 6.7648 |
| LoRA(微调后) | 1.1246 | 3.0791 |
结论:PPL 从 6.76 降到 3.08(↓54%),avg_loss 从 1.91 降到 1.12(↓41%)→ 微调明确有效。eval loss(1.12)只比 train loss(0.96)高一点点 → 无过拟合,3 epoch 恰到好处(不是太少,加 epoch 收益很小且有过拟合风险)。一并对照:本机 8GB 上 baseline 曾测得 PPL≈6.34,与 6.76 同区间,互相印证。
工程化改进小结
在「忠实复现文章」的基础上,落地的代码还补了这些(都不改变文章行为,只是更稳、可复现、跨平台):
| 项 | 改进 | 对应文件 |
|---|---|---|
| 公共模块 | 路径常量 + resolve_model_dir + format_alpaca 抽进 common.py,多脚本复用 |
common.py |
| 路径稳健 | 输出目录默认锚定 PROJECT_ROOT,不依赖 cwd(/wh/unsloth/ 路径下也自动生效) |
common.py / 03 / 05 |
| 可复现 | 显式 seed/data_seed=3407 |
03_train.py |
| 跨平台依赖 | requirements.txt 内置 torch CUDA 源 + triton 平台标记(PEP 508) |
requirements.txt |
| 推理稳健 | 去掉 for_inference(),改 model.eval()(规避 4060 ptxas 崩溃) |
04_inference.py |
| 量化评估 | 新增 PPL 评估,给出「有效性硬指标」 | 06_eval.py |
| 可选提速 | packing 做成参数(默认 False 保复现,可开 True) |
03_train.py |
一句话回顾
- 准备:环境(conda + 内置 CUDA 源的
requirements.txt)→ 下载 Qwen3.5-2B + alpaca-cleaned(HF 源)。 - 加载:
FastLanguageModel.from_pretrained(load_in_16bit=True),bf16、不走 4-bit。 - 挂 LoRA:
get_peft_model(r=16, alpha=16, dropout=0, grad_ckpt="unsloth"),只训 0.49% 参数。 - 格式化:Alpaca 模板拼 instruction/input/output +
EOS_TOKEN,成text字段。 - 训练:
SFTTrainer+SFTConfig(batch 2 / grad_accum 4 / lr 2e-4 / warmup 5 / seed 3407),3 epoch(packing 提速)。 - 验证:推理 sanity check + PPL 评估(BASE 6.76 → LoRA 3.08,有效)。
- 导出:
lora_model/(适配器)、merged_16bit(vLLM)、gguf(Ollama)三选一 / 全要。
训练过程记录(2026.08.10 – 08.16)
- 08-10 · 启动复现:搭 conda(Python 3.11)+ 单条
requirements.txt,内置 torch CUDA 源(cu126)与 triton 平台标记,锁定transformers==5.5.0;模型走 HF 源下载Qwen/Qwen3.5-2B与yahma/alpaca-cleaned(约 5 万条),路径收口common.py。明确两坑:Qwen3.5 不推荐 4-bit QLoRA → 走 bf16 / 16-bit LoRA。 - 08-13 · 跑通微调与推理:
common.py抽路径常量、resolve_model_dir与format_alpaca模板;load_in_16bit=True、get_peft_model(r=16, alpha=16, dropout=0, grad_ckpt="unsloth", random_state=3407)只训约 0.49% 参数;SFTTrainer+SFTConfig(batch 2 / grad_accum 4 / lr 2e-4 / warmup 5 / seed 3407 / 3 epoch)。推理改model.eval()规避 4060 ptxas 崩溃。显存:4060(8GB)跑满约 7.4GB,4090(24GB)batch=16 仅约 9.3GB。 - 08-16 · 评估与导出:
06_eval.py配对对照 2000 样本,BASE PPL 6.76 / avg_loss 1.91,LoRA PPL 3.08 / avg_loss 1.12(PPL ↓54%、loss ↓41%),无过拟合。导出lora_model/、merged_16bit、gguf --quant q5_k_m;踩到 GGUF 中间目录坑(qwen35_2b_alpaca_gguf/可删,省约 4GB)。
本文也以「实战笔记」形式发布在 Unsloth Qwen3.5-2B Alpaca 有监督微调实战笔记,从踩坑到落地的叙述更完整;结构化的技术栈与链路在此项目条目。