open-r1(deepseek-R1)训练代码逐文件解析
open-r1 训练代码逐文件解析
版本:基于
huggingface/open-r1主分支公开仓库整理
目标:讲清楚 open-r1 的目录结构、主调用链、关键脚本职责,以及它与 DeepSeek-R1 官方论文之间的对应关系。
1. open-r1 是什么
open-r1 的官方定位很明确:
A fully open reproduction of DeepSeek-R1
README 写得很直接:这个项目的目标是把 R1 管线里官方没有完整公开的部分补出来,让社区可以在透明环境中复现并扩展 R1 路线。
README 对工程主体的概括也很清楚:
src/open_r1/grpo.py:做 GRPO 强化学习训练src/open_r1/sft.py:做监督微调src/open_r1/generate.py:做 synthetic data generationMakefile:把安装、开发检查等常用动作简化recipes/:存各种可直接运行的配置slurm/:集群训练与评测脚本
因此,open-r1 本质上是一套“R1-like post-training 工程框架”,而不是只放论文结论的模型仓库。
2. 根目录结构总览
根据当前仓库结构,根目录可以抽象成下面这样:
open-r1/ ├── Makefile ├── README.md ├── recipes/ ├── scripts/ ├── slurm/ ├── src/open_r1/ │ ├── configs.py │ ├── sft.py │ ├── grpo.py │ ├── generate.py │ ├── rewards.py │ └── utils/ └── tests/理解这个结构最关键的是:
src/open_r1/:训练逻辑核心recipes/:实验配置与配方slurm/:分布式/多节点调度utils/:数据、模型、评测、回调、代码执行沙盒等公共组件
3. 整体调用链:从 README 到训练入口
open-r1 README 已经把三条主线写得很清楚:
3.1 SFT 主线
accelerate launch--config_filerecipes/accelerate_configs/zero3.yaml\src/open_r1/sft.py\--configrecipes/OpenR1-Distill-7B/sft/config_distill.yaml3.2 GRPO 主线
accelerate launch--config_filerecipes/accelerate_configs/zero3.yaml\src/open_r1/grpo.py\--configrecipes/DeepSeek-R1-Distill-Qwen-1.5B/grpo/config_demo.yaml\--vllm_modecolocate3.3 生成数据主线
generate.py负责把模型接成一个 Distilabel pipeline,批量生成训练数据或蒸馏 traces。
所以真正的工程骨架是:
recipes/*.yaml -> TrlParser 解析 -> configs.py dataclass -> sft.py / grpo.py / generate.py -> utils/*5. Makefile:最小开发入口
Makefile很短,但很实用。它主要负责:
install:初始化uv环境、安装 vLLM、flash-attn、开发依赖;style:格式化;quality:静态检查。
代码不复杂,但有两个工程含义:
5.1 它把 repo 默认的开发环境固定下来了
例如:
- Python 3.11
vllm==0.8.5.post1flash-attn.[dev]开发依赖安装方式
5.2 它强调了 repo 是“可开发工程”,不是只读仓库
你不是只把它当模型说明看,而是按这个 Makefile 去建环境和开发。
6. recipes/:把“实验思想”具体化成配置
recipes/是 open-r1 非常关键的目录。它把抽象训练思想变成了“某个模型 + 某个任务 + 某个配置”的可运行配方。
README 和 recipes 目录显示,当前比较重要的条目包括:
OpenR1-Distill-7B/sftDeepSeek-R1-Distill-Qwen-1.5B/grpoQwen2.5-1.5B-Instruct/grpoQwen2.5-Coder-7B-Instruct/grpoaccelerate_configsdataset_filtering
从工程视角看,recipes/解决的是:
同一套训练脚本,如何快速切换不同实验配置。
也就是说,open-r1 的脚本本身是“通用训练引擎”,而 recipes 才是“具体实验实例”。
7. slurm/:多节点训练与评测调度层
README 说明,open-r1 提供了slurm/train.slurm来启动训练作业。
其意图不是重写训练逻辑,而是把下面这些因素统一起来:
- 节点数
- data parallel / tensor parallel
- vLLM server 节点
- 训练节点
- benchmark 节点
- accelerate/deepspeed/fsdp 等不同后端
这意味着 open-r1 的系统分层是:
训练逻辑层:src/open_r1/*.py 实验配置层:recipes/*.yaml 集群调度层:slurm/*.slurm这是一个非常标准、也很适合大模型工程扩展的分层方式。
8. src/open_r1/configs.py:参数系统中枢
configs.py是 open-r1 的参数核心,几乎所有脚本都会经过它。
8.1 ScriptArguments:统一数据集入口
ScriptArguments继承自trl.ScriptArguments,但扩展了一个很重要的能力:
支持 dataset mixture。
它允许你不是只加载单个数据集,而是把多个数据集按权重、列配置、随机种子混合起来。
这对 reasoning 训练非常关键,因为真实训练往往不会只喂一个纯数学集,而会混合:
- math
- code
- science
- reasoning QA
- synthetic traces
所以它是 open-r1 能够做“多源 reasoning 训练”的前提。
8.2 GRPOConfig / SFTConfig:训练配置扩展
GRPOConfig继承trl.GRPOConfig,增加了:
benchmarkscallbackschat_templatehub_model_revisionsystem_promptwandb_*
SFTConfig继承trl.SFTConfig,也加入了类似字段。
它们的意义是:
- TRL 自带 config 只管“训练”;
- open-r1 额外把“评测、回调、Hub 推送、聊天模板、wandb 记录”统一纳入配置层。
8.3 GRPOScriptArguments:把 reward 系统参数化
GRPOScriptArguments是 GRPO 专属参数集。
它最关键的一点是:reward 几乎都通过名字字符串来启用,例如:
accuracyformatreasoning_stepscosinerepetition_penaltylengthtag_countcodeioi_codecode_formatsoft_overlong_punishment
这意味着:
grpo.py本身不硬编码某一类 reward;- 真正的 reward 组合是在配置层决定的;
- 同一套 GRPOTrainer 可以很容易切到 math/code/format 等不同任务。
9. src/open_r1/sft.py:监督微调主入口
sft.py的职责非常纯粹:
把一个 base model 用某个 reasoning 数据集做 SFT。
9.1 它导入了什么
核心导入包括:
ScriptArguments, SFTConfigget_dataset, get_model, get_tokenizerget_callbacksinit_wandb_trainingtrl.SFTTrainer
这说明 sft.py 本身更像“装配脚本”,而不是把所有逻辑写死在一个文件里。
9.2 主流程
sft.py的主流程大致是:
解析参数 -> 设置日志与 seed -> 检查 checkpoint -> 初始化 wandb -> 加载 dataset -> 加载 tokenizer -> 加载 model -> 如果 tokenizer 没有 chat_template,则默认切到 ChatML -> 初始化 SFTTrainer -> train() -> save / log / push / benchmark9.3 为什么要自动处理 chat template
sft.py里有一段很关键的逻辑:
- 如果 tokenizer 没有 chat template,就默认使用 ChatML;
- 如果用户在 config 里传入了
chat_template,则会覆盖 tokenizer 原配置。
这很重要,因为 reasoning SFT 极度依赖消息格式是否一致。
如果训练时模板和推理时模板不一致,很容易出现:
- EOS 位置错乱;
<think>与 answer 区域错位;- 训练损失正常但推理风格异常。
9.4 SFTTrainer 初始化
sft.py的核心调用非常直接:
trainer=SFTTrainer(model=model,args=training_args,train_dataset=...,eval_dataset=...,processing_class=tokenizer,peft_config=get_peft_config(model_args),callbacks=get_callbacks(training_args,model_args),)所以:
- 训练主体直接委托给 TRL;
- open-r1 主要负责把数据、模型、模板、回调这些周边条件装配好。
10. src/open_r1/grpo.py:强化学习主入口
grpo.py是整个 open-r1 最核心的文件之一。
10.1 它导入了什么
关键依赖包括:
GRPOConfig, GRPOScriptArgumentsget_reward_funcsget_dataset, get_model, get_tokenizerget_callbacksinit_wandb_trainingtrl.GRPOTrainer
这说明它的架构和sft.py类似,也是在做“高层 orchestration”。
10.2 数据流转主线
grpo.py的流程大致可以概括为:
解析参数 -> 设 seed / 日志 -> 检查 checkpoint -> 初始化 wandb -> 加载 dataset -> 加载 tokenizer -> 加载 model -> 从 rewards.py 注册 reward_funcs -> 把样本转成 conversation 格式 -> 初始化 GRPOTrainer -> train() -> 保存模型 / 指标 / 状态 -> 可选 benchmark / Hub push10.3 conversation 格式构造
grpo.py里有一个make_conversation(),它会把数据集中的 prompt 字段包装成:
[{"role":"system","content":training_args.system_prompt},# 可选{"role":"user","content":example[prompt_column]}]然后把原始messages列移除。
这说明 open-r1 在 GRPO 阶段默认也是按“chat conversation”格式训练,而不是把 prompt 当作一段裸文本。
10.4 奖励函数的注册方式
grpo.py本身不关心 reward 细节,它只做一件事:
reward_funcs=get_reward_funcs(script_args)然后把结果喂给:
trainer=GRPOTrainer(model=model,reward_funcs=reward_funcs,args=training_args,train_dataset=...,eval_dataset=...,peft_config=...,callbacks=...,processing_class=tokenizer,)这种设计的优点很明显:
- reward 可以自由组合;
- trainer 主循环不用重写;
- 同一套训练框架可服务于 math、code、format、length-control 等不同任务。
10.5 它与官方 R1 的对应关系
grpo.py不是官方 R1 训练代码,但它是一个很合理的“公开复现版本”:
- 官方论文说:R1-Zero / R1 的关键是 GRPO + 可验证奖励;
- open-r1 把这件事落成了
GRPOTrainer + rewards.py + dataset/config system。
所以如果你要“学 R1 工程怎么搭”,grpo.py是最值得读的入口之一。
11. src/open_r1/rewards.py:整个项目最有价值的文件之一
如果说grpo.py是训练调度中心,rewards.py就是 open-r1 的任务语义中心。
它的作用是:
- 定义各类 reward function;
- 把 reward 做成可注册组件;
- 支持 math / code / format / length / repetition 等不同方向;
- 让 GRPO 训练从“纯算法”变成“可定制任务系统”。
11.1 accuracy_reward
accuracy_reward()的作用是校验 completion 是否与 ground truth 一致。
对数学类任务,它会做解析、归一化和答案提取,而不是简单字符串匹配。
这说明 open-r1 非常重视“可验证 reward”的鲁棒性。
11.2 format_reward / reasoning_steps_reward
这些 reward 用来鼓励:
- 输出符合预期格式;
- 显式出现推理步骤;
- reasoning block 足够规范。
它们是复现 R1 “先 reasoning、后 answer”风格的重要抓手。
11.3 cosine / repetition_penalty / length / tag_count
这些 reward 更像“行为整形器”:
cosine:对答案正确/错误时的长度收益做缩放;repetition_penalty:减少重复 n-gram;length:控制 completion 长度;tag_count:约束标签结构;soft_overlong_punishment:惩罚过长输出。
这些都非常符合 reasoning-RL 的实际工程需求:
不是只看答对,还要控制“怎么答”。
11.4 code / binary_code / ioi_code / cf_code
这是 open-r1 很强的一部分:
它把 code reasoning 奖励也工程化了。
code_reward:执行代码并按通过率/结果打分;binary_code_reward:把连续 reward 二值化;ioi_code:偏 IOI 风格;cf_code:偏 Codeforces 风格;code_format:检查代码格式。
11.5 reward 注册表
get_reward_funcs()里维护一个REWARD_FUNCS_REGISTRY。
这是整个文件最重要的结构之一。它把 reward 名称映射到具体函数或带 partial 包装的函数。
这带来的好处是:
- 配置文件里写名字即可启用;
- 同一 reward 可以通过 script args 注入参数;
- math/code/task-specific reward 能在一个统一系统里共存。
如果你要给 open-r1 加自定义任务,通常第一步就是改这个注册表。
12. src/open_r1/generate.py:生成 synthetic data 的入口
generate.py的定位非常清楚:
不是训练,而是用模型去生成训练数据。
12.1 它依赖 Distilabel
文件顶部直接使用:
distilabel.llms.OpenAILLMdistilabel.pipeline.Pipelinedistilabel.steps.tasks.TextGeneration
这意味着 generate.py 的职责是把一个兼容 OpenAI API 的模型服务,包装成一个批量生成数据的流水线。
12.2 build_distilabel_pipeline()
这个函数会接收:
- model 名称
- base_url
- prompt_column
- prompt_template
- temperature / top_p
- max_new_tokens
- num_generations
- input_batch_size
- client_replicas
- timeout / retries
然后创建一个 DistilabelPipeline().ray(),内部挂一个TextGeneration步骤。
它的工程意义是:
把“单次模型推理”提升为“可分布式、可批处理、可复制的 synthetic data generation 管线”。
12.3 generate.py 的场景
它很适合用于:
- 从 DeepSeek-R1 或其他 teacher 生成 reasoning traces;
- 构建蒸馏数据;
- 做 rejection sampling 前的多样本采样;
- 大规模 prompt -> response 数据生产。
13. src/open_r1/utils/data.py:数据加载与混合集
data.py的核心函数是get_dataset(args)。
它支持两种模式:
13.1 单数据集模式
如果只设置dataset_name,就直接datasets.load_dataset(...)。
13.2 dataset mixture 模式
如果配置了dataset_mixture,它会:
- 读取多个数据集配置;
- 按配置加载;
- 记录列与权重;
- 进行混合与拼接。
这对 reasoning 训练尤其关键。
因为现实中的推理训练几乎都会混合:
- 纯数学
- 代码题
- 科学问答
- synthetic traces
- 过滤后的教师推理数据
所以data.py虽短,但它支撑了 open-r1 的“多源数据喂养能力”。
14. src/open_r1/utils/model_utils.py:模型与 tokenizer 装配器
这个文件的核心函数有两个:
14.1 get_tokenizer()
主要职责:
AutoTokenizer.from_pretrained(...)- 按配置覆盖
chat_template
这非常关键,因为 open-r1 的训练高度依赖 prompt/message 模板的一致性。
14.2 get_model()
主要职责:
- 解析
torch_dtype - 处理 quantization config
- 组织
model_kwargs - 按需设置
use_cache - 通过
AutoModelForCausalLM.from_pretrained(...)加载模型
这里能看到一个细节:
- 如果开启
gradient_checkpointing,则会把use_cache=False
这很常见,因为 checkpointing 与 KV cache 通常不同时用于训练。
所以model_utils.py是一个典型的“训练端模型装配层”。
15. src/open_r1/utils/callbacks.py:训练后处理与自动动作
callbacks.py的价值在于:
它让训练脚本不只会“train”,还会在保存节点上自动触发附加动作。
15.1 PushToHubRevisionCallback
这是最重要的 callback 之一。
它会在on_save()时把当前 checkpoint 以新 revision 的形式推到 Hub。
也就是说,训练过程中的 checkpoint 不只是本地文件夹,还可以变成 Hub 上可追踪的版本。
15.2 get_callbacks()
get_callbacks(train_config, model_config)会按配置名称把 callback 实例化出来。
这延续了 open-r1 的一贯风格:
- 行为不写死在训练脚本里;
- 统一由 config 决定。
这对大规模实验管理非常有用。
16. src/open_r1/utils/evaluation.py:训练后的 benchmark 启动器
evaluation.py的目标不是自己计算所有 benchmark,而是负责发起 benchmark 作业。
run_benchmark_jobs()会:
- 读取
training_args.benchmarks - 判断 benchmark 名称是否合法
- 对支持的 benchmark 调用对应评测作业
- 通过 slurm / subprocess 等方式发起实际评测
这说明 open-r1 的 benchmark 设计是“异步作业型”而不是“trainer 内嵌评测型”。
优点是:
- 训练脚本保持简洁;
- benchmark 可以独立扩展;
- 更适合多节点/多模型实验。
17. src/open_r1/utils/code_providers.py:把代码奖励真正落地
这是 open-r1 很“工程”的一个文件。
做 code reward 时,不能只看字符串,要真的执行代码。
这个文件把执行后端抽象成 provider。
17.1 get_provider()
当前公开代码能看到它支持:
e2bmorph
这说明 open-r1 的目标不是把代码奖励锁死在一个沙盒服务上,而是做成可替换后端。
17.2 E2BProvider
E2BProvider支持两种模式:
- 直接异步沙盒执行;
- 通过 router 批量执行。
它会处理:
- 并发控制;
- timeout;
- request timeout;
- 异常回退;
- sandbox 关闭。
这说明 open-r1 的 code reward 不是“概念 demo”,而是真在考虑大规模训练中的执行开销与稳定性。
17.3 MorphProvider
MorphProvider也承担类似职责,只是换了另一套沙盒后端。
这种 provider abstraction 对于复杂 code-RL 非常有必要。
18. src/open_r1/utils/wandb_logging.py:极简但必要
这个文件非常短,只负责把:
WANDB_ENTITYWANDB_PROJECTWANDB_RUN_GROUP
从训练参数写入环境变量。
虽然简单,但它把训练记录的组织规则统一到了配置层。
19. 一条完整的 SFT 调用链
把 open-r1 的 SFT 路线串起来,可以写成:
recipes/.../sft/config_*.yaml -> TrlParser -> configs.py (ScriptArguments + SFTConfig + ModelConfig) -> utils/data.py::get_dataset -> utils/model_utils.py::get_tokenizer -> utils/model_utils.py::get_model -> sft.py::setup_chat_format (必要时) -> trl.SFTTrainer -> callbacks.py / evaluation.py / hub push你在改 SFT 任务时,一般会动的地方是:
- recipe yaml
- dataset source / mixture
- chat template / eos token
- benchmark 配置
20. 一条完整的 GRPO 调用链
GRPO 主线则是:
recipes/.../grpo/config_*.yaml -> TrlParser -> configs.py (GRPOScriptArguments + GRPOConfig + ModelConfig) -> utils/data.py::get_dataset -> utils/model_utils.py::get_tokenizer -> utils/model_utils.py::get_model -> rewards.py::get_reward_funcs -> grpo.py::make_conversation -> trl.GRPOTrainer -> callbacks / evaluation / hub push真正决定训练语义的不是GRPOTrainer本身,而是:
- 数据集长什么样
- prompt 列选哪列
- chat template 怎么设置
- reward 组合怎么配
- 是否加入 code sandbox / language consistency / format reward
这也是 open-r1 最值得参考的地方。
21. open-r1 与 DeepSeek-R1 官方路线的关系
要避免一个常见误区:
open-r1 不是 DeepSeek 官方 R1 训练代码的镜像。
它更像是:
- 以 DeepSeek-R1 论文为指导思想;
- 用 Hugging Face / TRL / vLLM / Distilabel 生态重构出一条“足够透明、足够可扩展”的复现路线。
因此两者关系是:
DeepSeek 官方
回答“R1 为什么有效”
open-r1
回答“R1 风格工程怎么搭起来”
22. 你读 open-r1 时最应该先读哪几个文件
如果你时间有限,建议这个顺序:
第一层:先看路线
README.mdrecipes/README.md- 一个具体 recipe yaml
第二层:看训练入口
src/open_r1/sft.pysrc/open_r1/grpo.py
第三层:看训练语义
src/open_r1/configs.pysrc/open_r1/rewards.py
第四层:看公共装配
src/open_r1/utils/data.pysrc/open_r1/utils/model_utils.pysrc/open_r1/utils/callbacks.pysrc/open_r1/utils/evaluation.py
第五层:看扩展场景
src/open_r1/generate.pysrc/open_r1/utils/code_providers.py
23. 如何基于 open-r1 改你自己的任务
如果你想把 open-r1 改成自己的 reasoning 项目,通常最实用的改法是:
23.1 换数据
改dataset_name或dataset_mixture
23.2 换 prompt 字段
改dataset_prompt_column
23.3 换模板
改chat_template/eos_token
23.4 换奖励
在rewards.py新增函数,并注册到REWARD_FUNCS_REGISTRY
23.5 换评测
在evaluation.py加 benchmark 入口
23.6 换代码执行后端
扩展code_providers.py
所以 open-r1 的扩展点设计是比较清晰的。
24. 总结
open-r1 最值得学习的,不只是“复现了 R1”,而是它把 reasoning post-training 拆成了 5 个彼此清晰解耦的层:
配置层 + 数据层 + 模型装配层 + 奖励层 + 训练入口层 + 调度 / 评测 / Hub 层如有解释错误,请指正
25. 参考资料
open-r1 官方仓库
https://github.com/huggingface/open-r1DeepSeek-R1 官方仓库
https://github.com/deepseek-ai/DeepSeek-R1DeepSeek-R1 论文
https://arxiv.org/abs/2501.12948DeepSeek-V3 官方仓库
https://github.com/deepseek-ai/DeepSeek-V3
