Open R1:更新#3

在过去的几周里,我们致力于复现 DeepSeek-R1 配方中竞争性编程(代码推理)方面的内容。
在此文章中,我们很高兴能分享
- CodeForces-CoTs 的构建:一个包含近 10 万个高质量样本的数据集,这些样本从 R1 中提炼而来,用于生成 C++ 和 Python 解决方案。
- IOI 基准:一个包含 2024 年国际信息学奥林匹克竞赛 (IOI) 挑战性问题的新基准。
- OlympicCoder:两个经过微调的 7B 和 32B 代码模型,它们在 IOI 问题上的表现优于 Claude 3.7 Sonnet 等闭源前沿模型。
以下是 OlympicCoder 模型与各种指令微调和推理模型相比的概述。我们发现,在 CodeForces-CoTs 上训练的模型表现出色,OlympicCoder-32B 的性能优于我们测试过的所有开源模型,其中一些模型的规模甚至超过 100 倍 🤯。
请继续阅读,了解我们如何构建数据集、基准和模型!
关键链接
CodeForces
- 问题数据集:
open-r1/codeforces
- DeepSeek-R1 CoTs 数据集:
open-r1/codeforces-cots
国际信息学奥林匹克竞赛 (IOI)
- 问题描述数据集 (IOI’2020 - IOI’2024):
open-r1/ioi
- 测试用例:
open-r1/ioi-test-cases
- 官方(真实)解决方案:
open-r1/ioi-sample-solutions
- DeepSeek-R1 CoTs 数据集 (IOI’2020-IOI’2023):
open-r1/ioi-cots
- 40+ 领先模型在 IOI’2024 上的评估数据:
open-r1/ioi-2024-model-solutions
- 运行生成和评估的代码
OlympicCoder
CodeForces-CoTs 数据集
CodeForces 是竞争性编程者中最受欢迎的网站之一,定期举办竞赛,参与者必须解决具有挑战性的算法优化问题。这些问题的挑战性使其成为一个有趣的数据集,可以改进和测试模型的代码推理能力。
尽管之前的一些工作(如 DeepMind 的 CodeContests 数据集)已经编译了大量的 CodeForces 问题,但今天我们发布了我们自己的 open-r1/codeforces
数据集,其中包含 1 万多个问题,涵盖了从最早的竞赛到 2025 年的所有问题,其中约 3 千个问题未包含在 DeepMind 的数据集中。此外,对于约 60% 的问题,我们包含了题解,这是竞赛组织者撰写的解释正确解决方案的说明。您还会找到从官方网站提取的每个问题的 3 个正确解决方案。
此外,我们正在发布 open-r1/codeforces-cots
,其中包含 DeepSeek-R1 在这些问题上生成的思维链,我们要求模型用 C++(竞争性编程中使用的主要语言)和 Python 生成解决方案,总计近 10 万个样本。
我们使用该数据集对 Qwen2.5 Coder Instruct 7B 和 32B 进行了微调,从而得到了我们的 OlympicCoder-7B 和 OlympicCoder-32B 模型。您将在本博客文章的后续内容中找到有关这些模型的更多详细信息。
代码可验证性危机
尽管 DeepMind 的竞赛数据集及其他包含竞争性编程问题的数据集包含测试用例并声称可验证,但这些测试用例通常只是竞赛网站上使用的完整测试套件的一小部分。特别是 CodeForces,其显示的测试用例限制在约 500 个字符以内,这意味着这些数据集仅包含符合此限制的较短、较简单的测试用例。
举个例子,我们选取了 7 个 R1 生成的解决方案通过了所有公开测试用例的问题,并尝试将它们提交到 CodeForces 平台。
尽管它们通过了较短的测试,但这些解决方案中的每一个都在完整的测试集上失败了。这凸显了对新的完全可验证的竞争性编程数据集的需求。虽然我们计划尝试基于模型的解决方案来生成和验证可能在未来添加到 CodeForces 数据集中的其他具有挑战性的测试,但目前我们正在其他地方寻找完全可用的问题数据。
国际信息学奥林匹克竞赛 (IOI)
国际信息学奥林匹克竞赛 (IOI) 是五项国际科学奥林匹克竞赛之一(如果您熟悉 AIME,IOI 相当于 IMO 的编程竞赛,最优秀的 AIME 学生会被邀请参加),它测试一小部分高中生(每个国家 4 名)解决复杂算法问题的能力。
这些问题极具挑战性,并且完整的测试集在允许的 (CC-BY) 许可下可用并发布。这意味着 IOI 是测试模型代码推理能力的完美数据集。
在 IOI 中,每个问题都有几个子任务,每个子任务都有不同的输入限制。要解决一个子任务,提交的程序需要在(严格的)时间限制内通过其所有测试用例。虽然最终子任务通常是“完整问题”,但一些子任务实际上描述了一个更容易(更受限制)的问题,参赛者通常会针对特定的子任务以获得部分分数,而不是仅仅尝试解决完整问题(获得满分的相对较少)。
遵循 OpenAI 最近的一篇论文,其中 o1 实时参加了 IOI’2024(最新一届),我们同样处理了 IOI’2024(以及 2020 年之前的 IOI)的所有问题,并将它们拆分为子任务,以便每个提示都要求模型解决一个特定的子任务。我们发布了处理后的问题描述,以及运行它们和测试用例所需的评分/检查文件,这些文件可在 open-r1/ioi
和 open-r1/ioi-test-cases
中找到。
我们创建了自定义代码来运行解决方案(许多问题设置复杂,需要一个“管理器”进程与运行用户提交的几个进程和特殊检查器通信以验证解决方案),并根据 IOI 规则进行评分,这些代码可在 https://github.com/huggingface/ioi 找到,并且评估了 40 多个领先的推理模型在 IOI’2024 上的表现。
在 IOI 中,参赛者每个问题有 50 次提交限制。我们为每个子任务生成了 50 次提交,然后应用了与 OpenAI 对 o1-ioi 使用的类似的选择策略,以获得每个问题在比赛条件下的分数。结果可以在下面找到,其中水平线代表真实比赛数据中铜牌、银牌和金牌模型的阈值。虽然 o1 非常接近铜牌,但最终没有模型能达到奖牌阈值(参赛者中位数的 50%)。
我们的 OlympicCoder 模型(红色)与其他前沿模型相比表现相当不错,甚至超越了一些闭源模型(金色),例如 claude-3.7-sonnet-thinking,而 OlympicCoder-32B 在 50 次提交限制设置下甚至优于 o1-mini 和我们从中提取的 DeepSeek-R1 模型。
提交策略
需要注意的是,我们的提交策略可能会惩罚非推理模型,例如 Qwen-2.5-Coder-32B-Instruct,OlympicCoder-32B 的基础模型。为了模拟真实比赛条件,即提交的分数只有在实际提交后才可知晓,我们采用了类似于 OpenAI 对 o1-ioi 使用的轮询提交策略:我们首先提交一个针对问题最后一个子任务的解决方案,然后是倒数第二个,依此类推,只在选中提交时才评估解决方案。我们跳过针对已由先前选择的提交解决的子任务的提交,并且在每个目标子任务中,我们更喜欢来自更长生成的提交——这是一个对推理模型有意义的标准,但对其他模型则不然。
如果我们取消 50 次提交限制(这将使我们脱离比赛条件),并评估我们生成的所有提交(每个子任务 50 次),我们将获得以下结果
从 R1 轨迹训练代码模型中吸取的教训
在创建 OlympicCoder 模型时,我们进行了大量 SFT 实验,以了解 CodeForces 数据集上各种过滤器的作用。我们发现 open-r1/codeforces-cots
的以下子集提供了最佳的整体性能:
solutions
:根据问题描述生成的 R1 解决方案。solutions_w_editorials
:根据问题描述和解释正确解决方案的说明生成的 R1 解决方案。
请注意,我们只关注了 C++ 解决方案,但如果融合 Python 解决方案,性能可能会进一步提升。
我们使用 LiveCodeBench 作为我们模型的测试平台,然后将表现最佳的检查点通过更难的 IOI 基准测试。我们测试了各种超参数配置来训练我们的模型,并确定了以下配置:
- 模型:Qwen2.5 Coder Instruct 7B 和 32B
- 训练轮次:10
- 有效批大小:128
- 学习率:4e-5
- 调度器:余弦调度,衰减到峰值学习率的 10%
- 上下文大小:7B 模型为 32,768 个 token,32B 模型为 22,528 个 token
下面我们分享一些从 R1 推理轨迹上微调 Qwen2.5 Coder 模型中学到的经验。
教训 1:样本打包会损害推理性能
样本打包是一种广泛使用的方法,可以有效地处理可变长度序列并加速训练。如下图所示,其工作原理是将训练样本(彩色)连接成大小相等的块,从而消除了在批次之间使用填充标记(灰色)的需要。
通过打包,样本可以跨每个块的边界重叠,但实际上,如果大多数样本远小于块大小,这并不太重要。
然而,对于我们从 R1 中提取的推理轨迹,我们想知道打包是否会损害性能,因为许多轨迹很长,并且剪切答案的比例可能很高。这意味着训练模型关注长上下文信息可能很困难,特别是当问题和答案被打包到不同的块中时。
如下面的惊人图所示,我们发现打包极大地损害了模型的性能:使用打包时,模型几乎无法解决 LiveCodebench 中的问题,但没有打包时,性能在几个 epoch 后才得到改善并趋于稳定。
我们怀疑这种显著差异是因为我们的训练集仅包含 C++ 解决方案,而 LiveCodeBench 仅评估 Python 性能。尽管如此,我们发现,在我们消融的所有数据集中,打包始终表现更差。
教训 2:使用较大的学习率以获得最佳性能
对于我们使用 Qwen 模型进行的大多数 SFT 实验,2e-5 的学习率通常足以获得强大的性能。然而,当将 SFT 与推理数据应用于现有指令模型时,我们发现通过使用更大的学习率 4e-5 可以获得显著的性能提升。如下图所示,学习率每次加倍,LiveCodeBench 上的性能几乎提升了 10 个百分点!
教训 3:包含题解并不能提升性能
当我们创建 open-r1/codeforces-cots
的 solutions_w_editorials
子集时,我们的直觉是,通过向 R1 提供问题和解决方案,我们将获得更好的推理轨迹。令人惊讶的是,这种直觉在评估过程中并未得到证实,从问题描述中进行朴素采样在训练过程中带来了适度但持续的提升。
教训 4:使用 <think>
进行预填充以持续启用长 CoT
在对我们的一些模型进行感觉测试时,我们遇到了一种特殊现象:如果以领域内查询(如竞争性编码)进行提示,模型会发出预期的长 CoT,其中包含 <think>
和 </think>
标记,然后是最终答案。然而,对于领域外查询(如“瑞士的首都是什么”),模型会恢复到原始指令模型的行为!
这是一个简单查询未能返回长 CoT 的示例:
用户:1+1 是多少?
助手:2。这是最简单的数学加法问题之一。1 + 1 等于 2。这个问题经常被用作谜语或谜题,尤其是在工作面试或在线测试中,以确定候选人快速解决问题的能力。虽然对大多数人来说这可能很明显,但对于雇主来说,它可能是一个有用的问题,用于评估批判性思维和解决问题的能力。此外,它是数学中的一个重要概念,构成了更复杂算术运算的基础。所以下次有人问你 1+1 是什么时,你可以自信地说它是 2!
# Define a function to calculate sum of two numbers def add_numbers(a, b): return a + b # Example usage of the function result = add_numbers(1, 1) print("The sum of 1 + 1 is:", result)
在这个解决方案中,我们定义了一个函数
add_numbers
,它接受两个参数a
和b
并返回它们的和。然后我们用 1 和 1 调用这个函数,将结果存储在变量result
中,然后打印出来。这演示了一种计算和显示两个整数和的直接方法。这个例子简单而有效地说明了 Python 中的基本算术运算。
然而,如果我们用 <think>
预填充助手的回复,就会出现所需行为:
用户:1+1 是多少?
助手:<think> 好的,我需要回答“1+1 是什么?”这个问题。让我想想。哦对了,1 加 1 等于 2。但是等等,也许还有更多。比如,如果有人用不同的方式问呢?或者考虑其他进制?比如在十进制中是 2,但在二进制中是 10。嗯,但问题很简单。用户可能期望得到简单的答案。所以这里的正确答案是 2。我应该直接说出来吗?</think> 是的,1 + 1 等于 2。您还想了解这个计算的任何其他信息吗?
这表明,在将推理轨迹提炼到现有预训练模型中时,应该在最终模型的聊天模板中默认强制进行预填充。巧合的是,DeepSeek 在其提炼模型中也这样做了,而上述行为可能就是原因。(猜测在流行的聊天 UI 上点击“思考”按钮是否只是预填充了助手回复也很有趣。)
结合所有这些经验,我们得到了 OlympicCoder-7B,其性能与 DeepSeek 的提炼模型相当,并显著优于基础 Qwen2.5-Coder 模型。
教训 5:使用 8 位优化器来扩展长上下文大型模型
在 OlympicCoder-7B 的整个训练过程中,我们发现 DeepSpeed ZeRO-3 足以在单个 8 个 H100 节点上训练每个模型,上下文长度为 32k。然而,当我们尝试将我们的配方扩展到 32B 时,我们遇到了许多内存问题。特别是,我们的运行在上下文扩展到 20k token 后就会 OOM,即使在 16 个节点上也是如此 😢。这并不理想,因为 CodeForces-CoTs 轨迹中 20% 的长度超过 20k token,这意味着它们将在训练期间被截断。
问题的根本原因是 transformers
和 trl
尚不支持上下文并行,尽管可以查看此 问题 来跟踪进度。
与此同时,我们探索了各种节省内存的技术,发现将 FSDP 与 paged_adamw_8bit
优化器结合使用,使我们能够将上下文扩展到 22,528 个 token:虽然仍不理想,但只有 9% 的数据被截断。
更新
GRPO 更新
TRL 中的 GRPO 实现取得了最新进展,带来了进一步提升效率、可扩展性和资源利用率的增强功能。以下是自上次更新以来最重要的更改摘要:
生成复用
GRPO 的主要瓶颈之一与任何在线方法相同:生成需要时间。提高 GRPO 样本效率的关键方法是在优化过程中多次复用生成的样本,而不是在使用一次后就将其丢弃。这项技术实际上在很久以前就随着 PPO 被引入。
对于 GRPO,样本被复用的次数表示为 μ。
现在可以多次复用生成的样本,显著加快了流程。
from trl import GRPOConfig
training_args = GRPOConfig(..., num_iterations=...)
但是,请注意——如果 μ 过大,可能会对学习产生负面影响。根据我们的经验,一个好的平衡点是介于 2 到 4 之间。
奖励加权
在训练模型时,并非所有奖励都同等重要。例如,我们可能希望模型优先考虑正确性而不是格式,而不是同等对待这两个方面。
为了解决这个问题,现在可以为不同的奖励分配不同的权重,从而对优化过程进行更精细的控制。通过调整这些权重,我们可以引导模型更多地关注给定任务中最重要的方面。
from trl import GRPOConfig, GRPOTrainer
def very_important_reward(completions, **kwargs):
...
def less_important_reward(completions, **kwargs):
...
training_args = GRPOConfig(
...,
reward_weights=[0.9, 0.1],
)
trainer = GRPOTrainer(
...,
reward_funcs=[very_important_reward, less_important_reward],
args=training_args,
)
其他增强功能
GRPO 还有一些较小但具有影响力的改进:
- PEFT + vLLM 集成 – 现在可以将 PEFT (Parameter-Efficient Fine-Tuning) 和 vLLM 结合使用,将高效微调与优化推理相结合,以实现更好的可扩展性。
- 梯度检查点 – 此功能已添加,通过重新计算某些激活而不是存储它们来减少训练期间的内存消耗,从而可以训练更大的模型。
- 优化选择性对数 softmax 计算 – 引入了一种新的对数 softmax 计算方法,减少了训练期间的内存峰值。
下一步和进行中的工作
目前的重点是两个关键领域:
- 提高生成速度 – 正在探索进一步的优化(如静态缓存),以使生成过程更快。
- 将 GRPO 扩展到多节点设置 – 正在努力使 GRPO 能够跨多个节点进行扩展,从而可以训练更大的模型。
Open R1 数学数据集更新
我们进一步丰富了之前发布的 OpenR1-Math-Raw 数据集,增加了新的元数据,以实现更明智的过滤和验证决策。具体来说,我们添加了以下列:
reparsed_answers
:我们观察到answer
列中的许多条目要么格式不正确,要么只包含部分答案。此外,由于有些问题是多项选择题,因此正确答案本身和相应的字母都应被视为有效响应。为了解决这个问题,我们使用 Llama-3.3-70B-Instruct 模型从solution
列重新提取了所有答案,以确保reparsed_answers
包含正确答案,在多项选择题的情况下还包含相应的字母。我们相信这项补充将对社区极具价值,提高 GRPO 期间的列评分准确性和验证过程。correctness
:当依赖基于模型的验证时,运行答案验证可能会消耗大量资源。因此,我们使用 Llama-3.3-70B-Instruct 作为判断器评估了所有解决方案,同时针对answer
和reparsed_answers
列运行了math_verify
。
实验详情
为了帮助社区理解基于验证的过滤对 SFT 蒸馏数学数据集的影响,我们进行了几项消融实验。我们从随机选择的 20 万个样本池开始,根据以下过滤规则创建了六个不同的 SFT 数据集:
no_restrictions
(20 万) - 未应用任何过滤。llama_verification
(12.4 万) - 经过 LLAMA-70B 验证为正确的样本。math_verify_answer
(8.87 万) - 经math_verify
在answer
列上验证为正确的样本。math_verify_reparsed
(10.1 万) - 经math_verify
在reparsed_answers
列上验证为正确的样本。llama_verification_or_math_verify_reparsed
(LorMV) (15.4 万) - 数据集 2 和 4 的并集。llama_verification_and_math_verify_reparsed
(LandMV) (7.12 万) - 数据集 2 和 4 的交集。
训练与评估
对于数据受限设置中的过滤,精度和召回率都是重要的考量因素。因此,我们没有以相同的 token 预算运行每个实验,而是对所有数据训练了一个 epoch。对于模型,我们选择了 Qwen7B-Instruct,并使用 RoPE 扩展将其微调到 32k 的上下文长度和余弦调度。为了跟踪性能进展,我们每隔 40 步使用 lighteval
在 AIME-24、AIME-25 和 MATH-500 上评估模型。结果总结如下:
主要观察
- 验证显著影响早期性能。在最初的 40 步中,过滤被证明尤其重要。在 MATH-500 数据集上,更严格的验证方法在早期阶段提供了显著的性能提升(例如,
no_restrictions
得分为 0.61,而LandMV
为 0.72)。然而,随着训练的进行,这种性能差距减小,并且拥有更多样本被证明是有益的——即使其中一些包含错误。 - 训练损失在不同数据集之间存在显著差异。使用
math_verify
过滤的数据集表现出持续较低的训练损失。我们假设math_verify
有效地识别了特定的任务子集(主要是数值任务),而基于 Llama 的验证或未过滤的数据集则保持了更广泛的数据种类。 - 令人惊讶的是,未过滤的数据集没有遭受严重的性能下降。尽管包含不正确的样本,但
no_restrictions
数据集在长时间训练运行中仍保持了竞争性性能。
建议
根据我们的发现,最佳的过滤方法在很大程度上取决于训练 token 预算。对于较短的运行,更严格的验证方法提供了显著优势。作为一般建议,我们建议将 Llama 验证与 math_verify
结合使用——如 LorMV
数据集所示。
推理课程
Hugging Face 学习团队正在致力于提供关于强化学习、GRPO 以及使用 TRL 训练推理模型的可访问材料。它包括 Maxime Labonne、Unsloth 和 Marimo 的教程和演示。如果您正在这个快速发展的领域中寻找一个好的起点,请查看推理课程!
社区亮点
过去几周,GRPO 在各种任务中得到了持续探索,同时还发布了几个针对数学和代码之外领域的新推理数据集。以下是我们发现特别令人兴奋的一些发布:
GRPO 探索
- Unsloth 的优化向导进一步降低了使用 LoRA 训练 GRPO 模型所需的 VRAM,现在 1.5B 模型仅需 5GB 🧙
- Kalomaze 写了一篇非常棒的博客文章,介绍了如何为 GRPO 选择合适的超参数。他们还观察到小于 7B 的模型收敛速度往往慢得多,这意味着您应该在这些规模下探索新的想法,然后再断定它们不起作用。
- Hrishbh Dalal 已经证明,您可以使用 GRPO 来教导大型语言模型解决数独谜题!
- Yutai Li 及其合著者发表了一篇非常好的论文,表明对于小型模型,最好从更强大的教师模型中提炼出长短 CoT 数据的混合体。
推理数据集
- KodKode 发布了一个非常大的数据集,包含约 50 万个编程样本。这看起来是 CodeForces-CoTs 的绝佳补充,我们很高兴能用它训练一些新模型!
- GeneralReasoning 团队已开始发布高质量且多样化的数据集,例如
GeneralReasoning/GeneralThought-323K
,这些数据集涵盖了比公开可用数据更广泛的领域和模型。他们还有一个不错的网站,允许您探索数据,以及
接下来是什么?
通过这次更新,我们现在已经具备了完成我们复现计划的第 1 步和第 2 步的主要部分。
在接下来的几周,我们计划重点关注:
- 完善蒸馏数据集的混合,以训练通用推理器。
- 将 GRPO 扩展到更大的模型,例如
Qwen/Qwen2.5-Coder-32B-Instruct
,以便我们可以推导出 R1-Zero 变体。 - 结合来自数学和代码等多个领域的奖励信号,并整合奖励模型以评估非推理数据。