在过去的几周里,我们主要致力于复现 DeepSeek-R1 配方中 competitive programming(代码推理)的部分。
从 R1 轨迹训练代码模型中获得的经验教训
在创建 OlympicCoder 模型时,我们进行了大量的 SFT 实验,以了解应用于 CodeForces 数据集的各种过滤器的作用。我们发现 open-r1/codeforces-cots
的以下子集给出了最佳的整体性能:
solutions
:R1 根据问题陈述生成的解决方案。solutions_w_editorials
:R1 根据问题陈述和解释(解释了正确的解决方案)生成的解决方案。
请注意,我们只关注了 C++ 解决方案,但如果混合使用 Python 解决方案,可能会进一步提高性能。我们使用 LiveCodeBench 作为我们模型的试验平台,然后通过更难的 IOI 基准测试运行性能最佳的检查点。我们测试了各种超参数配置来训练我们的模型,并确定了以下配置:
- 模型:Qwen2.5 Coder Instruct 7B 和 32B
- Epochs: 10
- Effective batch size: 128
- Learning rate: 4e-5
- Scheduler: Cosine with a decay to 10% of the peak learning rate
- Context size: 32,768 tokens for 7B 和 22,528 tokens for 32B
下面我们分享一些从在 R1 推理轨迹上调整 Qwen2.5 Coder 模型中获得的经验教训。
第一课:样本打包会损害推理性能
样本打包是一种广泛使用的方法,可以有效地处理可变长度的序列并加速训练。如下图所示,它的工作方式是将训练样本(彩色)连接成大小相等的块,从而无需在批次中使用填充token(灰色):
通过打包,样本可以在每个块的边界上重叠,但实际上,如果大多数样本远小于块大小,则这无关紧要。但是,对于我们从 R1 中提取的推理轨迹,我们想知道打包是否会损害性能,因为许多轨迹很长,并且被截断的答案比例可能很高。这意味着可能难以训练模型来关注长上下文信息,尤其是在问题和答案已被打包到不同的块中时。
如下图所示,我们发现打包会显著损害模型的性能:使用打包时,模型几乎无法解决来自 LiveCodebench 的问题;而不使用打包时,性能会在经过几个 epoch 后提升并趋于稳定:
我们推测这种显著差异的原因在于,我们的训练集仅包含 C++ 解决方案,而 LiveCodeBench 仅评估 Python 代码的性能。尽管如此,我们发现在我们进行消融实验的所有数据集中,打包的表现始终更差。
第二课:使用更大的学习率以获得最佳性能
对于我们使用 Qwen 模型运行的大多数 SFT 实验,2e-5 的学习率通常足以获得强大的性能。然而,当将 SFT 与推理数据应用于现有的 instruct 模型时,我们发现通过使用更大的学习率 4e-5 可以获得显著的性能提升。如下图所示,学习率每翻一倍,LiveCodeBench 上的改进幅度几乎达到 10 个点!
第三课:包含题解并不能提升性能
当我们创建 open-r1/codeforces-cots
的 solutions_w_editorials
子集时,我们的直觉是,通过向 R1 提供问题和题解,我们将获得更好的推理轨迹。 令人惊讶的是,这种直觉在评估期间并未得到证实,从问题陈述中进行朴素抽样在训练期间给出了适度但一致的提升:
第四课:使用 <think>
预填充以持续启用长 CoT(思维链)
在对我们的一些模型进行氛围测试时,我们遇到了一种奇特的现象:如果使用领域内查询(例如, competitive programming)进行提示,模型会在 <think>
和 </think>
Token 中输出预期的长 CoT(思维链),然后给出最终答案。但是,对于领域外查询,例如“瑞士的首都是什么”,模型会恢复到原始指令模型的行为! 这是一个简单的查询未能返回长 CoT(思维链)的示例:
用户:1+1 是多少?
助手:2。这是最简单的数学加法问题之一。1 + 1 等于 2。这个问题经常被用作谜语或难题,尤其是在工作面试或在线测试中,以确定候选人快速解决问题的能力。虽然对大多数人来说这似乎很明显,但对于雇主来说,这是一个评估批判性思维和解决问题能力的有用问题。此外,它是数学中的一个重要概念,构成了更复杂的算术运算的基础。所以下次有人问你 1+1 是多少时,你可以自信地说它是 2!
def add_numbers(a, b): return a + b # 函数的示例用法 result = add_numbers(1, 1) print("1 + 1 的总和是:", result)
在这个解决方案中,我们定义了一个名为
add_numbers
的函数,该函数接受两个参数a
和b
,并返回它们的和。接着,我们使用 1 和 1 调用这个函数,并将结果存储在变量result
中,然后打印该变量。这演示了一种直接计算并显示两个整数之和的方法。这个例子简单而有效地说明了 Python 中的基本算术运算。
然而,如果我们用 <think>
预填充 AI 智能体的回复,就会出现期望的行为:
用户:1+1 是多少?
AI 智能体:<think> 好的,我需要回答“1+1 是多少?”这个问题。让我想想。哦,对了,1 加 1 等于 2。但是等等,可能还有更多内容。例如,如果有人以不同的方式提问怎么办?或者考虑其他数字基数?比如在十进制中是 2,但在二进制中是 10。嗯,但问题很简单。用户可能期望简单的答案。所以这里正确的答案是 2。我应该直接说明吗?</think> 是的,1 + 1 等于 2。您还想了解关于此计算的任何其他信息吗?
这表明,当将推理轨迹提炼到现有的预训练模型中时,应该默认在最终模型的聊天模板中强制执行预填充。顺便说一句,这也是 DeepSeek 对其提炼模型所做的事情,上述行为可能就是原因。(有趣的是,推测在流行的聊天用户界面上点击“思考”按钮是否只是预填充 AI 智能体响应🙂)。
结合所有这些经验,我们创造了 OlympicCoder-7B,其性能与 DeepSeek 的精馏模型相当,并且显著优于基础的 Qwen2.5-Coder 模型:
第五课:使用 8 比特优化器扩展具有长上下文的大模型
在 OlympicCoder-7B 的整个训练过程中,我们发现 DeepSpeed ZeRO-3 足以在单个包含 8 个 H100 的节点上训练每个具有 32k 上下文的模型。然而,当我们尝试将我们的方案扩展到 32B 时,我们遇到了一系列内存问题。特别是,一旦上下文扩展到超过 20k 个token,我们的运行就会内存溢出 (OOM),即使在 16 个节点上也是如此 😢。这并不理想,因为 20% 的 CodeForces-CoTs 跟踪大于 20k 个token,这意味着它们将在训练期间被截断。
问题的根源在于 transformers
和 trl
尚未支持上下文并行性。
与此同时,我们探索了各种节省内存的技术,发现将 FSDP 与 paged_adamw_8bit
优化器相结合,使我们能够将上下文扩展到 22,528 个token:仍然不理想,但只有 9% 的数据被截断。
更新
GRPO 更新
最近在改进 TRL 中 GRPO 的实现方面取得了一些进展,带来了进一步提高效率、可扩展性和资源利用率的增强功能。以下是自上次更新以来最重要的更改摘要:
Generation Reuse
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(参数高效微调)和 vLLM 结合使用,将高效的微调与优化的推理相结合,以实现更好的可扩展性。
- 梯度检查点 – 此功能已添加,通过重新计算某些激活而不是存储它们来减少训练期间的内存消耗,从而可以训练更大的模型。
- 优化的选择性 Log Softmax 计算 – 引入了一种新的计算 log softmax 的方法,减少了训练期间的内存峰值。
后续步骤和进行中的工作
目前的重点是两个关键领域:
- 提高生成速度 – 正在探索进一步的优化(如静态缓存)以使生成过程更快。
- 将 GRPO 扩展到多节点设置 – 正在进行的工作是使 GRPO 能够跨多个节点进行扩展,从而可以训练更大的模型。
Open R1 数学数据集更新
我们进一步丰富了之前发布的 OpenR1-Math-Raw 数据集,添加了新的元数据,以便在过滤和验证过程中做出更明智的决策。具体来说,我们添加了以下列:
reparsed_answers
: 我们观察到answer
列中的许多条目,其 LaTeX 格式不正确,或者仅包含部分答案。此外,由于某些问题是多项选择题,正确的答案本身及其对应的选项字母都应被视为有效答案。为了解决这个问题,我们使用 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 数量限制,而是在所有数据上进行单次迭代训练。对于模型,我们选择了 Qwen7B-Instruct,并使用 RoPE 扩展对其进行微调,使其上下文长度达到 32k,并采用余弦退火策略。为了跟踪性能进展,我们使用 lighteval
在 AIME-24、AIME-25 和 MATH-500 上每 40 步评估一次模型。结果总结如下:
主要观察结果
- 验证显著影响早期阶段的性能。 过滤在最初的 40 步中尤其重要。在 MATH-500 数据集上,更严格的验证方法在早期阶段提供了显著的性能提升(例如,
no_restrictions
得分为 0.61,而LandMV
得分为 0.72)。然而,随着训练的进行,这种性能差距逐渐缩小,拥有更多样本被证明是有益的——即使其中一些包含错误。 - 不同数据集的训练损失存在显著差异。 使用
math_verify
过滤的数据集表现出持续更低的训练损失。我们推测math_verify
有效地识别了任务的特定子集(主要为数值任务),而基于 Llama 的验证或未经过滤的数据集则保留了更广泛的数据多样性。 - 令人惊讶的是,未经过滤的数据集并未出现严重的性能下降。 尽管包含不正确的样本,
no_restrictions
数据集在长时间的训练中仍保持了具有竞争力的性能。
建议
根据我们的研究结果,最佳的过滤方法很大程度上取决于训练 Token 预算。对于较短的训练周期,更严格的验证方法能带来显著优势。作为一般性建议,我们推荐将 Llama 验证与 math_verify
结合使用,正如在 LorMV
数据集中所做的那样。
代码数据集构建
CodeForces-CoTs 的构建:这是一个从 R1 提炼出的近 10 万个高质量样本的数据集,用于生成 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
International Olympiad in Informatics (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
- IOI’2024 上 40 多个领先模型的评估数据:
open-r1/ioi-2024-model-solutions
- 运行生成和评估的代码 OlympicCoder
- 7B 模型:
open-r1/OlympicCoder-7B
- 32B 模型:
open-r1/OlympicCoder-32B
CodeForces 是 competitive programming者中最受欢迎的网站之一,定期举办竞赛,参赛者需要在竞赛中解决具有挑战性的算法优化问题。 这些问题的挑战性使其成为一个有趣的数据集,可以用来改进和测试模型的代码推理能力。
虽然之前像 DeepMind 的 CodeContests 数据集 这样的工作已经收集了大量的 CodeForces 题目,但今天我们发布了自己的 open-r1/codeforces
数据集,其中包含超过 1 万道题目,涵盖了从最早的比赛到 2025 年的所有比赛,其中约 3 千道题目未包含在 DeepMind 的数据集中。此外,对于大约 60% 的题目,我们包含了_题解_,这是由比赛组织者编写的对正确解法的解释。您还可以找到从官方网站提取的每道题目的 3 个正确解法。更进一步,我们还发布了 open-r1/codeforces-cots
,其中包含 DeepSeek-R1 在这些题目上生成的思维链 (chain of thought),我们要求模型使用 C++ ( competitive programming中使用的主要语言) 和 Python 生成解法,总计接近 10 万个样本。
我们使用这个数据集对 Qwen2.5 Coder Instruct 7B 和 32B 进行了微调 (fine-tuning),从而得到了 OlympicCoder-7B 和 OlympicCoder-32B 模型。
虽然像 DeepMind 的竞赛和其他包含 competitive programming问题的数集声称是可验证的,并且包含测试用例,但这些测试用例通常只是竞赛网站上使用的完整套件的一小部分。特别是 CodeForces,将显示的测试用例限制在约 500 个字符,这意味着这些数据集仅包含符合此限制的较短、较简单的测试用例。
例如,我们选择了 7 个问题,R1 生成的解决方案通过了所有公共测试用例,并尝试将它们提交到 CodeForces 平台:
虽然它们通过了较短的测试,但这些解决方案中的每一个都在完整的测试集上失败了。这突显了对新的完全可验证的 competitive programming数据集的需求。虽然我们计划尝试基于模型的解决方案来生成和验证可能在未来添加到我们的 CodeForces 数据集中的其他具有挑战性的测试,但目前我们正在寻找其他地方完全可用的问题数据。
国际信息学奥林匹克竞赛 (IOI)
国际信息学奥林匹克竞赛 (IOI) 是五大国际科学奥林匹克竞赛之一(如果您熟悉 AIME,IOI 相当于 IMO 的编程竞赛,AIME 中最优秀的学生会被邀请参加 IMO),它测试了一小部分高中生(每个国家/地区 4 名)解决复杂算法问题的能力。
这些问题极具挑战性,完整的测试集以宽松的 (CC-BY) 许可发布。这意味着 IOI 是测试模型代码推理能力的完美数据集。在 IOI 中,每个问题都有若干个子任务,每个子任务都有不同的输入约束。要解决一个子任务,提交的程序需要通过其所有测试用例,并且要在(严格的)时间限制内完成。虽然最终的子任务通常是“完整的问题”,但有些子任务实际上描述了一个更容易(约束更多)的问题,参赛者通常会针对特定的子任务来获得部分分数,而不是仅仅尝试解决完整的问题(满分相对罕见)。
根据 OpenAI 最近的一篇论文,其中 o1 live 参加了 IOI’2024(最新一届国际信息学奥林匹克竞赛),我们以类似的方式处理了 IOI’2024 的所有问题(以及之前直到 2020 年的各届 IOI),并将它们分解为子任务,以便每个提示词都要求模型解决一个特定的子任务。我们发布了处理后的问题陈述,以及运行这些问题和测试用例所需的所有评分/检查器文件,这些文件位于 open-r1/ioi
和 open-r1/ioi-test-cases
目录中。我们创建了自定义代码来运行解决方案(许多问题都有复杂的设置,需要一个“管理器”进程与运行用户提交代码的多个进程以及特殊检查器进行通信,以验证解决方案),并根据 IOI 规则进行评分。此外,我们还在 IOI’2024 上评估了 40 多个领先的推理模型。
在 IOI 中,参赛者每个问题有 50 次提交限制。我们为每个子任务生成了 50 次提交,然后应用了类似于 OpenAI 使用的筛选策略来获得每个问题在竞赛条件下的分数。结果如下所示,其中水平线代表铜牌、银牌和金牌模型的阈值(来自真实竞赛数据)。虽然 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 个),我们将获得以下结果:
推理课程
Hugging Face learn 团队正在努力制作关于强化学习、GRPO 以及使用 TRL 训练推理模型的可访问材料。它包括来自 Maxime Labonne、Unsloth 和 Marimo 的教程和演示。
社区亮点
在过去的几周里,人们一直在持续探索 GRPO 在各种任务中的应用,同时还出现了一些新的推理数据集,这些数据集的目标领域比数学和代码更广泛。以下是我们发现特别令人兴奋的一些发布:
GRPO 探索
- Unsloth 的优化向导进一步减少了使用 LoRA 训练 GRPO 模型所需的 VRAM 量,现在对于 1.5B 模型来说,降至 5GB 🧙
- Kalomaze 撰写了一篇精彩的博文,详细介绍了为 GRPO 选择合适的超参数。 他们还观察到,参数量小于 7B 的模型收敛速度通常较慢。 这意味着,在得出结论认为某些想法行不通之前,应该先在这些较小规模的模型上进行探索。
- Hrishbh Dalal 已经证明,可以使用 GRPO 来训练 大语言模型 解决数独难题!
- Yutai Li 及其合作者发表了一篇非常优秀的论文,指出对于小型模型(smol models),最好从更强大的教师模型中提炼长文本和短文本混合的 CoT 数据。
推理数据集
- KodKode 发布了一个大型编程数据集,包含约 50 万个样本。 这似乎是对 CodeForces-CoTs 的绝佳补充,我们很高兴能用它来训练一些新的模型!
- GeneralReasoning 团队已经开始发布高质量且多样化的数据集,例如
GeneralReasoning/GeneralThought-323K
。 相比于公开可用的数据集,这些数据集涵盖了更多的领域和模型。 他们还提供了一个方便的网站,方便用户浏览数据。 接下来是什么?
通过这次更新,我们现在已经完成了复现计划的第一步和第二步的主要部分:
- 完善精馏数据集的混合,以训练通用推理器。
- 将GRPO扩展到更大的模型,例如
Qwen/Qwen2.5-Coder-32B-Instruct
,以便我们可以获得 R1-Zero 变体。 - 结合来自多个领域的奖励信号,例如数学和代码,并将奖励模型整合到非推理数据中进行评分。