项目 / Projects

Unsloth 高效微调实战:用 Qwen3.5-2B 跑通 Alpaca 微调(有监督微调)

用 16-bit LoRA + TRL SFTTrainer 在 Qwen3.5-2B 上跑通 Alpaca 指令微调,并在工程上做了可复现、跨平台与量化评估的改进;4090 实测 PPL 从 6.76 降到 3.08(↓54%)。

已实现工程化改进已完成,PPL 评估验证有效角色 工程化实现周期 2026.8.10–2026.8.16

背景与目标

大语言模型全参微调需要更新全部权重,显存与算力开销极大: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

复现前必须先记住两个硬坑:

  1. Qwen3.5 不推荐 4-bit QLoRA(量化误差偏大)→ 本工程走 bf16 / 16-bit LoRA,不用 4-bit。
  2. 需要 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 量化误差坑。

挂上 LoRAr=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.pyformat_alpaca):按模板拼 instruction / input / output末尾必须加 EOS_TOKEN,否则生成容易停不下来。训练前 dataset.map(format_alpaca, batched=True) 得到 text 字段供 SFTTrainer 使用。

SFTTrainer 训练:忠实复现文章默认值(packing=Falsemax_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

一句话回顾

  1. 准备:环境(conda + 内置 CUDA 源的 requirements.txt)→ 下载 Qwen3.5-2B + alpaca-cleaned(HF 源)。
  2. 加载FastLanguageModel.from_pretrained(load_in_16bit=True),bf16、不走 4-bit。
  3. 挂 LoRAget_peft_model(r=16, alpha=16, dropout=0, grad_ckpt="unsloth"),只训 0.49% 参数。
  4. 格式化:Alpaca 模板拼 instruction/input/output + EOS_TOKEN,成 text 字段。
  5. 训练SFTTrainer + SFTConfig(batch 2 / grad_accum 4 / lr 2e-4 / warmup 5 / seed 3407),3 epoch(packing 提速)。
  6. 验证:推理 sanity check + PPL 评估(BASE 6.76 → LoRA 3.08,有效)。
  7. 导出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-2Byahma/alpaca-cleaned(约 5 万条),路径收口 common.py。明确两坑:Qwen3.5 不推荐 4-bit QLoRA → 走 bf16 / 16-bit LoRA。
  • 08-13 · 跑通微调与推理common.py 抽路径常量、resolve_model_dirformat_alpaca 模板;load_in_16bit=Trueget_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_16bitgguf --quant q5_k_m;踩到 GGUF 中间目录坑(qwen35_2b_alpaca_gguf/ 可删,省约 4GB)。

本文也以「实战笔记」形式发布在 Unsloth Qwen3.5-2B Alpaca 有监督微调实战笔记,从踩坑到落地的叙述更完整;结构化的技术栈与链路在此项目条目。