AI 实践

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

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

· 约 6 分钟阅读

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

两个必须提前记住的硬坑

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

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

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

继续阅读