647. Al Mathematical Olympiad - Progress Prize 2 | ai-mathematical-olympiad-progress-prize-2
感谢 Aimo 和 Kaggle 举办这场精彩的比赛。我们也非常感谢比赛期间所有的竞争对手以及所有分享伟大想法的开源项目。现在是时候与社区分享我们的想法和经验了!
我们的解决方案获得了第 2 名。它在公共排行榜上获得 34/50(排名第 1),在私有排行榜上获得 31/50(排名第 2)。
本次比赛需要同时优化效率和推理性能。我们的最终解决方案主要由三部分组成:
对于本地验证,我们使用了 AIME 2025 测试集(30 个问题)以及参考集(10 个问题),评估了平均样本准确率和聚合准确率(通过自一致性),以便对我们的试验解决方案获得初步判断。
我们的训练脚本基于 Light-R1 项目,我们非常感谢他们的工作。
考虑到 DeepSeek-R1-Distill-Qwen-14B 在数学、编码和推理方面的出色表现,我们选择它作为基础模型。
我们将来自 Light-R1 的阶段 2 数据和来自 Limo 的训练数据结合在一起(去除重复项),这两者都是由 deepseek-r1 生成的高难度数学问题的推理轨迹。
我们在单台 8×A800 机器上对基础模型进行了 8 个 epoch 的微调,耗时 11 小时:

准确率提高了,但输出长度也显著增加。
我们使用 DPO 来减少模型的输出长度。
我们选择 OpenR1-Math-220k 的默认子集来构建我们的数据集。
具体来说,我们尝试使用以下四个标准来构建 DPO 对(y_w, y_l 分别表示被选中的响应和被拒绝的响应):
应用前三个标准,我们构建了数据集 dpo-1,用于训练我们提交的模型。
应用全部四个标准,我们构建了数据集 dpo-2,用于训练另一个模型,但其性能与我们提交的模型相似。
我们使用 360-LLaMA-Factory,因为他们添加了序列并行 (SP) 技术以支持在有限内存下进行更长上下文的训练。
我们在单台 8×A800 机器上在 dpo-1 数据集(2k 对)上训练了 4 个 epoch,耗时 40 小时。
通过上述过程,我们得到了最终提交的两个模型:
deepseek-14b-sft-dpo2 和 deepseek-14b-sft-dpo4
我们所有的训练数据都可以在这里找到:
训练数据

注意:在上表中,我们对每个问题采样 32 次(直接推理 16 次和代码求解 16 次)。Pass@1 是在 AIME 2025 数据集上计算的。更多关于我们推理方法的细节将在后面讨论。
我们选择 lmdeploy 作为 LLM 推理框架。与 vllm 相比,带有 TurboMind 引擎的 lmdeploy 框架可以提供更高的吞吐量和更短的模型初始化时间。
我们使用的 lmdeploy 版本是 0.7.0.post1
第一张图片来自 这里

我们应用了 4-bit AWQ 权重量化和 8-bit KV Cache 量化(将配置 main_model.inference_cfg.quant_policy 设置为 8 以使用 lmdeploy 实现的 8-bit KV Cache 量化)。
我们用于提交的两个模型可以在这里找到:模型 1, 模型 2
一些效率结果:

推理工作流如下图所示:提供一个问题作为输入。我们首先准备两种类型的提示词,包括 CoT 提示词和代码提示词("提示词准备任务")。然后,我们让 LLM 开始使用 lmdeploy 批量生成多个样本("LLM 生成任务")。同时,我们 continuously 尝试从每个样本的流式输出中提取答案,聚合多个样本的答案,并判断是否早停某些生成:
stream_infer(...) 调用获得的迭代器的每 N 个 yield 上进行样本级检查,并判断是否早停相应样本的生成。此处使用 Python 代码执行器和答案提取组件。
方法:
# CoT 提示词
- system: "你是一个有用的数学助手。请一步步推理并将答案放在 \\boxed{} 中。"
user_suffix: "\n你擅长推理。\n你必须将最终答案放在 \\boxed{} 中。\n如果最终答案大于 1000,则取模 1000。\n仔细 thoroughly 思考,避免重复。"
# 代码提示词
- system: "你是一个有用的数学助手。请提供 Python 代码来解决数学问题,并将最终答案放在 \\boxed{} 中。"
user_suffix: "\n你擅长编码\n你必须提供 Python 代码,避免冗余分析。\n如果最终答案大于 1000,则取模 1000。\n答案必须是整数。\n每个问题只有一个答案。\n导入必要的库。"
一些实验:
| 模型 | 量化 | 总解决时间 | 平均输出长度 | 聚合正确问题数 (/30) | 平均正确样本数 (/32) | 代码错误细分 (/16) |
|---|---|---|---|---|---|---|
| dpsk-qwen-14b | KV16 | 11838.22 | 9776.94 | 20.00 | 14.63 | 无代码:1.93; 执行错误:2.97; 解析 int 失败:0.13; 错误数字:5.30 |
| dpsk-qwen-14b-awq | AWQ4 KV8 | 6844.75 | 10118.54 | 21.00 | 14.40 | 无代码:3.30; 执行错误:2.53; 解析 int 失败:0.33; 错误数字:4.57 |
| dpsk-qwen-14b-finetune-v1-epoch4 | KV16 | 12971.18 | 11151.10 | 21.00 | 18.90 | 无代码:11.00; 执行错误:0.83; 解析 int 失败:0.03; 错误数字:1.43 |
| dpsk-qwen-14b-finetune-v1-epoch4-awq | AWQ4 KV8 | 7963.94 | 11557.06 | 21.00 | 16.80 | 无代码:11.07; 执行错误:1.30; 解析 int 失败:0.03; 错误数字:0.90 |
动机:通常,推理模型在早期获得答案后会自我怀疑很多,即使它最终通常给出相同的答案。而且在大多数情况下,在 <think></think> 之间给出答案后,模型会再次重写解决方案(至少两次)。我们能否减少 token 的浪费?
方法:虽然我们实验了来自 "Fu et al., Efficiently Serving LLM Reasoning Programs with Certaindex, arXiv 2412" 的主动探测方法,但我们最终采用了一种更简单的样本级早停技术来简化我们的推理工作流。具体来说,一旦我们检测到第一个成功执行的代码或第一个在 "\boxed{…}" 中的答案,我们就停止该样本的生成过程。
一些实验:一个自然的问题是,这是否会损害后来将最初错误的答案修订为正确答案的潜力。在我们的本地测试中,我们使用 scripts/analyze_early_stop.py 验证了这种情况相对罕见,如下图所示。

我们使用常用的自一致性方法进行答案聚合。我们使用如下问题级早停策略。
动机:不同问题的难度各不相同,因此我们 aim 避免在简单问题上花费太多时间。如下图所示,单个问题的样本之间输出长度差异很大。这表明对于某些问题,我们可能很早就获得了几个正确答案,但仍需要等待最长的样本完成——导致显著的时间浪费(例如 q1, q11, q16–q20 等)。

方法:如果通过检查现有答案达到了足够的确定性,我们可以提前停止问题的生成。具体来说,当大多数输出一致时,我们在问题级终止生成,例如,如果 7 个答案中有 5 个一致。详见 imagination_aimo2/local_eval_kaggle.py 中的 early_stop_strategy.consistency_rules 配置。
动机:解决不同难度级别问题所需的时间差异很大,我们设计了一个 adjust_speed 模块来动态调整一些超参数。
方法:随着推理的进行,我们的 adjust_speed 模块计算剩余时间和剩余问题数量,并相应地动态调整模型的采样数量和早停策略。
例如,默认速度是 3(normal),如果系统检测到每个问题的平均剩余时间少于 5 分钟,它会自动将速度调整为 1(fastest)。这意味着样本数量减少到 10 个,每个问题的最大推理时间也减少了。请参阅我们的代码以获取详细实现。
W_k 和 W_q 的重参数化以获得更通道平衡的 key 以更好地进行 KV cache 量化。这在我们的本地测试中带来了一致但较小的推理性能提升,特别是在 KV4 设置中。然而,由于我们没有足够的提交配额来为 KV4 设置调整其他超参数,我们的最终解决方案没有使用 KV4。