Unsloth 高效微调实战:用 Qwen3.5-2B 跑通 Alpaca 微调(有监督微调)
用 16-bit LoRA + TRL SFTTrainer 在 Qwen3.5-2B 上跑通 Alpaca 指令微调,并在工程上做了可复现、跨平台与量化评估的改进;4090 实测 PPL 从 6.76 降到 3.08(↓54%)。
Unsloth 高效微调实战:用 Qwen3.5-2B 跑通 Alpaca 微调(有监督微调)
用 16-bit LoRA + TRL SFTTrainer 在 Qwen3.5-2B 上跑通 Alpaca 指令微调,并在工程上做了可复现、跨平台与量化评估的改进;4090 实测 PPL 从 6.76 降到 3.08(↓54%)。结构化的项目信息与指标对照见 项目条目:Unsloth Qwen3.5-2B Alpaca 有监督微调。
为什么做这件事
全参微调一个 2B 模型,仅 AdamW 的优化器状态(m/v)就能吃掉约 16GB 显存——单卡基本跑不动。对中小团队、单卡或 Colab 用户来说,「用自己的一条指令数据把模型调教成会按格式回答」这件事,长期被显存和训练时长挡在门外。
这次的目标很朴素:用最低成本把开源小模型 Qwen/Qwen3.5-2B 通过 Alpaca 指令数据教会它「理解 instruction + input → 产出 output」的对话 / 指令范式,且能在 8GB 起步的显卡上跑起来。
底座选 Unsloth。和 LLaMA-Factory、XTuner 比,它的卖点正好命中上面的痛点——速度 2~5 倍、显存降 50%~80%,2B 的 bf16 LoRA 约 5GB 即可起步:
| 维度 | LLaMA-Factory | XTuner | Unsloth(本文) |
|---|---|---|---|
| 上手方式 | WebUI / CLI | 配置文件驱动 | Notebook / 少量 Python |
| 最大优势 | 功能全家桶 | 可深度定制 | 速度 + 显存 |
| 适合人群 | 业务快速落地 | 算法工程师 | 单卡 / Colab / 快速验证 |
模型用 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(
load_in_16bit=True、load_in_4bit=False),不用 4-bit。 - 需要 transformers v5(旧版直接报错)→ 锁定
transformers==5.5.0。
这两个坑决定了后面环境和训练脚本的写法,先记住能省很多返工。
环境:把坑提前焊死在 requirements.txt
文章用 conda + AutoDL。我落地为等价路径,并把两个跨平台易错点直接写进 requirements.txt 首行:
# 1) 建环境
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 源下载,路径统一收口到 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__ 定位,不依赖工作目录。所以无论在本地还是不同路径下执行,模型 / 数据 / 输出都稳定落在项目根——训练日志里 lora_model/ 自动落到对应路径就是这个机制在生效。
微调实现:高效的本质是只训 0.49% 参数
加载模型(scripts/03_train.py):dtype=None 让 Unsloth 自动选 bfloat16,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% 参数(约 10.9M / 2.2B)——这就是 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 使用。
训练:忠实复现文章默认值(packing=False、max_steps=60 仅演示),正式训练改用 --num_epochs(本工程用了 3)。相对文章的工程化改进:seed/data_seed=3407 显式传入使训练可复现;输出目录默认锚定项目根;packing 做成可选参数(--packing True 可提速 2~3×,用短样本拼满窗口减少 padding 浪费)。
注意:文章演示用
max_steps=60。正式训练应改num_epochs(如本工程的 3)。packing=True时每个 step 处理多条打包样本,epoch 按 packed 序列计,所以 3 epoch 只跑 609 步就完成——这是正常且更快的,不是参数错位。
推理验证:绕开 4060 的 ptxas 崩溃
文章用 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,与 4090 的 6.76 同区间,互相印证。
工程化改进小结
在「忠实参考文章方案」的基础上,落地的代码还补了这些(都不改变文章行为,只是更稳、可复现、跨平台):
| 项 | 改进 | 对应文件 |
|---|---|---|
| 公共模块 | 路径常量 + resolve_model_dir + format_alpaca 抽进 common.py,多脚本复用 |
common.py |
| 路径稳健 | 输出目录默认锚定 PROJECT_ROOT,不依赖 cwd |
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 |
写在最后
这次实践最大的收获不是「跑通了一个模型」,而是把一套实战方案改造成了带硬指标、跨卡可复现的小工程:显存实测(4060≈7.4GB / 4090≈9.3GB)、PPL 量化对比、以及把两个硬坑焊死在依赖与脚本里。
如果你也想上手,建议从「两个硬坑」和 requirements.txt 那两行开始——它们能帮你省掉大部分环境返工。完整的项目信息、技术栈与链路结构见 项目条目:Unsloth Qwen3.5-2B Alpaca 有监督微调。