基于千问的黑盒蒸馏 SOP
手把手教学: 从零到冒烟跑通的完整操作步骤见 基于千问的黑盒蒸馏操作教学文档
一、 课程核心知识点与理论背景
课程内容概览:
- 蒸馏的核心知识
- 蒸馏的环境准备
- 蒸馏的实战演练
核心概念与目标:
- 本质:知识转移,将大模型(教师)的知识迁移到小模型(学生)。
- 特点:参数量更小,计算更高效。
- 应用:模型压缩,边缘计算,低功耗场景。
- 技术路线:大模型 → 生成数据 → 微调 → 小模型。
- 方法分类:全量微调(所有参数都调,资源消耗大) 和 高效微调(如LoRA/QLoRA)。
- 蒸馏类型:本 SOP 采用的是黑盒蒸馏(Data Distillation),即通过 API 获取教师模型文本输出作为训练数据,而非对齐教师 logits/隐层。
蒸馏为何“突然火了”?
① 精力放在了应用上;
② 强大的基模开源了;
③ DeepSeek 公布了蒸馏技术方案(OpenAI 支持蒸馏但未公开技术方案)。
大模型强大的推理能力有何作用?
① 构建知识图谱;
② 作为 Agent 智能中枢;
③ 创建高质量合成数据,训练更强大的基模。
蒸馏的流程概述:
- 教师模型(如 DeepSeek-R1) → 生成软标签,创建高质量数据集 → 学生模型基于这些数据学习。
- 注:DeepSeek 公布了一系列蒸馏模型,如 1.5B, 7B, 8B, 14B, 32B, 70B, 671B。
DeepSeek 公布的模型蒸馏过程(三轮训练):
- 第一轮(Instruction 模型): 原始 Base 模型 → (借助推理问答数据进行冷启动有监督微调/SFT) → Instruction 模型。
- 第二轮(初级推理模型): Instruction 模型 → (借助 CoT 数据(思维链数据)进行强化学习 RL) → 初级推理模型。
- 第三轮(终极推理模型): 初级推理模型 → (借助推理问答数据,第二次有监督微调 SFT) → 终极推理模型。
注:SFT 全称 Supervised Fine-Tuning。
二、 微调工具与硬件资源评估
微调工具推荐:
- Unsloth:核心技术优势:① 显著提升微调效率;② 降低了硬件需求;③ 开源免费。
- LLaMA-Factory:① 广泛支持;② 高效的微调方法;③ 多模态任务支持;④ 实验监控;⑤ 运行速度快。
- MS-Swift(阿里系工具)。
微调资源与显存估算(大致的 x3 关系):
| 模型参数量 | Freeze | LoRA | QLoRA(INT8) | QLoRA(INT4) |
|---|---|---|---|---|
| 7B | 20 | 16 | 10 | 6 |
| 13B | 40 | 32 | 20 | 12 |
| 30B | 80 | 64 | 40 | 24 |
| 70B | 200 | 160 | 80 | 48 |
| 110B | 360 | 240 | 140 | - |
硬件最低配置推荐:
- RTX 4090:24GB
- RTX 4080:12GB
- RTX 3060:<12GB
- A100:40GB
- L40:48GB
- H100:80GB
云平台推荐:
- 除 AutoDL (https://www.autodl.com/) 外,还可使用 魔搭社区 (https://www.modelscope.cn/)。
三、 环境准备与模型下载
网络加速(AutoDL 必做):访问 GitHub 或 Hugging Face 前,先执行:
source /etc/network_turbo注意:开启后 pip 源等非 GitHub/HF 资源可能变慢,建议仅在 clone/下载模型时使用。
1. 下载 LLaMA-Factory 并安装依赖
工作目录(实测): /root/autodl-tmp/Distill(原 SOP 写 /autodl-tmp/DeepSeek-R1-Distill,AutoDL 实际挂载点为 /root/autodl-tmp/)
# 创建并进入工作目录
mkdir -p /root/autodl-tmp/Distill
cd /root/autodl-tmp/Distill
# 克隆 LLaMA-Factory 仓库(--depth 1 只下载最新版本)
source /etc/network_turbo
git clone --depth 1 https://github.com/hiyouga/LLaMA-Factory.git
# 进入 LLaMA-Factory 目录安装依赖
cd LLaMA-Factory
pip install -e ".[torch,metrics]"
实测问题与解决:
| 问题 | 现象 | 解决方案 |
|---|---|---|
| torchaudio 版本冲突 | OSError: libcudart.so.13: cannot open shared object file |
pip install 会拉取 torchaudio 2.11.0(需 CUDA 13),与系统 PyTorch 2.8.0+cu128 不兼容。执行:pip install "torchaudio==2.8.0" --index-url https://download.pytorch.org/whl/cu128 |
| OMP 警告 | libgomp: Invalid value for environment variable OMP_NUM_THREADS |
不影响运行,可忽略;或 export OMP_NUM_THREADS=1 |
2. 下载开源模型 Qwen2.5-1.5B-Instruct
(注:Model ID 非常重要,需精确填写)
首先在魔搭社区搜索 Qwen2.5-1.5B-Instruct 获取可复制的精确 ID:Qwen/Qwen2.5-1.5B-Instruct。
创建 download.py 并填入以下内容:
from modelscope import snapshot_download
model_id = "Qwen/Qwen2.5-1.5B-Instruct"
local_dir = "/root/autodl-tmp/Distill/models"
model_dir = snapshot_download(model_id, cache_dir=local_dir)
print(f"Model downloaded to: {model_dir}")
注:需先安装必要的依赖:pip install modelscope(LLaMA-Factory 安装时已包含)
实测下载路径:
/root/autodl-tmp/Distill/models/models/Qwen--Qwen2.5-1.5B-Instruct/snapshots/master
ModelScope 会在 cache_dir 下自动创建 models/Qwen--... 嵌套目录,配置训练时须使用完整绝对路径。
四、 蒸馏数据集的生成与评估策略
核心思路: 让教师模型(DeepSeek-R1)回答这些数学/逻辑问题,然后使用这些高质量数据去教学生模型(1.5B)。
0. 配置 API Key
在项目根目录创建 .env(勿提交到公开仓库):
DEEPSEEK_API_KEY=sk-your-key-here
或临时导出:
export DEEPSEEK_API_KEY=sk-your-key-here
1. 步骤一:NuminaMath + DeepSeek-R1 生成软标签
- 使用数据集:NuminaMath CoT (Hugging Face:
AI-MO/NuminaMath-CoT) - 字段说明:
problem(题目,送给 R1)、solution(标准解答,留给 V3 裁判)、source(数据来源) - 原则:只将
problem发给 R1,不把solution给教师模型 - 教师模型:
deepseek-reasoner,提取reasoning_content(思维链)+content(最终答案) - 脚本:
step1_generate_r1.py
source /etc/network_turbo # 加载 NuminaMath 需要
cd /root/autodl-tmp/Distill
python step1_generate_r1.py --num-samples 5 # 冒烟 5 条;正式设 17000
中间产物:/root/autodl-tmp/data/r1_raw_outputs.jsonl
每条记录包含:problem、ground_truth_solution、reasoning_content、content、assistant_text(思维链+答案合并,供训练使用)。
R1 API 调用核心代码(已封装在脚本中):
from openai import OpenAI
client = OpenAI(api_key="...", base_url="https://api.deepseek.com")
response = client.chat.completions.create(
model="deepseek-reasoner",
messages=[
{"role": "system", "content": "你是一个数学推理助手,请仔细思考并给出解答。"},
{"role": "user", "content": problem}, # 仅传 problem,不传 solution
],
stream=False,
timeout=180,
)
msg = response.choices[0].message
reasoning = msg.reasoning_content # 思维链
answer = msg.content # 最终回答
2. 步骤二:DeepSeek-V3 评判与数据清洗
- 裁判模型:
deepseek-chat(DeepSeek-V3) - 逻辑:将 R1 的
assistant_text与 NuminaMath 的solution(标准解答)一并交给 V3,判断最终答案是否数学等价 - 保留:
judge_correct: true的样本,转为 sharegpt 格式写入 arrow - 脚本:
step2_judge_v3.py
python step2_judge_v3.py
中间产物:/root/autodl-tmp/data/v3_judged_outputs.jsonl(含评判理由)
最终产物:/root/autodl-tmp/data/Distil-data-17k-train.arrow
3. 一键运行完整流水线
source /etc/network_turbo
export DEEPSEEK_API_KEY=sk-your-key-here # 或写入 .env
cd /root/autodl-tmp/Distill
python run_distill_pipeline.py --num-samples 5
数据格式(sharegpt):
{
"system": "你是一个智能助手",
"conversations": [
{"from": "user", "value": "<NuminaMath problem>"},
{"from": "assistant", "value": "<R1 reasoning_content>\\n\\n<R1 content>"}
]
}
冒烟实测结果(2026-07-08):
- NuminaMath 加载 5 条,R1 成功 4 条(1 条超时,已加重试机制)
- V3 评判 4/4 全部通过
- 最终 arrow 含 4 条真实蒸馏数据(含 R1 思维链)
实测问题与解决:
| 问题 | 现象 | 解决方案 |
|---|---|---|
| NuminaMath 下载慢 | HF 首次加载卡顿 | 先 source /etc/network_turbo;使用 streaming=True 逐条读取 |
| R1 请求超时 | Request timed out |
step1 已加 timeout=180 和 3 次重试;复杂题可适当增大 timeout |
| V3 评判路径错误 | AttributeError: 'str' object has no attribute 'open' |
distill_utils.load_jsonl 已支持 str/Path |
| 冒烟样本量 | 5 条耗时约 1-2 分钟 | 冒烟 --num-samples 5;正式 --num-samples 17000 预计数小时+大量 API 费用 |
五、 全量微调实战配置与执行
1. 硬件与镜像要求
- 推荐服务器配置:L20 显卡 * 1片,20核心,100GB 内存,30GB 显存,50GB SSD。
- 镜像:PyTorch 2.1.0 / 3.10 (Ubuntu 22.04 / 12.1)。
- 预估耗时:约 3.87 小时 / 整体 15 小时左右。
- 实测环境:RTX 3090 24GB,冒烟 2 step 约 15 秒(DeepSpeed ZeRO-3 offload)。
2. 修改 LLaMA-Factory 的 dataset_info.json
文件路径:LLaMA-Factory/data/dataset_info.json(非根目录下的 dataset_info.json)
添加自定义数据集配置:
"Distil": {
"file_name": "/root/autodl-tmp/data/Distil-data-17k-train.arrow",
"formatting": "sharegpt",
"columns": {
"messages": "conversations",
"system": "system"
},
"tags": {
"role_tag": "from",
"content_tag": "value",
"user_tag": "user",
"assistant_tag": "assistant"
}
}
3. 创建全量微调配置文件
在 LLaMA-Factory/examples/train_full/ 目录下创建 qwen2-full-sft.yaml:
### model
model_name_or_path: /root/autodl-tmp/Distill/models/models/Qwen--Qwen2.5-1.5B-Instruct/snapshots/master
trust_remote_code: true
### method
stage: sft
do_train: true
finetuning_type: full
deepspeed: examples/deepspeed/ds_z3_offload_config.json # 24GB 显卡必须加
### dataset
dataset: Distil
template: qwen
cutoff_len: 2048
max_samples: 1000
overwrite_cache: true
preprocessing_num_workers: 4
dataloader_num_workers: 2
### output
output_dir: /root/autodl-tmp/Distill/saves/qwen2.5-1.5b/smoke-sft
logging_steps: 1
save_steps: 100
plot_loss: true
overwrite_output_dir: true
save_only_model: true # 避免保存 DeepSpeed 优化器状态(约 12GB)
report_to: none
### train
per_device_train_batch_size: 1
gradient_accumulation_steps: 1
learning_rate: 1.0e-5
max_steps: 2 # 冒烟测试用 2;正式训练改为 num_train_epochs: 3.0
lr_scheduler_type: cosine
warmup_ratio: 0.1
bf16: true
ddp_timeout: 180000000
关键配置说明:
template: qwen:Qwen2.5 系列使用qwen模板(非qwen3)deepspeed: ds_z3_offload_config.json:24GB 显卡全量微调 1.5B 必须启用 ZeRO-3 + CPU offload,否则 OOMsave_only_model: true:仅保存模型权重,避免 DeepSpeed 优化器 checkpoint 占满磁盘- 正式训练时:删除
max_steps,改为num_train_epochs: 3.0;output_dir改为正式路径
4. 运行微调命令
cd /root/autodl-tmp/Distill/LLaMA-Factory
# 单卡 24GB:必须加 DeepSpeed + torchrun
pip install deepspeed # 若未安装
FORCE_TORCHRUN=1 NNODES=1 NODE_RANK=0 MASTER_PORT=29501 \
llamafactory-cli train examples/train_full/qwen2-full-sft.yaml
注:原 SOP 预计 15 小时(17k 数据 × 3 epoch)。冒烟测试 2 step 约 15 秒即通过。
实测问题与解决:
| 问题 | 现象 | 解决方案 |
|---|---|---|
| 全量微调 OOM | RTX 3090 24GB,CUDA out of memory 在 optimizer.step() |
添加 deepspeed: examples/deepspeed/ds_z3_offload_config.json 并 pip install deepspeed;使用 FORCE_TORCHRUN=1 启动 |
| 磁盘空间不足 | 训练完成后保存 checkpoint 失败,file write failed,磁盘 100% |
设置 save_only_model: true;确保 SSD 至少预留 10GB(模型 3GB + checkpoint 3GB);清理 global_step* 优化器目录 |
| 训练后退出码 1 | 2 step 训练成功、model.safetensors 已保存,但 DeepSpeed 优化器写入失败 | 冒烟可通过;设置 save_only_model: true 后重跑可完全成功 |
冒烟实测结果:
loss: 0.9479 → 1.05(2 steps)
checkpoint: /root/autodl-tmp/Distill/saves/qwen2.5-1.5b/smoke-sft/checkpoint-2/model.safetensors
六、 微调后的模型效果验证
为了突出微调之后的作用,在测试阶段提出一个专业领域问题,如数学领域。
推理测试脚本(test_inference.py):
from transformers import AutoModelForCausalLM, AutoTokenizer
import os
MODEL_PATH = "/root/autodl-tmp/Distill/saves/qwen2.5-1.5b/smoke-sft/checkpoint-2"
BASE_PATH = "/root/autodl-tmp/Distill/models/models/Qwen--Qwen2.5-1.5B-Instruct/snapshots/master"
model_name = MODEL_PATH if os.path.exists(os.path.join(MODEL_PATH, "model.safetensors")) else BASE_PATH
model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype="auto", device_map="auto", low_cpu_mem_usage=True)
tokenizer = AutoTokenizer.from_pretrained(model_name)
system_prompt = "你是一名助人为乐的助手。"
user_prompt = "请证明根号2是无理数。"
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
model_inputs = tokenizer([text], return_tensors="pt").to(model.device)
generated_ids = model.generate(**model_inputs, max_new_tokens=256)
generated_ids = [
output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
]
response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
print(response)
cd /root/autodl-tmp/Distill
python test_inference.py
冒烟实测: 模型成功加载微调 checkpoint,对「证明根号 2 是无理数」给出反证法推理回答,pipeline 端到端通过。
七、 Python 脚本设计与代码说明
本节详细说明项目中各 Python 文件的构建思路、执行流程和关键代码解释。
7.1 整体架构
项目采用「公共工具层 + 分步脚本 + 流水线编排」三层结构:
设计原则:
| 原则 | 说明 |
|---|---|
| 关注点分离 | 数据生成(step1)、质量过滤(step2)、格式转换(utils)各自独立 |
| 中间产物可追溯 | 每步输出 jsonl,便于排查 R1 生成质量、V3 误判 |
| 冒烟/正式统一入口 | run_distill_pipeline.py 通过 --num-samples 控制规模 |
| 配置外置 | API Key 放 .env 或环境变量,不写死在代码里 |
脚本依赖关系:
run_distill_pipeline.py
├── step1_generate_r1.py → distill_utils.py → r1_raw_outputs.jsonl
└── step2_judge_v3.py → distill_utils.py → v3_judged_outputs.jsonl
→ Distil-data-17k-train.arrow
download.py → 学生模型权重(独立,训练前执行)
test_inference.py → 微调后验证(独立,训练后执行)
7.2 distill_utils.py — 公共工具模块
构建思路: 将 step1 和 step2 重复使用的逻辑(API 鉴权、数据集加载、文件读写、格式转换)抽取到单一模块,避免代码重复,也方便后续扩展(如换数据集、换输出格式)。
7.2.1 路径常量
DATA_DIR = Path("/root/autodl-tmp/data")
R1_RAW_FILE = DATA_DIR / "r1_raw_outputs.jsonl" # step1 输出
DISTIL_ARROW = DATA_DIR / "Distil-data-17k-train.arrow" # step2 最终输出
SYSTEM_PROMPT = "你是一个智能助手" # sharegpt 格式的 system 字段
三个路径约定了整个流水线的数据落盘位置,LLaMA-Factory 的 dataset_info.json 直接指向 DISTIL_ARROW。
7.2.2 get_api_key() — API Key 读取
def get_api_key() -> str:
key = os.environ.get("DEEPSEEK_API_KEY") # 优先读环境变量
if not key and Path(".../.env").exists(): # 其次读 .env 文件
for line in Path(".../.env").read_text().splitlines():
if line.startswith("DEEPSEEK_API_KEY="):
key = line.split("=", 1)[1].strip()
if not key:
raise RuntimeError("请设置 DEEPSEEK_API_KEY ...")
return key
设计要点: 支持两种配置方式(环境变量 / .env),优先级明确;缺失时立即报错,避免跑到一半才发现鉴权失败。
7.2.3 get_client() — DeepSeek API 客户端
def get_client():
from openai import OpenAI
return OpenAI(api_key=get_api_key(), base_url="https://api.deepseek.com")
DeepSeek API 兼容 OpenAI SDK 格式,因此直接使用 openai 库,base_url 指向 DeepSeek 端点。step1 和 step2 共用同一个 client 工厂函数。
7.2.4 load_numinamath_samples() — 加载 NuminaMath 数据集
def load_numinamath_samples(num_samples: int):
ds = load_dataset("AI-MO/NuminaMath-CoT", split="train", streaming=True)
samples = []
for i, row in enumerate(ds):
samples.append({
"id": i,
"source": row.get("source", ""), # 数据来源标签(如 cn_k12, olympiads)
"problem": row["problem"], # 仅取题目,送给 R1
"solution": row["solution"], # 标准解答,留给 V3 裁判,不给 R1
})
if len(samples) >= num_samples:
break
return samples
关键设计:
- 使用
streaming=True:数据集约 86 万条、1.23GB,流式读取避免全量下载,冒烟时只取前 N 条。 - 严格隔离 problem 和 solution:
problem送给教师模型 R1,solution仅用于 V3 评判,确保是「模型教模型」而非泄露答案。
NuminaMath-CoT 字段说明:
| 字段 | 用途 | 传给谁 |
|---|---|---|
problem |
数学题目(英文) | DeepSeek-R1 |
solution |
标准 CoT 解答 | DeepSeek-V3(评判用) |
source |
数据来源分类 | 日志记录 |
messages |
预制对话格式 | 不使用(我们自己构建 sharegpt) |
7.2.5 build_assistant_text() — 合并思维链与答案
def build_assistant_text(reasoning: str | None, content: str) -> str:
reasoning = (reasoning or "").strip()
content = (content or "").strip()
if reasoning and content:
return f"{reasoning}\n\n{content}" # 思维链 + 最终答案
return reasoning or content
DeepSeek-R1 返回两个字段:
reasoning_content:内部推理过程(思维链 CoT)content:格式化后的最终回答
合并后作为 sharegpt 中 assistant 的 value,让学生模型学到完整的推理链。
7.2.6 save_jsonl() / load_jsonl() — 中间产物读写
def save_jsonl(path, records):
path = Path(path) # 兼容 str 和 Path
path.parent.mkdir(parents=True, exist_ok=True) # 自动创建目录
with path.open("w", encoding="utf-8") as f:
for rec in records:
f.write(json.dumps(rec, ensure_ascii=False) + "\n") # 每行一条 JSON
选择 JSONL(每行一个 JSON 对象)而非单个 JSON 数组,好处是:
- 追加写入方便(断点续跑时可扩展)
- 单行损坏不影响其他记录
- 大文件逐行读取,内存友好
7.2.7 save_sharegpt_arrow() — 导出 LLaMA-Factory 可读格式
def save_sharegpt_arrow(samples, output_file=DISTIL_ARROW):
table = pa.Table.from_pydict({
"system": [s["system"] for s in samples],
"conversations": [s["conversations"] for s in samples],
})
with pa.OSFile(str(output_file), "wb") as sink:
with ipc.new_file(sink, table.schema) as writer:
writer.write_table(table)
将 sharegpt 格式转为 Apache Arrow IPC 文件。LLaMA-Factory 的 dataset_info.json 中 formatting: "sharegpt" 配合列映射,可直接加载此 arrow 文件进行训练。
7.3 step1_generate_r1.py — 教师模型生成软标签
构建思路: 这是蒸馏流水线的第一步——黑盒蒸馏的核心。从 NuminaMath 取纯问题,让 DeepSeek-R1(教师)独立推理,产出带思维链的高质量回答。
执行流程:
加载 NuminaMath 前 N 条 problem
↓ 逐条调用
DeepSeek-R1 API (deepseek-reasoner)
↓ 提取
reasoning_content + content → 合并为 assistant_text
↓ 连同 ground_truth_solution 一起
保存至 r1_raw_outputs.jsonl
7.3.1 call_r1() — 调用 R1 并重试
def call_r1(client, problem: str, max_retries: int = 3) -> dict:
for attempt in range(max_retries):
try:
response = client.chat.completions.create(
model="deepseek-reasoner", # R1 推理模型
messages=[
{"role": "system", "content": "你是一个数学推理助手,请仔细思考并给出解答。"},
{"role": "user", "content": problem}, # 只传题目,不传答案
],
stream=False,
timeout=180, # R1 推理较慢,给 3 分钟
)
msg = response.choices[0].message
return {
"reasoning_content": getattr(msg, "reasoning_content", None),
"content": msg.content,
"assistant_text": build_assistant_text(...),
}
except Exception as e:
time.sleep(2 * (attempt + 1)) # 指数退避:2s, 4s, 6s
raise last_err
设计要点:
deepseek-reasoner是 DeepSeek-R1 的 API 模型名,区别于 V3 的deepseek-chatreasoning_content通过getattr安全获取(兼容不同 SDK 版本)- 3 次重试 + 指数退避:R1 对复杂数学题推理耗时较长,实测有超时情况
timeout=180:冒烟中 id=2 曾因默认超时失败,增大后改善
7.3.2 main() — 主流程
def main():
samples = load_numinamath_samples(args.num_samples) # 1. 加载题目
client = get_client() # 2. 初始化 API
records = []
for sample in samples:
r1 = call_r1(client, sample["problem"]) # 3. 逐条调用 R1
records.append({
"id": sample["id"],
"source": sample["source"],
"problem": sample["problem"],
"ground_truth_solution": sample["solution"], # 保留标准答案给 step2
"reasoning_content": r1["reasoning_content"],
"content": r1["content"],
"assistant_text": r1["assistant_text"],
})
time.sleep(0.5) # 限速,避免 API 限流
save_jsonl(output, records) # 4. 保存中间产物
输出文件 r1_raw_outputs.jsonl 单条记录示例:
{
"id": 0,
"source": "synthetic_math",
"problem": "Consider the terms of an arithmetic sequence: ...",
"ground_truth_solution": "For an arithmetic sequence... \\boxed{\\frac{13}{6}}",
"reasoning_content": "我们被问到...所以答案是13/6...",
"content": "在等差数列中...\\boxed{\\dfrac{13}{6}}",
"assistant_text": "<reasoning_content>\\n\\n<content>"
}
7.4 step2_judge_v3.py — V3 裁判与数据清洗
构建思路: 教师模型也会犯错。用 DeepSeek-V3 作为「评判器」,将 R1 的回答与 NuminaMath 标准解答对比,只保留数学上正确的样本。这是数据质量控制的关键环节。
执行流程:
读取 r1_raw_outputs.jsonl
↓ 逐条调用
DeepSeek-V3 API (deepseek-chat) + 评判 Prompt
↓ 解析 JSON 结果
judge_correct == true → 转为 sharegpt 格式
↓
保存 v3_judged_outputs.jsonl(全部评判记录)
保存 Distil-data-17k-train.arrow(仅通过样本)
7.4.1 JUDGE_PROMPT — 评判提示词
JUDGE_PROMPT = """你是一个数学答案评判器。请判断「模型回答」与「标准解答」在数学上是否一致(最终答案正确即可,推理过程可不同)。
【题目】{problem}
【标准解答】{ground_truth}
【模型回答】{model_answer}
请只输出 JSON,不要输出其他内容:
{{"correct": true或false, "reason": "一句话说明"}}"""
设计要点:
- 明确评判标准:最终答案正确即可,推理过程可不同(避免因表述差异误杀)
- 要求 JSON 输出,便于程序解析
temperature=0:评判需要确定性,不需要创造性
7.4.2 parse_judge_result() — 容错解析
def parse_judge_result(text: str) -> dict:
try:
return json.loads(text) # 直接解析
except json.JSONDecodeError:
match = re.search(r"\{.*\}", text, re.DOTALL) # 从混合文本中提取 JSON
if match:
return json.loads(match.group())
return {"correct": False, "reason": "无法解析"} # 解析失败视为不通过
V3 有时会在 JSON 前后加说明文字,正则提取 {...} 做容错。解析失败时默认不通过(保守策略,保证训练数据质量)。
7.4.3 to_sharegpt() — 格式转换
def to_sharegpt(record: dict) -> dict:
return {
"system": SYSTEM_PROMPT,
"conversations": [
{"from": "user", "value": record["problem"]},
{"from": "assistant", "value": record["assistant_text"]},
],
}
将通过评判的样本转为 LLaMA-Factory 要求的 sharegpt 格式:
from: "user"/from: "assistant"对应dataset_info.json中的user_tag/assistant_tagvalue对应content_tag
7.4.4 main() — 主流程
def main():
records = load_jsonl(args.input) # 1. 读取 step1 产物
for record in records:
result = judge_one(client, record) # 2. V3 逐条评判
judged.append(result)
if result["judge_correct"]:
accepted.append(to_sharegpt(result)) # 3. 通过的转 sharegpt
save_jsonl(JUDGED_FILE, judged) # 4. 保存全部评判记录
save_sharegpt_arrow(accepted, output) # 5. 导出 arrow 训练文件
冒烟实测: 4 条 R1 输出全部通过 V3 评判(4/4),说明 R1 在 NuminaMath 数学题上质量较高。
7.5 run_distill_pipeline.py — 流水线编排
构建思路: 最薄的一层编排脚本,用 subprocess 按顺序调用 step1 和 step2,自身不包含业务逻辑。
def main():
steps = [
[sys.executable, "step1_generate_r1.py", "--num-samples", str(args.num_samples)],
[sys.executable, "step2_judge_v3.py"],
]
for cmd in steps:
subprocess.run(cmd, check=True, cwd="/root/autodl-tmp/Distill")
为什么用 subprocess 而非 import?
- 每步独立进程,一步崩溃不影响另一步的中间产物
- 可以单独重跑某一步(如 R1 完成后只需
python step2_judge_v3.py) - 日志清晰,每步有明确的
[Step1]/[Step2]前缀
使用方式:
# 冒烟(5 条,约 1-2 分钟)
python run_distill_pipeline.py --num-samples 5
# 正式(17000 条,数小时 + 大量 API 费用)
python run_distill_pipeline.py --num-samples 17000
7.6 download.py — 学生模型下载
构建思路: 独立脚本,与蒸馏流水线无关,负责从魔搭社区下载学生模型 Qwen2.5-1.5B-Instruct。
from modelscope import snapshot_download
model_id = "Qwen/Qwen2.5-1.5B-Instruct"
local_dir = "/root/autodl-tmp/Distill/models"
model_dir = snapshot_download(model_id, cache_dir=local_dir)
- 使用 ModelScope 而非 HuggingFace:国内 AutoDL 环境下载更快,无需
network_turbo cache_dir指定缓存目录,ModelScope 会自动创建嵌套路径- 下载后须在训练 yaml 中使用完整绝对路径(见第三节实测路径)
7.7 test_inference.py — 微调效果验证
构建思路: 训练完成后的推理冒烟脚本,验证微调 checkpoint 能否正常加载并生成回答。
执行流程:
检查微调 checkpoint 是否存在
↓ 存在则用 checkpoint,否则回退基座模型
加载 model + tokenizer
↓
构造 messages(system + user)
↓
apply_chat_template → tokenize → generate
↓
截断 input 部分,decode 输出
关键代码解释:
# 优先微调模型,回退基座模型
model_name = MODEL_PATH if os.path.exists(..., "model.safetensors") else BASE_PATH
# 使用 Qwen 聊天模板格式化输入
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
# 只解码新生成的 token(去掉输入部分)
generated_ids = [
output_ids[len(input_ids):]
for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
]
apply_chat_template:自动添加<|im_start|>system等特殊 token,与训练时格式一致add_generation_prompt=True:在末尾添加<|im_start|>assistant,提示模型开始生成- 截断输入 token:避免把 prompt 也 decode 出来
7.8 完整数据流与文件产物
NuminaMath-CoT (HF streaming)
│
│ problem only
▼
step1_generate_r1.py ──→ /root/autodl-tmp/data/r1_raw_outputs.jsonl
│ (含 problem, ground_truth, reasoning, content)
│ assistant_text + ground_truth
▼
step2_judge_v3.py ──→ /root/autodl-tmp/data/v3_judged_outputs.jsonl
│ (含 judge_correct, judge_reason)
│ 仅 judge_correct=true
▼
Distil-data-17k-train.arrow ──→ LLaMA-Factory dataset_info.json (Distil)
│
▼
qwen2-full-sft.yaml ──→ llamafactory-cli train ──→ checkpoint-N/
│
▼
test_inference.py ──→ 验证生成质量
产物文件格式对照:
| 文件 | 格式 | 内容 | 下游消费者 |
|---|---|---|---|
r1_raw_outputs.jsonl |
JSONL | R1 原始输出 + 标准答案 | step2 输入 |
v3_judged_outputs.jsonl |
JSONL | 评判结果 + 通过/拒绝理由 | 人工审查 |
Distil-data-17k-train.arrow |
Arrow IPC | sharegpt 格式训练数据 | LLaMA-Factory |
checkpoint-N/model.safetensors |
SafeTensors | 微调后模型权重 | test_inference.py |
7.9 扩展与正式训练建议
| 场景 | 修改方式 |
|---|---|
| 增大数据量 | run_distill_pipeline.py --num-samples 17000 |
| R1 超时频繁 | 增大 call_r1() 中 timeout(如 300)或 max_retries(如 5) |
| 断点续跑 step1 | 读取已有 jsonl 的 id 集合,跳过已生成条目(需自行扩展) |
| 换评判模型 | 修改 step2_judge_v3.py 中 model="deepseek-chat" |
| 并发生成 | 在 step1 中用 concurrent.futures 并行调用 R1(注意 API 限流) |
| 正式训练 | yaml 中删除 max_steps,改 num_train_epochs: 3.0 |
八、 实施记录与文件清单(冒烟测试 2026-07-08)
| 文件 | 用途 |
|---|---|
SOP.md |
本文档 |
.env |
DeepSeek API Key(本地配置,勿公开) |
distill_utils.py |
蒸馏流水线公共工具(API 客户端、arrow 保存等) |
step1_generate_r1.py |
NuminaMath 问题 → DeepSeek-R1 生成软标签 |
step2_judge_v3.py |
DeepSeek-V3 评判 → 导出 arrow |
run_distill_pipeline.py |
一键运行步骤1+2 |
download.py |
ModelScope 下载 Qwen2.5-1.5B-Instruct |
test_inference.py |
微调后推理验证 |
generate_smoke_data.py |
run_distill_pipeline.py |
LLaMA-Factory/ |
微调框架(clone) |
LLaMA-Factory/data/dataset_info.json |
注册 Distil 数据集 |
LLaMA-Factory/examples/train_full/qwen2-full-sft.yaml |
全量微调配置 |
/root/autodl-tmp/data/r1_raw_outputs.jsonl |
R1 原始输出(中间产物) |
/root/autodl-tmp/data/v3_judged_outputs.jsonl |
V3 评判结果(中间产物) |
/root/autodl-tmp/data/Distil-data-17k-train.arrow |
蒸馏训练数据(最终产物) |
saves/qwen2.5-1.5b/smoke-sft/checkpoint-2/ |
冒烟微调产出 |
冒烟 Checklist:
- LLaMA-Factory 克隆与安装
- Qwen2.5-1.5B-Instruct 下载
- NuminaMath-CoT 加载
- DeepSeek-R1 生成软标签(4/5 条,含思维链)
- DeepSeek-V3 评判清洗(4/4 通过)
- Distil arrow 数据导出
- dataset_info.json 配置
- 全量微调 2 step 运行成功
- 推理脚本输出正常
正式训练待办:
-
run_distill_pipeline.py --num-samples 17000生成完整数据 - 确保磁盘 ≥ 20GB 可用空间
- 将
max_steps: 2改为num_train_epochs: 3.0,预计耗时 ~15h
更多推荐





所有评论(0)