本文档带你从零开始完成白盒 Logits 蒸馏:环境搭建 → 数据集准备 → 教师 SFT → 合并 → ASFT 蒸馏 → 学生合并 → 推理对比。
理论背景与实测记录见 基于千问的白盒蒸馏 SOP;黑盒蒸馏(API 数据)。
目录
- 开始之前
- [第一步:克隆 LLaMA-Factory](#第一步:克隆 LLaMA-Factory)
- 第二步:安装依赖
- 第三步:下载模型(download_models.py)
- 第四步:准备数据集(prepare_dataset.py)
- 第五步:估算训练时间(estimate_time.py)
- 第六步:冒烟验证(可选但推荐)
- [第七步:正式四步训练(30 epoch)](#第七步:正式四步训练(30 epoch))
- 第八步:推理对比(run_inference_compare.py)
- 一键脚本(run_full_pipeline.sh)
- [YAML 配置(全部 8 个)](#YAML 配置(全部 8 个))
- [常见问题 FAQ](#常见问题 FAQ)
- [附录:项目资产全文(Python + YAML)](#附录:项目资产全文(Python + YAML))
第0步:开始之前
0.1 硬件与平台
| 项目 | 最低要求 | 实测配置 |
|---|---|---|
| GPU | Step1/2 ≥16GB;Step3 双模型 ≥24GB | RTX 3090 24GB |
| 数据盘 | 建议 ≥100GB(50GB 盘需精细清理) | AutoDL 50GB(实测紧张) |
| 内存 | ≥16GB | 默认即可 |
| Python | 3.10+ | 3.12 |
| PyTorch | 2.x + CUDA | 2.7.0+cu128 |
0.2 你要完成什么
#mermaid-svg-0jqebTqZNlzS8z8I{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0jqebTqZNlzS8z8I .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0jqebTqZNlzS8z8I .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0jqebTqZNlzS8z8I .error-icon{fill:#552222;}#mermaid-svg-0jqebTqZNlzS8z8I .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0jqebTqZNlzS8z8I .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0jqebTqZNlzS8z8I .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0jqebTqZNlzS8z8I .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0jqebTqZNlzS8z8I .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0jqebTqZNlzS8z8I .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0jqebTqZNlzS8z8I .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0jqebTqZNlzS8z8I .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0jqebTqZNlzS8z8I .marker.cross{stroke:#333333;}#mermaid-svg-0jqebTqZNlzS8z8I svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0jqebTqZNlzS8z8I p{margin:0;}#mermaid-svg-0jqebTqZNlzS8z8I .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0jqebTqZNlzS8z8I .cluster-label text{fill:#333;}#mermaid-svg-0jqebTqZNlzS8z8I .cluster-label span{color:#333;}#mermaid-svg-0jqebTqZNlzS8z8I .cluster-label span p{background-color:transparent;}#mermaid-svg-0jqebTqZNlzS8z8I .label text,#mermaid-svg-0jqebTqZNlzS8z8I span{fill:#333;color:#333;}#mermaid-svg-0jqebTqZNlzS8z8I .node rect,#mermaid-svg-0jqebTqZNlzS8z8I .node circle,#mermaid-svg-0jqebTqZNlzS8z8I .node ellipse,#mermaid-svg-0jqebTqZNlzS8z8I .node polygon,#mermaid-svg-0jqebTqZNlzS8z8I .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0jqebTqZNlzS8z8I .rough-node .label text,#mermaid-svg-0jqebTqZNlzS8z8I .node .label text,#mermaid-svg-0jqebTqZNlzS8z8I .image-shape .label,#mermaid-svg-0jqebTqZNlzS8z8I .icon-shape .label{text-anchor:middle;}#mermaid-svg-0jqebTqZNlzS8z8I .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0jqebTqZNlzS8z8I .rough-node .label,#mermaid-svg-0jqebTqZNlzS8z8I .node .label,#mermaid-svg-0jqebTqZNlzS8z8I .image-shape .label,#mermaid-svg-0jqebTqZNlzS8z8I .icon-shape .label{text-align:center;}#mermaid-svg-0jqebTqZNlzS8z8I .node.clickable{cursor:pointer;}#mermaid-svg-0jqebTqZNlzS8z8I .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0jqebTqZNlzS8z8I .arrowheadPath{fill:#333333;}#mermaid-svg-0jqebTqZNlzS8z8I .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0jqebTqZNlzS8z8I .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0jqebTqZNlzS8z8I .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0jqebTqZNlzS8z8I .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0jqebTqZNlzS8z8I .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0jqebTqZNlzS8z8I .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0jqebTqZNlzS8z8I .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0jqebTqZNlzS8z8I .cluster text{fill:#333;}#mermaid-svg-0jqebTqZNlzS8z8I .cluster span{color:#333;}#mermaid-svg-0jqebTqZNlzS8z8I div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0jqebTqZNlzS8z8I .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0jqebTqZNlzS8z8I rect.text{fill:none;stroke-width:0;}#mermaid-svg-0jqebTqZNlzS8z8I .icon-shape,#mermaid-svg-0jqebTqZNlzS8z8I .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0jqebTqZNlzS8z8I .icon-shape p,#mermaid-svg-0jqebTqZNlzS8z8I .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0jqebTqZNlzS8z8I .icon-shape .label rect,#mermaid-svg-0jqebTqZNlzS8z8I .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0jqebTqZNlzS8z8I .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0jqebTqZNlzS8z8I .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0jqebTqZNlzS8z8I :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} prepare_dataset.py
42,677 条中文指令
Step1 教师 LoRA SFT
Qwen3-4B
Step2 合并教师
Step3 ASFT 蒸馏
Qwen3-1.7B + ref_model
Step4 合并学生
白盒 vs 黑盒: 黑盒用 API 文本当标签;白盒用教师 forward 的 Logits 做 KL 对齐(LLaMA-Factory 的 use_asft_loss)。
0.3 最终目录结构
/root/autodl-tmp/Distill2/
├── SOP.md # 理论 + 实测记录
├── TUTORIAL.md # 本文档
├── download_models.py # 下载教师/学生基座
├── prepare_dataset.py # 下载并导出本地数据集
├── estimate_time.py # 30 epoch 耗时估算
├── run_inference_compare.py # 三路推理对比
├── validation_prompt.txt # 从数据集第 0 条导出的验证 prompt
├── logs_*.log # 各步日志
├── models/ # ModelScope 模型缓存 (~11GB)
└── LLaMA-Factory/
├── data/alpaca_gpt4_zh.json
├── examples/train_lora/
│ ├── qwen3_4b_teacher_lora.yaml # 冒烟
│ ├── qwen3_4b_teacher_lora_full.yaml # 正式 30 epoch
│ ├── qwen3_1.7b_distill_lora.yaml # 冒烟(含 ASFT)
│ └── qwen3_1.7b_distill_lora_full.yaml # 正式 30 epoch
├── examples/merge_lora/
│ ├── qwen3_4b_teacher_merge.yaml # Step2 冒烟合并
│ ├── qwen3_4b_teacher_merge_full.yaml # Step2 正式合并
│ ├── qwen3_1.7b_student_merge.yaml # Step4 冒烟合并
│ └── qwen3_1.7b_student_merge_full.yaml
└── saves/ # 训练产出
└── benchmark/tmp/ # 微基准 train_results.json
第一步:克隆 LLaMA-Factory
bash
mkdir -p /root/autodl-tmp/Distill2
cd /root/autodl-tmp/Distill2
source /etc/network_turbo # AutoDL 访问 GitHub 前必做
git clone --depth 1 https://github.com/hiyouga/LLaMA-Factory.git
检查点: 存在 LLaMA-Factory/src/llamafactory/。
第二步:安装依赖
bash
cd /root/autodl-tmp/Distill2/LLaMA-Factory
pip install -e ".[torch,metrics]"
pip install addict # ModelScope MsDataset 需要
llamafactory-cli version
| 问题 | 解决 |
|---|---|
| torchaudio 与 torch 版本不匹配 | pip install "torchaudio==2.7.0" --index-url https://download.pytorch.org/whl/cu128(须与当前 torch 主版本一致) |
OMP 警告 Invalid value for OMP_NUM_THREADS |
export OMP_NUM_THREADS=1(勿设为 0) |
第三步:下载模型(download_models.py)
3.1 脚本作用
download_models.py 从 ModelScope 下载两个基座模型到本地,避免训练时联网。
| 变量 | Model ID | 角色 |
|---|---|---|
TEACHER_ID |
Qwen/Qwen3-4B-Instruct-2507 |
教师(4B Instruct) |
STUDENT_ID |
Qwen/Qwen3-1.7B |
学生(1.7B Thinking 基座) |
LOCAL_DIR |
/root/autodl-tmp/Distill2/models |
缓存根目录 |
3.2 逐行说明
python
from modelscope import snapshot_download
调用魔搭下载 API;cache_dir 下会生成 models/Qwen--.../snapshots/master 嵌套路径。
python
for mid in (TEACHER_ID, STUDENT_ID):
path = snapshot_download(mid, cache_dir=LOCAL_DIR)
循环下载;教师约 7.6GB,学生约 3.8GB,合计 ~35 分钟(视网速)。
3.3 运行
bash
source /etc/network_turbo
cd /root/autodl-tmp/Distill2
python3 download_models.py
检查点: 打印两条 OK: 路径。后续 YAML 中使用:
教师: .../Qwen--Qwen3-4B-Instruct-2507/snapshots/master
学生: .../Qwen--Qwen3-1.7B/snapshots/master
第四步:准备数据集(prepare_dataset.py)
4.1 为什么需要本地 JSON
实测:LLaMA-Factory 加载 Hub 名 alpaca_gpt4_zh 时默认走 HuggingFace ,AutoDL 无加速时常报 Network is unreachable。因此本教程强制导出本地副本。
4.2 脚本作用
prepare_dataset.py 完成三件事:
- 从 ModelScope(失败则 HuggingFace)加载
llamafactory/alpaca_gpt4_zh - 导出为
LLaMA-Factory/data/alpaca_gpt4_zh.json - 在
dataset_info.json注册alpaca_gpt4_zh_local,并写入validation_prompt.txt
4.3 关键函数
load_hub_dataset() --- 优先 ModelScope,失败回退 HF:
python
ds = MsDataset.load(HUB_ID, split="train") # 需 pip install addict
ensure_registered() --- 向 dataset_info.json 追加:
json
"alpaca_gpt4_zh_local": { "file_name": "alpaca_gpt4_zh.json" }
训练 YAML 中写 dataset: alpaca_gpt4_zh_local 即可离线加载。
export_validation_prompt(ds) --- 取第 0 条样本的 instruction(+input)写入 validation_prompt.txt,供第八步推理对比。内容来自真实数据,非手写。
4.4 运行
bash
source /etc/network_turbo # 首次下载需要
pip install addict
python3 prepare_dataset.py
检查点:
- 打印
exported 42677 records - 存在
validation_prompt.txt(实测为:保持健康的三个提示。)
第五步:估算训练时间(estimate_time.py)
5.1 为什么先估算
30 epoch × 全量 42,677 条在 RTX 3090 上约 9~10 天。正式开跑前应心里有数,并确保磁盘、实例不会中途释放。
5.2 脚本原理
estimate_time.py 根据优化步数外推:
steps_per_epoch = ceil(样本数 / (batch_size × grad_accum))
= ceil(42677 / 4) = 10,670
total_steps = 10,670 × 30 = 320,100
耗时 = total_steps / train_steps_per_second
速度来源按优先级分三种:
| 模式 | 触发条件 | 说明 |
|---|---|---|
| 微基准 | 传入 --micro-samples N |
现场跑 N 条、ceil(N/4) 步训练,实测 train_steps_per_second(最准,约 2~3 分钟) |
| 冒烟外推 | 第六节已跑完 | 读取 saves/qwen3-4b/lora/teacher-sft/train_results.json 与 saves/qwen3-1.7b/lora/distill/train_results.json |
| 内置参考值 | 无上述文件且未开微基准 | 使用 RTX 3090 参考速度(教师 0.71、蒸馏 0.778 steps/s) |
冒烟 YAML 全文见本文档 [YAML 配置](#YAML 配置) 一节,文件路径:
5.3 运行(正式训练前必做)
方式 A:快速估算(无需 GPU 训练,默认)
bash
cd /root/autodl-tmp/Distill2
python3 estimate_time.py --epochs 30
无第六节冒烟产物时,脚本自动使用内置参考速度,不会 因缺少 train_results.json 报错。
方式 B:微基准(本机实测,推荐)
bash
cd /root/autodl-tmp/Distill2/LLaMA-Factory
python3 estimate_time.py --epochs 30 --micro-samples 50
也可在根目录执行(脚本会调用 LLaMA-Factory 下的 YAML 与 llamafactory-cli):
bash
cd /root/autodl-tmp/Distill2
python3 estimate_time.py --epochs 30 --micro-samples 50
微基准产物写入 LLaMA-Factory/saves/benchmark/tmp/{teacher,distill}/train_results.json。
5.4 实测外推结果(RTX 3090,2026-07-08)
内置参考值 / 冒烟外推(教师 0.71、蒸馏 0.778 steps/s):
| 阶段 | 优化步数 | 预估耗时 |
|---|---|---|
| Step1 教师 LoRA | 320,100 | ~5.2 天(125h) |
| Step3 ASFT 蒸馏 | 320,100 | ~4.8 天(114h) |
| Step2+4 合并 | --- | ~1 分钟 |
| 合计 | --- | ~10.0 天(240h) |
微基准实测(--micro-samples 50,教师 0.689、蒸馏 0.857 steps/s):
| 阶段 | 优化步数 | 预估耗时 |
|---|---|---|
| Step1 教师 LoRA | 320,100 | ~5.4 天(129h) |
| Step3 ASFT 蒸馏 | 320,100 | ~4.3 天(104h) |
| Step2+4 合并 | --- | ~1 分钟 |
| 合计 | --- | ~9.7 天(233h) |
另计:首次全量 tokenize 预处理 +10~30 分钟。
5.5 参数说明
| 参数 | 含义 |
|---|---|
--epochs 30 |
训练轮数 |
--samples 42677 |
样本数(可改小做子集实验) |
--micro-samples 50 |
可选,先跑微基准再估算(50 与冒烟规模一致) |
第六步:冒烟验证
在投入约 10 天之前,用 50 条 / 1 epoch 验证四步链路(约 5 分钟)。8 个 YAML 完整正文见 [第十一节 YAML 配置](#8 个 YAML 完整正文见 第十一节 YAML 配置),本节冒烟使用其中前 4 个:
| 步骤 | YAML |
|---|---|
| Step1 教师 LoRA | examples/train_lora/qwen3_4b_teacher_lora.yaml |
| Step2 合并教师 | examples/merge_lora/qwen3_4b_teacher_merge.yaml |
| Step3 白盒蒸馏 | examples/train_lora/qwen3_1.7b_distill_lora.yaml |
| Step4 合并学生 | examples/merge_lora/qwen3_1.7b_student_merge.yaml |
6.1 执行命令
bash
cd /root/autodl-tmp/Distill2/LLaMA-Factory
export OMP_NUM_THREADS=1
llamafactory-cli train examples/train_lora/qwen3_4b_teacher_lora.yaml
llamafactory-cli export examples/merge_lora/qwen3_4b_teacher_merge.yaml
llamafactory-cli train examples/train_lora/qwen3_1.7b_distill_lora.yaml
llamafactory-cli export examples/merge_lora/qwen3_1.7b_student_merge.yaml
6.2 检查点
saves/qwen3-4b/lora/teacher-sft/train_results.json
saves/qwen3-1.7b/lora/distill/train_results.json
检查点: Step3 日志出现 Created reference model from saves/qwen3-4b/merged-teacher。
第七步:正式四步训练(30 epoch)
7.1 磁盘准备(50GB 盘必看)
合并教师需额外 ~7.6GB。建议开跑前:
bash
df -h /root/autodl-tmp # 确保 Step2 前 ≥8GB 可用
# 可删除旧冒烟产物、merged-student 等(Step4 会重建)
rm -rf LLaMA-Factory/saves/qwen3-1.7b/merged-student
磁盘策略: Step3 完成后可删 merged-teacher 再合并学生(见 SOP 9.2)。
7.2 分步执行(推荐,便于断点续训)
8 个 YAML 完整正文见 [第十一节 YAML 配置](#8 个 YAML 完整正文见 第十一节 YAML 配置) ,本节正式训练使用后 4 个(*_full.yaml):
| 步骤 | YAML |
|---|---|
| Step1 教师 30 epoch | examples/train_lora/qwen3_4b_teacher_lora_full.yaml |
| Step2 合并教师 | examples/merge_lora/qwen3_4b_teacher_merge_full.yaml |
| Step3 蒸馏 30 epoch | examples/train_lora/qwen3_1.7b_distill_lora_full.yaml |
| Step4 合并学生 | examples/merge_lora/qwen3_1.7b_student_merge_full.yaml |
bash
cd /root/autodl-tmp/Distill2/LLaMA-Factory
export OMP_NUM_THREADS=1
# Step1 教师(输出 -> saves/qwen3-4b/lora/teacher-sft-full)
llamafactory-cli train examples/train_lora/qwen3_4b_teacher_lora_full.yaml \
2>&1 | tee ../logs_full_step1_teacher.log
# Step2 合并教师(输出 -> saves/qwen3-4b/merged-teacher)
llamafactory-cli export examples/merge_lora/qwen3_4b_teacher_merge_full.yaml \
2>&1 | tee ../logs_full_step2_merge_teacher.log
# Step3 蒸馏(输出 -> saves/qwen3-1.7b/lora/distill-full)
llamafactory-cli train examples/train_lora/qwen3_1.7b_distill_lora_full.yaml \
2>&1 | tee ../logs_full_step3_distill.log
# Step4 合并学生(输出 -> saves/qwen3-1.7b/merged-student)
llamafactory-cli export examples/merge_lora/qwen3_1.7b_student_merge_full.yaml \
2>&1 | tee ../logs_full_step4_merge_student.log
7.3 冒烟 vs 正式 YAML 差异
| 字段 | 冒烟 | 正式 full |
|---|---|---|
max_samples |
50 | 删除(全量) |
num_train_epochs |
1.0 | 30.0 |
output_dir |
teacher-sft / distill |
teacher-sft-full / distill-full |
save_steps |
100 | 1000 |
adapter_name_or_path(merge) |
teacher-sft / distill |
teacher-sft-full / distill-full |
7.4 断点续训
若 Step1 中断,在 YAML 中添加:
yaml
resume_from_checkpoint: saves/qwen3-4b/lora/teacher-sft-full/checkpoint-XXXX
第八步:推理对比(run_inference_compare.py)
8.1 脚本作用
run_inference_compare.py 用同一条 validation_prompt.txt,依次加载三个模型并生成回答,避免显存同时加载多模型 OOM。
| 顺序 | 模型 | 加载方式 |
|---|---|---|
| 1 | 基座 Qwen3-1.7B | 直接 from_pretrained(STUDENT_BASE) |
| 2 | 教师 Qwen3-4B+LoRA | PeftModel.from_pretrained(基座, TEACHER_LORA) |
| 3 | 蒸馏学生 | from_pretrained(STUDENT_MERGED) |
8.2 关键逻辑
python
def generate(model, tokenizer, user_content, max_new_tokens=200):
messages = [{"role": "user", "content": user_content}]
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
...
outputs = model.generate(..., do_sample=False) # 贪心解码,便于对比
每轮推理后 del model; torch.cuda.empty_cache() 释放显存。
8.3 运行
bash
cd /root/autodl-tmp/Distill2
python3 run_inference_compare.py
# 指定合并路径(可选)
python3 run_inference_compare.py --student-merged LLaMA-Factory/saves/qwen3-1.7b/merged-student
如何解读结果:
- 教师 应输出结构化中文列表(与 Alpaca 数据风格一致)
- 蒸馏学生 正式 30 epoch 后应更接近教师;冒烟 1 epoch 可能仍带 Thinking 模型思维链
一键脚本(run_full_pipeline.sh)
run_full_pipeline.sh尚未提供;可按下列顺序手动执行,或自行封装为脚本。每步日志建议tee到logs_full_*.log。
bash
# 脚本内容摘要
python3 estimate_time.py --epochs 30 --micro-samples 50 # 微基准估算(可选 --epochs 30 快速估算)
llamafactory-cli train ...teacher_lora_full.yaml
llamafactory-cli export ...teacher_merge_full.yaml
llamafactory-cli train ...distill_lora_full.yaml
llamafactory-cli export ...student_merge_full.yaml
bash
cd /root/autodl-tmp/Distill2
# nohup bash run_full_pipeline.sh > logs_full_pipeline.log 2>&1 &
# tail -f logs_full_pipeline.log
注意: 全流程约 9~10 天,务必使用
nohup/后台实例,并监控磁盘。
YAML 配置(全部 8 个)
以下为项目创建的 8 个 YAML 完整正文,与 LLaMA-Factory/examples/ 下磁盘文件一致。第六节冒烟用前 4 个,第七节正式训练用后 4 个。
微基准说明:
estimate_time.py --micro-samples使用冒烟训练 YAML(qwen3_4b_teacher_lora.yaml、qwen3_1.7b_distill_lora.yaml),会临时将num_train_epochs替换为max_steps。
冒烟训练 / 合并(4 个)
qwen3_4b_teacher_lora.yaml --- Step1 教师
路径:LLaMA-Factory/examples/train_lora/qwen3_4b_teacher_lora.yaml
yaml
### model
model_name_or_path: /root/autodl-tmp/Distill2/models/models/Qwen--Qwen3-4B-Instruct-2507/snapshots/master
trust_remote_code: true
### method
stage: sft
do_train: true
finetuning_type: lora
lora_rank: 8
lora_target: all
### dataset
dataset: alpaca_gpt4_zh_local
template: qwen3
cutoff_len: 2048
max_samples: 50
preprocessing_num_workers: 16
dataloader_num_workers: 4
### output
output_dir: saves/qwen3-4b/lora/teacher-sft
logging_steps: 10
save_steps: 100
plot_loss: true
overwrite_output_dir: true
save_only_model: false
report_to: none
### train
per_device_train_batch_size: 1
gradient_accumulation_steps: 4
learning_rate: 1.0e-4
num_train_epochs: 1.0
lr_scheduler_type: cosine
warmup_ratio: 0.1
bf16: true
ddp_timeout: 180000000
resume_from_checkpoint: null
qwen3_4b_teacher_merge.yaml --- Step2 合并教师
路径:LLaMA-Factory/examples/merge_lora/qwen3_4b_teacher_merge.yaml
yaml
### Note: DO NOT use quantized model or quantization_bit when merging lora adapters
### model
model_name_or_path: /root/autodl-tmp/Distill2/models/models/Qwen--Qwen3-4B-Instruct-2507/snapshots/master
adapter_name_or_path: saves/qwen3-4b/lora/teacher-sft
template: qwen3
trust_remote_code: true
### export
export_dir: saves/qwen3-4b/merged-teacher
export_size: 5
export_device: cpu
export_legacy_format: false
qwen3_1.7b_distill_lora.yaml --- Step3 白盒蒸馏
路径:LLaMA-Factory/examples/train_lora/qwen3_1.7b_distill_lora.yaml
yaml
### model
model_name_or_path: /root/autodl-tmp/Distill2/models/models/Qwen--Qwen3-1.7B/snapshots/master
trust_remote_code: true
### method
stage: sft
do_train: true
finetuning_type: lora
lora_rank: 8
lora_target: all
use_asft_loss: true
asft_alpha: 0.1
ref_model: saves/qwen3-4b/merged-teacher
### dataset
dataset: alpaca_gpt4_zh_local
template: qwen3
cutoff_len: 2048
max_samples: 50
preprocessing_num_workers: 16
dataloader_num_workers: 4
### output
output_dir: saves/qwen3-1.7b/lora/distill
logging_steps: 10
save_steps: 100
plot_loss: true
overwrite_output_dir: true
save_only_model: false
report_to: none
### train
per_device_train_batch_size: 1
gradient_accumulation_steps: 4
learning_rate: 1.0e-4
num_train_epochs: 1.0
lr_scheduler_type: cosine
warmup_ratio: 0.1
bf16: true
ddp_timeout: 180000000
resume_from_checkpoint: null
qwen3_1.7b_student_merge.yaml --- Step4 合并学生
路径:LLaMA-Factory/examples/merge_lora/qwen3_1.7b_student_merge.yaml
yaml
### Note: DO NOT use quantized model or quantization_bit when merging lora adapters
### model
model_name_or_path: /root/autodl-tmp/Distill2/models/models/Qwen--Qwen3-1.7B/snapshots/master
adapter_name_or_path: saves/qwen3-1.7b/lora/distill
template: qwen3
trust_remote_code: true
### export
export_dir: saves/qwen3-1.7b/merged-student
export_size: 2
export_device: cpu
export_legacy_format: false
正式训练 / 合并(4 个)
qwen3_4b_teacher_lora_full.yaml --- Step1 教师 30 epoch
路径:LLaMA-Factory/examples/train_lora/qwen3_4b_teacher_lora_full.yaml
yaml
### model
model_name_or_path: /root/autodl-tmp/Distill2/models/models/Qwen--Qwen3-4B-Instruct-2507/snapshots/master
trust_remote_code: true
### method
stage: sft
do_train: true
finetuning_type: lora
lora_rank: 8
lora_target: all
### dataset
dataset: alpaca_gpt4_zh_local
template: qwen3
cutoff_len: 2048
preprocessing_num_workers: 16
dataloader_num_workers: 4
### output
output_dir: saves/qwen3-4b/lora/teacher-sft-full
logging_steps: 10
save_steps: 1000
plot_loss: true
overwrite_output_dir: true
save_only_model: false
report_to: none
### train
per_device_train_batch_size: 1
gradient_accumulation_steps: 4
learning_rate: 1.0e-4
num_train_epochs: 30.0
lr_scheduler_type: cosine
warmup_ratio: 0.1
bf16: true
ddp_timeout: 180000000
resume_from_checkpoint: null
qwen3_4b_teacher_merge_full.yaml --- Step2 合并教师
路径:LLaMA-Factory/examples/merge_lora/qwen3_4b_teacher_merge_full.yaml
yaml
### Note: DO NOT use quantized model or quantization_bit when merging lora adapters
### model
model_name_or_path: /root/autodl-tmp/Distill2/models/models/Qwen--Qwen3-4B-Instruct-2507/snapshots/master
adapter_name_or_path: saves/qwen3-4b/lora/teacher-sft-full
template: qwen3
trust_remote_code: true
### export
export_dir: saves/qwen3-4b/merged-teacher
export_size: 5
export_device: cpu
export_legacy_format: false
qwen3_1.7b_distill_lora_full.yaml --- Step3 蒸馏 30 epoch
路径:LLaMA-Factory/examples/train_lora/qwen3_1.7b_distill_lora_full.yaml
yaml
### model
model_name_or_path: /root/autodl-tmp/Distill2/models/models/Qwen--Qwen3-1.7B/snapshots/master
trust_remote_code: true
### method
stage: sft
do_train: true
finetuning_type: lora
lora_rank: 8
lora_target: all
use_asft_loss: true
asft_alpha: 0.1
ref_model: saves/qwen3-4b/merged-teacher
### dataset
dataset: alpaca_gpt4_zh_local
template: qwen3
cutoff_len: 2048
preprocessing_num_workers: 16
dataloader_num_workers: 4
### output
output_dir: saves/qwen3-1.7b/lora/distill-full
logging_steps: 10
save_steps: 1000
plot_loss: true
overwrite_output_dir: true
save_only_model: false
report_to: none
### train
per_device_train_batch_size: 1
gradient_accumulation_steps: 4
learning_rate: 1.0e-4
num_train_epochs: 30.0
lr_scheduler_type: cosine
warmup_ratio: 0.1
bf16: true
ddp_timeout: 180000000
resume_from_checkpoint: null
qwen3_1.7b_student_merge_full.yaml --- Step4 合并学生
路径:LLaMA-Factory/examples/merge_lora/qwen3_1.7b_student_merge_full.yaml
yaml
### Note: DO NOT use quantized model or quantization_bit when merging lora adapters
### model
model_name_or_path: /root/autodl-tmp/Distill2/models/models/Qwen--Qwen3-1.7B/snapshots/master
adapter_name_or_path: saves/qwen3-1.7b/lora/distill-full
template: qwen3
trust_remote_code: true
### export
export_dir: saves/qwen3-1.7b/merged-student
export_size: 2
export_device: cpu
export_legacy_format: false
逐字段说明
以 qwen3_1.7b_distill_lora.yaml 为例:
| 字段 | 值 | 含义 |
|---|---|---|
model_name_or_path |
本地模型路径 | 教师 4B Instruct / 学生 1.7B 基座 |
stage: sft |
监督微调阶段 | 蒸馏也在 SFT 框架内完成 |
finetuning_type: lora |
LoRA | 只训练低秩适配器,省显存 |
lora_rank: 8 |
秩 8 | 越大容量越大、越慢 |
use_asft_loss: true |
白盒蒸馏 | 损失 = CE + α·KL(teacher logits) |
asft_alpha: 0.1 |
KL 权重 | 对应理论笔记中的 α |
ref_model |
saves/qwen3-4b/merged-teacher |
必须指向合并后的教师 |
dataset |
alpaca_gpt4_zh_local |
本地 JSON,离线可用 |
template: qwen3 |
聊天模板 | 4B Instruct 可试 qwen3_nothink;1.7B Thinking 基座用 qwen3 |
cutoff_len: 2048 |
最大序列长 | 超长样本截断;estimate_time.py 微基准在此注入 max_samples |
max_samples: 50 |
冒烟样本数 | 正式 full 版删除此行 |
output_dir |
teacher-sft / distill |
冒烟输出目录;正式版改为 *-full |
per_device_train_batch_size: 1 |
微批次 | 显存紧张保持 1 |
gradient_accumulation_steps: 4 |
梯度累积 | 等效 batch=4;与 estimate_time.py 外推公式一致 |
num_train_epochs |
1.0(冒烟)/ 30.0(正式) | 训练轮数;微基准时脚本替换为 max_steps |
bf16: true |
混合精度 | 3090 推荐开启 |
常见问题 FAQ
Q1:训练报 HF Hub unreachable?
A:确认 YAML 使用 alpaca_gpt4_zh_local,并重新运行 prepare_dataset.py。
Q2:合并报 No space left on device?
A:df -h 检查;删除旧 merged 产物或扩容;Step3 后删 merged-teacher 再合并学生。
Q3:蒸馏和普通 SFT 一样?
A:检查 Step3 是否 use_asft_loss: true 且日志有 Created reference model。
Q4:30 epoch 太久怎么办?
A:先用 max_samples: 5000 + 3 epoch 验证趋势;或子集 + 正式 epoch 折中。
Q5:学生回答全是思维链?
A:Qwen3-1.7B 为 Thinking 基座;可换 Instruct 学生,或 template: qwen3,并跑满 30 epoch。
Q6:estimate_time.py 报 train_results.json 不存在?
A:旧版脚本会直接崩溃。当前版本已支持内置参考值;或加 --micro-samples 50 跑微基准;或先完成第六节冒烟生成 train_results.json。
Q7:llamafactory-cli 报 torchaudio / libcudart 错误?
A:安装与 torch 同主版本的 torchaudio(如 pip install "torchaudio==2.7.0" --index-url https://download.pytorch.org/whl/cu128)。
完整命令速查
bash
cd /root/autodl-tmp/Distill2
source /etc/network_turbo
python3 download_models.py
pip install addict && python3 prepare_dataset.py
python3 estimate_time.py --epochs 30 # 快速估算
python3 estimate_time.py --epochs 30 --micro-samples 50 # 微基准(更准确)
cd LLaMA-Factory
export OMP_NUM_THREADS=1
llamafactory-cli train examples/train_lora/qwen3_4b_teacher_lora_full.yaml
llamafactory-cli export examples/merge_lora/qwen3_4b_teacher_merge_full.yaml
llamafactory-cli train examples/train_lora/qwen3_1.7b_distill_lora_full.yaml
llamafactory-cli export examples/merge_lora/qwen3_1.7b_student_merge_full.yaml
cd ..
python3 run_inference_compare.py
附录:项目资产全文(Python + YAML)
本附录收录项目自有的 Python 脚本 与 训练/合并 YAML 的完整内容(不含数据集 JSON、模型权重、saves/ 中间产物等)。
Python 脚本在根目录与 LLaMA-Factory/ 下各有一份内容相同的副本 ;下文以根目录路径为准,副本路径将 Distill2/ 换为 Distill2/LLaMA-Factory/ 即可。
download_models.py
路径:download_models.py(副本:LLaMA-Factory/download_models.py)
python
"""从 ModelScope 下载教师/学生基座模型。"""
from modelscope import snapshot_download
TEACHER_ID = "Qwen/Qwen3-4B-Instruct-2507"
STUDENT_ID = "Qwen/Qwen3-1.7B"
LOCAL_DIR = "/root/autodl-tmp/Distill2/models"
if __name__ == "__main__":
for mid in (TEACHER_ID, STUDENT_ID):
print(f"=== Downloading {mid} ===")
path = snapshot_download(mid, cache_dir=LOCAL_DIR)
print(f"OK: {path}")
prepare_dataset.py
路径:prepare_dataset.py(副本:LLaMA-Factory/prepare_dataset.py)
python
"""预下载 alpaca_gpt4_zh 并导出为 LLaMA-Factory 本地 JSON。"""
import json
from pathlib import Path
HUB_ID = "llamafactory/alpaca_gpt4_zh"
OUT_DIR = Path("/root/autodl-tmp/Distill2/LLaMA-Factory/data")
OUT_FILE = OUT_DIR / "alpaca_gpt4_zh.json"
VALIDATION_PROMPT = Path("/root/autodl-tmp/Distill2/validation_prompt.txt")
DATASET_INFO = OUT_DIR / "dataset_info.json"
REG_KEY = "alpaca_gpt4_zh_local"
def load_hub_dataset():
try:
from modelscope.msdatasets import MsDataset
ds = MsDataset.load(HUB_ID, split="train")
print("source: modelscope")
return ds
except Exception as exc:
print("modelscope failed:", exc)
from datasets import load_dataset
ds = load_dataset(HUB_ID, split="train")
print("source: huggingface")
return ds
def ensure_registered():
info = json.loads(DATASET_INFO.read_text(encoding="utf-8"))
if REG_KEY not in info:
info[REG_KEY] = {"file_name": "alpaca_gpt4_zh.json"}
DATASET_INFO.write_text(json.dumps(info, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(f"registered {REG_KEY} in dataset_info.json")
else:
print(f"{REG_KEY} already registered")
def export_validation_prompt(ds):
row = ds[0]
prompt = row["instruction"]
if row.get("input"):
prompt = prompt + "\n" + row["input"]
VALIDATION_PROMPT.write_text(prompt, encoding="utf-8")
print(f"wrote validation prompt -> {VALIDATION_PROMPT}")
def main():
ds = load_hub_dataset()
records = [dict(ds[i]) for i in range(len(ds))]
OUT_DIR.mkdir(parents=True, exist_ok=True)
OUT_FILE.write_text(json.dumps(records, ensure_ascii=False), encoding="utf-8")
print(f"exported {len(records)} records -> {OUT_FILE}")
ensure_registered()
export_validation_prompt(ds)
if __name__ == "__main__":
main()
estimate_time.py
路径:estimate_time.py(副本:LLaMA-Factory/estimate_time.py)
python
"""根据小规模实测或冒烟指标,估算 30 epoch 全量训练耗时。"""
from __future__ import annotations
import argparse
import json
import math
import os
import subprocess
import sys
from pathlib import Path
LF_ROOT = Path("/root/autodl-tmp/Distill2/LLaMA-Factory")
FULL_SAMPLES = 42_677
# 2026-07-08 RTX 3090 冒烟实测(50 条 / 1 epoch);无冒烟产物时作 fallback
DEFAULT_TEACHER_STEPS_PER_SEC = 0.71
DEFAULT_DISTILL_STEPS_PER_SEC = 0.778
TEACHER_SMOKE_RESULTS = LF_ROOT / "saves/qwen3-4b/lora/teacher-sft/train_results.json"
DISTILL_SMOKE_RESULTS = LF_ROOT / "saves/qwen3-1.7b/lora/distill/train_results.json"
def load_smoke_metrics() -> dict | None:
"""读取第六节冒烟产物;不存在则返回 None(第五节可在冒烟前运行)。"""
if not TEACHER_SMOKE_RESULTS.is_file() or not DISTILL_SMOKE_RESULTS.is_file():
return None
teacher = json.loads(TEACHER_SMOKE_RESULTS.read_text())
distill = json.loads(DISTILL_SMOKE_RESULTS.read_text())
return {"teacher": teacher, "distill": distill}
def steps_per_epoch(num_samples: int, batch: int = 1, grad_accum: int = 4) -> int:
return math.ceil(num_samples / (batch * grad_accum))
def estimate_from_metrics(num_samples: int, epochs: int, steps_per_sec: float) -> dict:
steps = steps_per_epoch(num_samples) * epochs
seconds = steps / max(steps_per_sec, 1e-9)
return {
"num_samples": num_samples,
"epochs": epochs,
"optimizer_steps": steps,
"seconds": seconds,
"hours": seconds / 3600,
}
def run_micro_benchmark(samples: int, yaml_path: Path) -> float:
"""跑 max_samples=N、max_steps=ceil(N/4) 的微基准,返回 steps/sec。"""
import tempfile
base = yaml_path.read_text()
micro_steps = max(1, math.ceil(samples / 4))
patched = base
for key in ("max_samples: 50", "max_samples: 100", "num_train_epochs: 1.0"):
patched = patched.replace(key, "")
if "max_samples:" not in patched:
patched = patched.replace(
"cutoff_len: 2048",
f"cutoff_len: 2048\nmax_samples: {samples}",
)
patched = patched.replace("num_train_epochs: 30.0", f"max_steps: {micro_steps}")
if "max_steps:" not in patched:
patched = patched.replace("num_train_epochs: 1.0", f"max_steps: {micro_steps}")
with tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False) as f:
f.write(patched)
tmp = Path(f.name)
out_dir = LF_ROOT / "saves/benchmark/tmp"
patched = patched.replace(
"output_dir: saves/qwen3-4b/lora/teacher-sft",
f"output_dir: {out_dir / 'teacher'}",
).replace(
"output_dir: saves/qwen3-1.7b/lora/distill",
f"output_dir: {out_dir / 'distill'}",
)
tmp.write_text(patched)
cmd = ["llamafactory-cli", "train", str(tmp)]
print("running micro benchmark:", " ".join(cmd))
env = os.environ.copy()
env["OMP_NUM_THREADS"] = "1"
subprocess.run(cmd, cwd=LF_ROOT, check=True, env=env)
bench_subdir = "teacher" if "teacher" in yaml_path.stem else "distill"
result_file = out_dir / bench_subdir / "train_results.json"
if not result_file.is_file():
raise RuntimeError(f"benchmark finished but {result_file} not found")
data = json.loads(result_file.read_text())
return float(data["train_steps_per_second"])
def fmt_hours(h: float) -> str:
if h < 1:
return f"{h * 60:.1f} 分钟"
if h < 24:
return f"{h:.1f} 小时"
return f"{h / 24:.1f} 天 ({h:.1f} 小时)"
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--epochs", type=int, default=30)
parser.add_argument("--samples", type=int, default=FULL_SAMPLES)
parser.add_argument("--micro-samples", type=int, default=0, help=">0 时先跑微基准再估算")
args = parser.parse_args()
if args.micro_samples > 0:
print("=== 微基准(较慢,但更准确)===")
t_sps = run_micro_benchmark(args.micro_samples, LF_ROOT / "examples/train_lora/qwen3_4b_teacher_lora.yaml")
d_sps = run_micro_benchmark(args.micro_samples, LF_ROOT / "examples/train_lora/qwen3_1.7b_distill_lora.yaml")
teacher_sec = t_sps
distill_sec = d_sps
else:
metrics = load_smoke_metrics()
if metrics is not None:
print("=== 使用冒烟指标外推(第六节已跑完)===")
teacher_sec = metrics["teacher"]["train_steps_per_second"]
distill_sec = metrics["distill"]["train_steps_per_second"]
else:
print("=== 使用内置默认速度外推(尚未跑第六节冒烟)===")
print(f" 教师 steps/s: {DEFAULT_TEACHER_STEPS_PER_SEC}(RTX 3090 参考值)")
print(f" 蒸馏 steps/s: {DEFAULT_DISTILL_STEPS_PER_SEC}(RTX 3090 参考值)")
print(" 完成第六节冒烟后可重新运行本脚本,将自动读取你的实测速度。")
teacher_sec = DEFAULT_TEACHER_STEPS_PER_SEC
distill_sec = DEFAULT_DISTILL_STEPS_PER_SEC
teacher = estimate_from_metrics(args.samples, args.epochs, teacher_sec)
distill = estimate_from_metrics(args.samples, args.epochs, distill_sec)
merge_sec = 60 # 两次 export 各约 30-45s
total_hours = teacher["hours"] + distill["hours"] + merge_sec / 3600
print("\n--- 估算结果 ---")
print(f"数据集规模: {args.samples:,} 条 × {args.epochs} epoch")
print(f"Step1 教师 LoRA: {teacher['optimizer_steps']:,} steps, {fmt_hours(teacher['hours'])}")
print(f"Step3 白盒蒸馏: {distill['optimizer_steps']:,} steps, {fmt_hours(distill['hours'])}")
print(f"Step2+4 合并: ~{merge_sec}s")
print(f"合计训练+合并: {fmt_hours(total_hours)}")
print("\n注意: 首次全量 tokenize 预处理额外 +10~30 分钟;磁盘需预留 merged-teacher ~7.6GB。")
if __name__ == "__main__":
main()
run_inference_compare.py
路径:run_inference_compare.py(副本:LLaMA-Factory/run_inference_compare.py)
python
"""对同一条 validation_prompt 比较基座 / 教师 LoRA / 蒸馏学生。"""
import argparse
import gc
from pathlib import Path
import torch
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
STUDENT_BASE = "/root/autodl-tmp/Distill2/models/models/Qwen--Qwen3-1.7B/snapshots/master"
TEACHER_BASE = "/root/autodl-tmp/Distill2/models/models/Qwen--Qwen3-4B-Instruct-2507/snapshots/master"
TEACHER_LORA = "/root/autodl-tmp/Distill2/LLaMA-Factory/saves/qwen3-4b/lora/teacher-sft"
STUDENT_MERGED = "/root/autodl-tmp/Distill2/LLaMA-Factory/saves/qwen3-1.7b/merged-student"
PROMPT_FILE = Path("/root/autodl-tmp/Distill2/validation_prompt.txt")
def generate(model, tokenizer, user_content: str, max_new_tokens: int = 200) -> str:
messages = [{"role": "user", "content": user_content}]
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer([text], return_tensors="pt").to(model.device)
with torch.no_grad():
outputs = model.generate(**inputs, max_new_tokens=max_new_tokens, do_sample=False)
return tokenizer.decode(outputs[0][inputs.input_ids.shape[1] :], skip_special_tokens=True).strip()
def run_once(label, loader):
print(f"\n{'=' * 20} {label} {'=' * 20}")
model, tokenizer = loader()
text = PROMPT_FILE.read_text(encoding="utf-8").strip()
print(generate(model, tokenizer, text)[:800])
del model, tokenizer
gc.collect()
torch.cuda.empty_cache()
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--student-merged", default=STUDENT_MERGED)
args = parser.parse_args()
run_once(
"基座 Qwen3-1.7B",
lambda: (
AutoModelForCausalLM.from_pretrained(
STUDENT_BASE, torch_dtype=torch.bfloat16, device_map="auto", trust_remote_code=True
),
AutoTokenizer.from_pretrained(STUDENT_BASE, trust_remote_code=True),
),
)
run_once(
"教师 Qwen3-4B + LoRA",
lambda: (
PeftModel.from_pretrained(
AutoModelForCausalLM.from_pretrained(
TEACHER_BASE, torch_dtype=torch.bfloat16, device_map="auto", trust_remote_code=True
),
TEACHER_LORA,
),
AutoTokenizer.from_pretrained(TEACHER_BASE, trust_remote_code=True),
),
)
run_once(
"蒸馏学生 merged",
lambda: (
AutoModelForCausalLM.from_pretrained(
args.student_merged, torch_dtype=torch.bfloat16, device_map="auto", trust_remote_code=True
),
AutoTokenizer.from_pretrained(args.student_merged, trust_remote_code=True),
),
)
if __name__ == "__main__":
main()
YAML 配置(8 个)
完整正文见正文 [第十一节 YAML 配置(全部 8 个)](#第十一节 YAML 配置(全部 8 个)):
| 文件 | 路径 | 用途 |
|---|---|---|
qwen3_4b_teacher_lora.yaml |
examples/train_lora/ |
Step1 冒烟 |
qwen3_4b_teacher_merge.yaml |
examples/merge_lora/ |
Step2 冒烟合并 |
qwen3_1.7b_distill_lora.yaml |
examples/train_lora/ |
Step3 冒烟蒸馏 |
qwen3_1.7b_student_merge.yaml |
examples/merge_lora/ |
Step4 冒烟合并 |
qwen3_4b_teacher_lora_full.yaml |
examples/train_lora/ |
Step1 正式 30 epoch |
qwen3_4b_teacher_merge_full.yaml |
examples/merge_lora/ |
Step2 正式合并 |
qwen3_1.7b_distill_lora_full.yaml |
examples/train_lora/ |
Step3 正式蒸馏 |
qwen3_1.7b_student_merge_full.yaml |
examples/merge_lora/ |
Step4 正式合并 |