{
  "id": 693953,
  "title": "AIMO3: a collection of delightful side quests",
  "content": "<p>For both <a href=\"https://www.kaggle.com/yeoyunsianggeremie\" target=\"_blank\">@yeoyunsianggeremie</a> and me, this is our third time participating in AIMO. Last year, we went all out and put everything into SFT + RLHF (Kaggle post: <a href=\"https://www.kaggle.com/competitions/ai-mathematical-olympiad-progress-prize-2/writeups/so-we-cooked-a-model\" target=\"_blank\">\"So… We cooked a model 🤷\"</a>, detailed blog post: <a href=\"https://hav4ik.github.io/improving-deepseek-r1/\" target=\"_blank\">\"Improving DeepSeek R1 in Math\"</a>). This year, with Geremie grinding 75+ hours per week at his startup and me in the middle of a release, we had almost no time to compete like we did last year. Luckily, we had companions helping us: Geremie's <a href=\"https://github.com/bogoconic1/Qgentic-AI\" target=\"_blank\">bogoconic1/Qgentic-AI</a> and my Claude Code Max subscription!</p>\n<p>Despite joining the competition only in the last couple of weeks, we got delightfully stuck in a few side quests, and ended up creating a few useful artifacts that we think are worth sharing.</p>\n<p>We'd like to express our gratitude to AIMO3 organizers and XTX market for hosting such a fun competition and providing H100 GPUs. Special thanks to Simon Frieder (<a href=\"https://www.kaggle.com/friederrr\" target=\"_blank\">@friederrr</a>) for actively engaging with the community, making it one of the most pleasant competitions I've ever participated in.</p>\n<hr>\n<h1>Summary</h1>\n<h2>Open-Source Contributions</h2>\n<p>Our little fun time with AIMO3 resulted in following open-source side quests, useful beyond this competition:</p>\n<ul>\n<li><strong>A collection of quantized Nemotron Cascade 2 models</strong>, <a href=\"https://huggingface.co/chankhavu/Nemotron-Cascade-2-30B-A3B-FP8\" target=\"_blank\">chankhavu/Nemotron-Cascade-2-30B-A3B-FP8</a> and <a href=\"https://huggingface.co/chankhavu/Nemotron-Cascade-2-30B-A3B-NVFP4\" target=\"_blank\">chankhavu/Nemotron-Cascade-2-30B-A3B-NVFP4</a>, totaling 60K downloads on Huggingface 🤗. Hybrid Mamba-MoE models are tricky to quantize because the Mamba state is sensitive to numerical precision; these FP8 and NVFP4 variants were first ones of such precisions published for Cascade 2 and show no degradation in quality.</li>\n<li><strong>Enabled Hybrid-Mamba models to use Prefix Caching (PC) with Speculative Decoding (SD) in vLLM.</strong> PC (reuse KV tensors across the common prefix of sibling generations) and SD (draft K tokens with a small model, verify K at once with the large one) are standard inference-time tricks, but running them together on Hybrid-Mamba models causes <em>mamba cache poisoning</em> — rejected speculative tokens leak into the shared Mamba state — and has been an open problem in both vLLM and SGLang. <a href=\"https://github.com/hav4ik/vllm/tree/pc-spec-v2-nonpc-slots\" target=\"_blank\">Our custom vLLM fork</a> solves it with per-request \"ghost\" Mamba states. See <em>Upstream PR status</em> in the vLLM section below for upstreaming status.</li>\n<li><strong>An EAGLE-3 speculator for Nemotron Cascade 2:</strong> <a href=\"https://huggingface.co/chankhavu/Nemotron-Cascade2-30B-A3B-Eagle3-Long-Context\" target=\"_blank\">chankhavu/Nemotron-Cascade2-30B-A3B-Eagle3-Long-Context</a> — the small draft-head model SD uses to propose tokens — reaching <strong>3.55</strong> mean accepted tokens at K=7. For long-context speculator training we <a href=\"https://github.com/hav4ik/SpecForge-Nemotron3/tree/nemotron-cascade-2-experiments\" target=\"_blank\">modified SpecForge</a> to use chunked fused soft-target CE, chunked MLP, sliding attention, and correct handling of Mamba SSM states. We'll make a PR to SpecForge upstream as well.</li>\n</ul>\n<p>We also uncovered cheating attempts by Cascade 2 on Math problems (searching the web for answers) and reported to model authors here: <a href=\"https://huggingface.co/nvidia/Nemotron-Cascade-2-30B-A3B/discussions/25#69e67838889c53e909054431\" target=\"_blank\">Cascade-2 cheating attempts on Math is… cute</a>.</p>\n<h2>Our final submission</h2>\n<blockquote>\n  <p>Kaggle notebook: <a href=\"https://www.kaggle.com/code/chankhavu/c2-distill-vllm018-watchdog\" target=\"_blank\">chankhavu/c2-distill-vllm018-watchdog</a>\n  Model: <a href=\"https://www.kaggle.com/models/chankhavu/c2-legaldistill-30b-a3b-fp8/transformers/fp8/1\" target=\"_blank\">chankhavu/c2-legaldistill-30b-a3b-fp8</a></p>\n</blockquote>\n<p>Our solution uses <a href=\"https://huggingface.co/nvidia/Nemotron-Cascade-2-30B-A3B\" target=\"_blank\">Nemotron-Cascade-2-30B-A3B</a>, which we soft-cloned via layer-wise linear distillation to make it legal for this competition (Cascade 2 was released after the March 15 cutoff — see the Introduction below), then selectively quantized to FP8 with no quality degradation.</p>\n<p>The final harness is intentionally simple: <strong>8 parallel rollouts per problem with Python tool use, entropy-weighted majority voting</strong>. We also tried a more sophisticated variant that filled the GPU idle bubbles between early- and late-finishing rollouts with pairwise LLM-as-judge self-verification, but didn't have time to tune it properly to beat plain majority voting. We also put a great deal of work to make speculative decoding work with prefix caching for hybrid-mamba models, but it didn't get into our final submission due to lack of submission budget and time to test things out properly. Summary of our main candidates on local validation and the Kaggle leaderboard:</p>\n<table>\n<thead>\n<tr>\n<th><strong>Submission</strong><br>(model, harness)</th>\n<th><strong>Local validation</strong><br>(3 shuffles)</th>\n<th><strong>Local validation</strong><br>(mean)</th>\n<th><strong>Public LB attempts</strong></th>\n<th><strong>Private LB</strong></th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>gpt-oss-120b, 44/50 public notebook</td>\n<td>33, 30, 36</td>\n<td>33.0</td>\n<td>38, 38, 37</td>\n<td>—</td>\n</tr>\n<tr>\n<td>Cascade 2, 8 rollouts, self-verification (untuned)</td>\n<td>33, 31</td>\n<td>32.0</td>\n<td>34</td>\n<td>—</td>\n</tr>\n<tr>\n<td><strong>Cascade 2, 8 rollouts, voting</strong><br><strong>(our final submission)</strong></td>\n<td><strong>36, 34, 34</strong></td>\n<td><strong>34.6</strong></td>\n<td><strong>36, 39, 38</strong></td>\n<td><strong>41</strong></td>\n</tr>\n</tbody>\n</table>\n<p>Our local validation imitates the Kaggle leaderboard setting — 50 hard IMO-level problems drawn from <a href=\"https://imobench.github.io/\" target=\"_blank\">IMO-AnswerBench</a> (an open benchmark of IMO-style problems with short-integer answers), 5 hours on 1× H100 SXM. We shuffle the 50 problems 3 times with fixed seeds and run each solver through each shuffle, which gives us a controlled way to compare candidates without burning LB submissions.</p>\n<hr>\n<h1>Introduction</h1>\n<p><strong>The competition.</strong> <a href=\"https://www.kaggle.com/competitions/ai-mathematical-olympiad-progress-prize-3\" target=\"_blank\">AIMO3</a> (the AI Mathematical Olympiad Prize 3, hosted by Kaggle and XTX Markets) evaluates math-problem solvers on 50 held-out IMO-level problems with short-integer answers. Each submission is a notebook that runs on a single NVIDIA H100 SXM (80 GB) with a 5-hour wall-clock budget. Models published after March 15, 2026 are not allowed to be used directly at runtime, which is the rule that shapes most of the model choices below. The public leaderboard is scored on a visible subset; the final standings use a disjoint private set revealed at the end.</p>\n<p><strong>The main model candidate</strong> everyone converged on this year is <a href=\"https://huggingface.co/openai/gpt-oss-120b\" target=\"_blank\">gpt-oss-120b</a> — OpenAI's 120 B-parameter open-weights reasoning model with native mxfp4 weights and a Harmony chat format. It's the model behind the strongest public-notebook baseline (the \"44/50\" notebook on the AIMO3 discussion board), and our main point of comparison throughout.</p>\n<p>In March 19, <a href=\"https://www.kaggle.com/chankhavu\" target=\"_blank\">@chankhavu</a>'s birthday, <a href=\"https://huggingface.co/nvidia/Nemotron-Cascade-2-30B-A3B\" target=\"_blank\">Nemotron-Cascade-2-30B-A3B</a> (shortened to <em>Cascade 2</em> in this post) was released — NVIDIA's 30 B hybrid Mamba-MoE reasoning model. Published just after the March 15 cutoff, so not directly usable as-is, but the architecture is identical to the pre-cutoff <a href=\"https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16\" target=\"_blank\">Nemotron-3-Nano-30B-A3B</a>. This is what made the \"soft-cloning\" approach legal, and it's the model inside our final submission. The hybrid-Mamba architecture is what forced us down a bunch of the side quests below — quantizing it, getting prefix caching to work with speculative decoding, training an Eagle3 speculator, and so on.</p>\n<p><strong>Other participant's findings.</strong> With only a few weeks to compete, we relied heavily on the open-source ecosystem and the community's collective findings. Some highlights:</p>\n<ul>\n<li><a href=\"https://www.kaggle.com/code/pawanmali/chasing-47-50-aimo3-journey-of-60-experiments\" target=\"_blank\">AIMO3 journey of 60 experiments</a> by <a href=\"https://www.kaggle.com/pawanmali\" target=\"_blank\">@pawanmali</a> - this incredible repository of experimentation results guided us towards simpler prompts in our solutions.</li>\n<li><a href=\"https://www.kaggle.com/code/nihilisticneuralnet/44-50-let-me-over-cook\" target=\"_blank\">44/50 public notebook</a> by <a href=\"https://www.kaggle.com/nihilisticneuralnet\" target=\"_blank\">@nihilisticneuralnet</a> - the strongest public notebook baseline, using gpt-oss-120b with a custom chat format and a lot of engineering to get the most out of it.</li>\n<li><a href=\"https://arxiv.org/abs/2603.27844\" target=\"_blank\">Inference-Time Optimization Lessons from AIMO 3</a> by Natapong Nitarach - similar ablation findings to <a href=\"https://www.kaggle.com/pawanmali\" target=\"_blank\">@pawanmali</a>.</li>\n</ul>\n<h1>Delightful side quests</h1>\n<h2>Soft-cloning Cascade 2</h2>\n<p>The competition rules prohibits using models published after March 15 directly during runtime. However, there's no limit on distillation or how distillation should be made. We rely on the following <a href=\"https://www.kaggle.com/competitions/ai-mathematical-olympiad-progress-prize-3/discussion/680682\" target=\"_blank\">FAQ clarification from the competition organizers</a>:</p>\n<blockquote>\n  <p><strong>Question:</strong> Say I initialize an entirely new model with same architecture as the post - cutoff teacher model, but then train it by layerwise MSE to the teacher on the residual stream; in the limit this could approximate the teacher model weights, I just didn't explicitly say that I used the weights themselves.</p>\n  <p><strong>Answer:</strong> Indeed, while conceptually, these two cases can blur, practically they don't and that is why the AMLT rules makes sense: In practice, with this approach you'd recreate in a way the model that was off limits.</p>\n</blockquote>\n<p>We further clarified with the organizers that this kind of \"soft cloning\" is legal for the competition (<a href=\"https://www.kaggle.com/competitions/ai-mathematical-olympiad-progress-prize-3/discussion/680682#3425516\" target=\"_blank\">here</a> and <a href=\"https://www.kaggle.com/competitions/ai-mathematical-olympiad-progress-prize-3/discussion/680682#3440271\" target=\"_blank\">here</a>).</p>\n<h3>Approach</h3>\n<p>After confirming the legality, we performed the following layer-wise MSE distillation from <a href=\"https://huggingface.co/nvidia/Nemotron-Cascade-2-30B-A3B\" target=\"_blank\">Nemotron-Cascade-2</a>. We initialized a randomly initialized student model with the same architecture and performed the following procedure:</p>\n<ul>\n<li>We build a collection of linear models $$F^\\text{Teacher}_i(X) = W^{Teacher}_i \\times X$$ for each layer <code>i</code> of the teacher <a href=\"https://huggingface.co/nvidia/Nemotron-Cascade-2-30B-A3B\" target=\"_blank\">Nemotron-Cascade-2</a> model, where <code>X</code> is the input to that layer and <code>W_i</code> the weight tensor.</li>\n<li>We do the same for the student model, building $$F^\\text{Student}_i(X) = W^{Student}_i \\times X$$ for each layer <code>i</code>. The student model happens to have the same architecture, so we can directly map each <code>F^Teacher}_i</code> to a student <code>F^Student_i</code>.</li>\n<li>We optimize each student layer <code>i</code> to minimize the following MSE loss: $$L_i = \\mathbb{E}_{X \\sim D} \\left[ | F^\\text{Teacher}_i(X) - F^\\text{Student}_i(X) |^2_2 \\right]$$ where <code>D</code> is a dataset of randomly sampled inputs. We optimize either by gradient descent or by closed-form MSE optimization techniques. Each student treats the teacher as a black-box function that produces outputs for given inputs.</li>\n</ul>\n<p>We <strong>never</strong> optimize towards the teacher weights directly; we only observe the teacher outputs and use them as MSE targets for the student. However, since the sub-models are linear, convergence is mathematically absolute. We published the implementation <a href=\"https://gist.github.com/hav4ik/e4b3513bb4ece8b45fc414e024aeae72\" target=\"_blank\">here</a>. The BF16 model can be found here: <a href=\"https://www.kaggle.com/models/chankhavu/bogoconic1-c2-softcpy-v3\" target=\"_blank\">chankhavu/bogoconic1-c2-softcpy-v3</a>.</p>\n<h2>Quantizing the Cascade 2 model</h2>\n<p><strong>Important clarification:</strong> for Kaggle submission, we quantized the cloned Cascade 2 model separately from the cloned BF16 model! We did NOT use our published open-source models.</p>\n<blockquote>\n  <p>Open-source artifacts (totaling 66K downloads on Huggingface as of April 21, 2026):</p>\n  <ul>\n  <li>🤗 Huggingface model: <a href=\"https://huggingface.co/chankhavu/Nemotron-Cascade-2-30B-A3B-NVFP4\" target=\"_blank\">chankhavu/Nemotron-Cascade-2-30B-A3B-NVFP4</a></li>\n  <li>🤗 Huggingface model: <a href=\"https://huggingface.co/chankhavu/Nemotron-Cascade-2-30B-A3B-FP8\" target=\"_blank\">chankhavu/Nemotron-Cascade-2-30B-A3B-FP8</a></li>\n  <li>🤗 Huggingface model: <a href=\"https://huggingface.co/chankhavu/Nemotron-Cascade-2-30B-A3B-AWQ-INT4\" target=\"_blank\">chankhavu/Nemotron-Cascade-2-30B-A3B-AWQ-INT4</a></li>\n  </ul>\n  <p>Quantization code: <a href=\"https://www.kaggle.com/code/chankhavu/quantize-cascade-2\" target=\"_blank\">chankhavu/quantize-cascade-2</a>; calibration data: <a href=\"https://www.kaggle.com/datasets/chankhavu/cascade-2-calibration-data\" target=\"_blank\">chankhavu/cascade-2-calibration-data</a>.</p>\n</blockquote>\n<h3>Precision recipe</h3>\n<p>We followed the Nano-architecture selective quantization recipe from the <a href=\"https://research.nvidia.com/labs/nemotron/files/NVIDIA-Nemotron-3-Nano-Technical-Report.pdf\" target=\"_blank\">Nemotron 3 Nano Technical Report</a> (Section 4). This is the same recipe as <a href=\"https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-FP8\" target=\"_blank\">NVIDIA's official FP8 checkpoint</a> of Nemotron-3-Nano-30B-A3B. Sensitive components are kept in higher precision:</p>\n<ul>\n<li>There are only 6 attention layers, so they are highly sensitive and should be preserved in BF16.</li>\n<li>To match the input granularity of attention layers, Mamba layers adjacent to attention layers should be kept at BF16 as well.</li>\n<li>The Mamba 1D conv is kept at BF16 across all 23 Mamba layers (quantizing it degrades the state update).</li>\n<li>MoE router gates should be kept at FP32. Degradation in routing precision will affect reasoning.</li>\n</ul>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2Fca39d8cf5ccf892e7080773c16103025%2Fcascade2_quant_layout.png?generation=1776888105108688&amp;alt=media\" alt=\"Per-layer quantization layout for Nemotron Cascade 2: 52 transformer layers flanked by Embedding and lm_head. Attention layers and the Mamba layer immediately preceding each attention are kept BF16; the other Mamba layers and all MoE expert blocks are quantized to FP8 / NVFP4.\"></p>\n<h3>Implementation details</h3>\n<p>For calibration, we constructed a dataset of <strong>4000 samples</strong> from <a href=\"https://huggingface.co/datasets/nvidia/Nemotron-Cascade-2-SFT-Data\" target=\"_blank\">Nemotron-Cascade-2-SFT-Data</a> with the following domain mix, specifically designed for long-context reasoning: math (1000), swe (900), terminal_agent (500), science (500), chat (400), conversational_agent (300), instruction_following (300), safety (100). We trained on sequence length of up to <strong>12,288 tokens</strong> (no padding, natural length per sample).</p>\n<p>We validated the correctness of our quantized models using <a href=\"https://github.com/NVIDIA-NeMo/Evaluator\" target=\"_blank\">NVIDIA-NeMo/Evaluator</a> with config from <a href=\"https://github.com/NVIDIA-NeMo/Evaluator/blob/main/packages/nemo-evaluator-launcher/examples/nemotron/nemotron-3-super/local_nemotron-3-super-120b-a12b_tools.yaml\" target=\"_blank\">Nemotron-3-Super-120B's eval config</a>. We see no degradation in reasoning quality of quantized models. More details in <em>Appendix B</em>.</p>\n<h2>Enable Hybrid-Mamba models to use PC and SD together in vLLM</h2>\n<blockquote>\n  <p>Open source artifact: <a href=\"https://github.com/hav4ik/vllm/tree/pc-spec-v2-nonpc-slots\" target=\"_blank\">hav4ik/vllm branch pc-spec-v2-nonpc-slots</a></p>\n</blockquote>\n<h3>The problem</h3>\n<p>Out of the box, neither vLLM nor SGLang can run Prefix Caching (PC) and Speculative Decoding (SD) together on Hybrid-Mamba models. Mamba's SSM layer keeps a fixed-size state vector updated <em>in place</em> — each token folds into it irreversibly. SD runs K candidate tokens through the model before the rejection sampler decides how many to keep; by then the SSM kernel has already overwritten the canonical state with the full K-token trajectory. Transformers escape this because their KV cache is append-only — rejected entries sit past the sequence end and touch nothing. Mamba has one state vector per layer per request and can't roll it back, so PC + SD needs an explicit mechanism to preserve the accepted state.</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2F6f4d1ef1b007dbf7421e0ef6f972dae9%2Fpc_sd_problem.png?generation=1776888162924985&amp;alt=media\" alt=\"When speculation crosses a PC block boundary, Mamba's shared state has already been overwritten by later tokens — the commit writes the final state (s7) into Block 0's cache slot instead of the correct boundary state (s4)\"></p>\n<h3>The correctness fix</h3>\n<p>The non-PC spec-decode path already does the right thing: the SSM and conv Triton kernels maintain K+1 per-request \"ghost\" slots with native <code>init_token_idx</code> rollback — those slots are just gated out whenever PC is active. We re-enable them and add a one-way <strong>boundary commit</strong>: only when a request's last accepted token lands exactly on a mamba-block boundary (<code>last_accepted_pos % block_size == block_size - 1</code>) do we write the ghost slot back to the PC pool. On every other step the pool is untouched, so the canonical cache state is never contaminated by rejected speculations. Ghost slots are indexed by batch position, so batch reordering moves them automatically — no extra bookkeeping.</p>\n<p>One memorable debug moment: the first cut of <code>init_spec_slots</code> allocated each spec slot at the pool's page-padded stride (~50 MB/slot). At 32 seqs × K=5 × 23 layers that comes out to <strong>52 GB</strong> — instant OOM. The fix was to allocate contiguously (~1.3 MB/slot); the kernels read <code>state.stride(0)</code> off the tensor itself, so ghost slots and pool slots can have completely different strides without breaking anything.</p>\n<p>We used AIME-25 as a unit test throughout, with a pass threshold of <strong>≥95% per-session accuracy</strong> — ironic, because AIME-25 is a notoriously hard competition that few high schoolers solve reliably, but at this point it's well within what a modern reasoning LLM with tool use can do consistently. A reliable gold standard for \"nothing silently corrupted\" is useful regardless.</p>\n<h3>Performance optimization</h3>\n<p>Once the correctness fix was in, Eagle3 was still a net loss at single-sequence concurrency: a full step took ~18 ms vs ~3.4 ms for no-Eagle, even though the target forward itself is only 4.2 ms. Profiling (H100, K=4, p=1) split the 18 ms budget roughly like this:</p>\n<table>\n<thead>\n<tr>\n<th>Stage</th>\n<th>Time (ms)</th>\n<th>Notes</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Target forward (K+1 tokens)</td>\n<td>4.2</td>\n<td>actual model compute</td>\n</tr>\n<tr>\n<td>Drafter, K=4 iterations</td>\n<td>11.2</td>\n<td><strong>serial</strong>: 4 × (~2 ms Python metadata + ~0.8 ms GPU forward)</td>\n</tr>\n<tr>\n<td><code>_build_attention_metadata</code></td>\n<td>5.7</td>\n<td>7× more than no-Eagle — all 52 layers × K+1</td>\n</tr>\n<tr>\n<td><code>_prepare_inputs</code></td>\n<td>1.7</td>\n<td>K+1 tokens per request</td>\n</tr>\n<tr>\n<td><code>commit_boundary_states</code> (23 mamba layers)</td>\n<td>3.9</td>\n<td><code>.nonzero()</code> D2H sync every step + <code>.synchronize()</code> wait</td>\n</tr>\n<tr>\n<td>Rejection sampling + misc</td>\n<td>1.1</td>\n<td></td>\n</tr>\n</tbody>\n</table>\n<p>Two things stood out. First, the drafter loop is serially CPU-bound: each iteration needs the previously accepted token before it can build the next metadata object, so Python rebuilds can't overlap the 0.8 ms GPU forwards, and the GPU sits idle ~70% of the drafter phase. Second, <code>commit_boundary_states</code> — Mamba-specific infrastructure — was paying a full host-sync tax on every step, not just the rare steps where a commit actually fires.</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2Ff9ef240767d03da1ecfd5b621f84b027%2Feagle3_step_timeline.png?generation=1776888210353385&amp;alt=media\" alt=\"Eagle3 step timeline showing CPU/GPU overlap and the serial drafter ping-pong\"></p>\n<p>The drafter issue needs a proper architectural fix (fuse all K iterations into one captured graph with persistent metadata buffers) that we left for upstream. What we did land is a sweep of surgical cuts targeting the other stages:</p>\n<ul>\n<li><strong>Masked Triton commit kernel.</strong> <code>commit_boundary_states</code> used to call <code>.nonzero()</code> on a CUDA bool mask to find requests that had crossed a mamba-block boundary. <code>.nonzero()</code> forces a device → host sync every step. The new kernel grid is <code>(num_layers, num_requests)</code>: each block checks its mask bit on-device and returns early if unset. No host ever sees the mask. <strong>−1.5 ms/step.</strong></li>\n<li><strong>Drop the deferred-commit <code>synchronize()</code>.</strong> The deferred-commit path was calling <code>sample_complete_event.synchronize()</code> — a blocking CPU wait — to guarantee <code>num_accepted_tokens</code> was visible before launching the commit kernel. Since both run on the same CUDA stream as the sampler, stream ordering already guarantees visibility. Removed. <strong>−1.0 ms/step.</strong></li>\n<li><strong>Re-enable <code>update_block_table</code> + batched commit indices.</strong> Two related cleanups on the commit path that were paying per-layer overhead on pure-decode steps. <strong>−4.5 ms/step combined.</strong></li>\n<li><strong>Fast decode <code>attn_meta</code>.</strong> On pure-decode steps, only <code>state_indices_tensor_d</code> differs between the 23 mamba attention builders, but <code>_prepare_inputs</code> was rebuilding the full metadata struct each time. Cache the first build; for the other 22, <code>copy_</code> the per-layer pool mapping into the persistent buffer and <code>dataclasses.replace</code> just that field. <strong>−0.3 ms/step.</strong></li>\n</ul>\n<p>End state at single-sequence (K=4, T=1.0): step time 18 → 8.9 ms, throughput 171 → 298 tok/s vs 303 tok/s for PC-only — essentially at parity.</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2Fb3245aec078557c3cb820f13d9fc5eae%2Feagle3_step_timeline_after.png?generation=1776888256293073&amp;alt=media\" alt=\"Eagle3 step timeline after all optimizations — step ends at 8.9 ms, no more sync stall, drafter phase compact\"></p>\n<p>At higher concurrency, where the serial drafter overhead is amortized across the batch, Eagle3 becomes a real win:</p>\n<table>\n<thead>\n<tr>\n<th>Concurrency</th>\n<th>PC-only</th>\n<th>Eagle3 K=4, T=0.7</th>\n<th>vs PC-only</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>p=1</td>\n<td>303 tok/s</td>\n<td>349 tok/s</td>\n<td>+15%</td>\n</tr>\n<tr>\n<td>p=8</td>\n<td>1186 tok/s</td>\n<td>1552 tok/s</td>\n<td>+31%</td>\n</tr>\n<tr>\n<td>p=12</td>\n<td>1383 tok/s</td>\n<td>1843 tok/s</td>\n<td>+33%</td>\n</tr>\n</tbody>\n</table>\n<h3>What we didn't land</h3>\n<p>On an experimental branch we captured the K-iteration drafter decode loop as a single FULL cudagraph, replayed in 0.16 ms. Draft-phase time dropped from 3.16 ms to 1.39 ms (−56%) with acceptance rates preserved. But overall step time at p=8 was unchanged (13.7 ms) — the CPU savings got absorbed by GPU pipeline stalls in the prep phase, since the drafter's GPU work that previously overlapped the PIECEWISE Python loop now blocked the next iteration. The theoretical payoff is at p=12+ where CPU dispatch becomes the real bottleneck, but we didn't validate that before the deadline.</p>\n<h3>Upstream PR status</h3>\n<p>We had planned to upstream the correctness fix and follow with the perf path, but two things have changed since the competition closed and the PR is currently blocked:</p>\n<ul>\n<li><strong>Hopper-only optimizations don't compile on Blackwell.</strong> Our perf work assumed Hopper SM 9.0 capabilities (cudagraph construction, scheduler shape, the specific PIECEWISE → FULL upgrade we pushed for at p=12+). On Blackwell SM 10.0 the same code hits graph-compilation issues that we haven't resolved yet. vLLM upstream maintains GPU-portable code paths, so we can't merge as-is without a Blackwell-clean rewrite of the kernel layer.</li>\n<li><strong>A separate community PR now covers the correctness fix.</strong> One week after the competition deadline, <a href=\"https://github.com/vllm-project/vllm/pull/41233\" target=\"_blank\">vllm-project/vllm#41233</a> was opened against the same mamba-cache-poisoning bug. It's still open as of writing, but it tackles correctness from a different angle and is likely the path upstream takes. Our perf optimizations (the FlashInfer attention switch and the surgical \"What we did land\" cuts) are orthogonal to that PR and would still apply on top of it — that PR doesn't include any speed work — but we're holding off on a perf-only PR until the correctness side lands and our Blackwell story is fixed.</li>\n</ul>\n<p>The fork itself remains useful for anyone running Hybrid-Mamba SD inference on Hopper; we just can't claim the upstream PR as a clean deliverable any more.</p>\n<h2>Training an Eagle3 speculator for Cascade 2</h2>\n<blockquote>\n  <p>Open-source artifacts:</p>\n  <ul>\n  <li>🤗 Huggingface model: <a href=\"https://huggingface.co/chankhavu/Nemotron-Cascade2-30B-A3B-Eagle3-Long-Context\" target=\"_blank\">chankhavu/Nemotron-Cascade2-30B-A3B-Eagle3-Long-Context</a></li>\n  <li>🤗 Huggingface dataset: <a href=\"https://huggingface.co/datasets/chankhavu/c2_eagle3_train\" target=\"_blank\">chankhavu/c2_eagle3_train</a> (2 stage train data)</li>\n  <li>🤗 Huggingface dataset: <a href=\"https://huggingface.co/datasets/chankhavu/c2-traces-10197\" target=\"_blank\">chankhavu/c2-traces-10197</a> (collected reasoning traces on 10'197 hard math problems)</li>\n  </ul>\n  <p>Code: <a href=\"https://github.com/hav4ik/SpecForge-Nemotron3/tree/nemotron-cascade-2-experiments\" target=\"_blank\">SpecForge-Nemotron3</a>.</p>\n</blockquote>\n<p>To our knowledge this is the first Eagle3 draft head trained against a hybrid Mamba-Transformer verifier that's verified end-to-end with vLLM. We forked <a href=\"https://github.com/sgl-project/SpecForge\" target=\"_blank\">SpecForge</a> as <a href=\"https://github.com/hav4ik/SpecForge-Nemotron3/tree/nemotron-cascade-2-experiments\" target=\"_blank\">SpecForge-Nemotron3</a> and added the engineering needed to fit long-context training (<code>L=65536, ttt=6</code>) on 2× RTX 6000 Pro Blackwell (96 GB each).</p>\n<h3>Data and recipe</h3>\n<p>Two stages: a broad SFT bootstrap on ~20K conversations (378M untruncated tokens, from Cascade 2's supervised-finetune distribution) followed by on-policy fine-tuning on ~9.7K Cascade 2 reasoning traces (207M tokens) — <strong>~292M loss-masked tokens total</strong>. We prune the 131K target vocab to a 32K draft vocab via top-K token frequency on the loss-masked positions, which keeps 99.77% coverage with ~4× smaller embedding / lm_head. Stage 1 uses LR 1e-4 with length-bucketed sampling (eliminates straggler-rank waste at <code>batch_size=1</code>); stage 2 drops to LR 5e-5 and disables bucketing for gradient diversity.</p>\n<h3>Engineering</h3>\n<p>Two classes of fixes were required to get this training loop working at long context.</p>\n<p><strong>Correctness — FP32 SSM scan state.</strong> Upstream <code>mamba_ssm</code> 2.3.1 silently downcasts the SSM scan boundary state to BF16 inside its Triton kernel. We monkey-patch it to force FP32, bit-equivalent to vLLM 0.19's NemotronH default. Without this, training converges to a worse minimum that costs ~10% absolute on AIME-class math.</p>\n<p><strong>Memory footprint — fitting L=65k on 96 GB.</strong> Naïve training peaks north of 120 GiB (OOM on 96 GB). Four independent activation-memory cuts on the draft forward + TTT unroll (chunked fused linear + soft-target CE for the lm_head, freeing the verifier's 131k-vocab logits before TTT unrolling, Liger-style chunked MLP, sliding-window attention on the draft) bring peak to <strong>~88–91 GiB</strong>, with the single biggest win being the verifier-logit release. Step time at L=65k ttt=6 is ~4 s/step (high variance across sequence lengths). Separately, a 100× speedup in the loss-mask parser that was otherwise re-tokenizing each conversation <code>num_turns</code> times. Full details in <em>Appendix C</em>.</p>\n<h3>Results</h3>\n<p>Temperature sweep, mean accepted tokens per step (on AIME-25):</p>\n<table>\n<thead>\n<tr>\n<th>K</th>\n<th>T=0.0</th>\n<th>T=0.7</th>\n<th>T=0.8</th>\n<th>T=0.9</th>\n<th>T=1.0</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>K=4 (out of K+1=5)</td>\n<td><strong>3.33</strong></td>\n<td>3.00</td>\n<td>3.05</td>\n<td>2.88</td>\n<td>2.91</td>\n</tr>\n<tr>\n<td>K=7 (out of K+1=8)</td>\n<td><strong>3.55</strong></td>\n<td>3.26</td>\n<td>3.14</td>\n<td>3.03</td>\n<td>2.90</td>\n</tr>\n</tbody>\n</table>\n<p>Lower temperature → draft head is more confident and its proposals track the target's argmax more closely, so acceptance climbs. You can find more information in <em>Appendix C</em>.</p>\n<h2>Cascade 2 searches the web when it's desperate</h2>\n<p>The sandbox we gave Cascade 2 for tool use was a <code>stateful_python_code_exec</code> Jupyter kernel with network egress not blocked — <code>urllib</code>, <code>requests</code>, and <code>pip install</code> all worked. On 10,197 rollouts against IMO-class problems, Cascade 2 issued real HTTP calls in <strong>246 traces</strong> (~2.4%).</p>\n<p>Top destinations (HTTP URL literals in tool-call code, per-trace dedup), by category:</p>\n<ul>\n<li><strong>Reference sites</strong> — <code>artofproblemsolving.com</code> (64), <code>en.wikipedia.org</code> (58), <code>oeis.org</code> (28), <code>mathworld.wolfram.com</code> (6)</li>\n<li><strong>Web search</strong> — <code>google.com</code> (40), <code>duckduckgo.com</code> (39), <code>bing.com</code> (3)</li>\n<li><strong>Q&amp;A archives</strong> — <code>math.stackexchange.com</code> (13), <code>api.stackexchange.com</code> (12), <code>stackoverflow.com</code> (3)</li>\n<li><strong>Competition archives</strong> — <code>imo-official.org</code> (11), <code>purplecomet.org</code> (5)</li>\n<li><strong>Misc</strong> — <code>raw.githubusercontent.com</code> (8), <code>arxiv.org</code> (3), <code>api.github.com</code> (3)</li>\n</ul>\n<p>The dominant pattern is AoPS wiki lookups by year/contest/number and Wikipedia for theorem statements. OEIS (28 traces) is usually queried with a computed-prefix sequence. DuckDuckGo Instant Answer API and <code>google.com/search</code> GETs with the prompt pasted verbatim show up often enough to be a recognizable tactic.</p>\n<p>A few representative (amusing) traces:</p>\n<ul>\n<li><strong>IMO 2021 Shortlist PDF</strong> — <code>urllib.request.urlopen</code> against <code>imo-official.org</code>, written to disk (byte-exact match to the real PDF), <code>PyPDF2.PdfReader</code> over every page, substring-grep for prompt keywords. Extraction worked; the problem wasn't in the document. Answer happened to be 0 → coincidental match.</li>\n<li><strong>Purple Comet 2023 HS Final</strong> — <code>requests.get(...)</code>, saved response, tried <code>PyPDF2</code> / <code>pdftotext</code> / <code>fitz</code> / <code>pdfminer</code> — all four failed with <code>EOF marker not found</code> / <code>No /Root object</code>. First 100 bytes of the \"PDF\" were <code>&lt;!DOCTYPE html&gt;</code>; the server returned an HTML error page with a <code>.pdf</code> extension. Wrong answer.</li>\n<li><strong>IMO shortlist in-memory grep</strong> — fetched <code>IMO2021SL.pdf</code> and <code>IMO2022SL.pdf</code> into memory via <code>response.read()</code>, never persisted, <code>grep_bytes(b'Mario')</code>. Problem was IMO 2022 SL C2 (\"Mario and Bowser\"). Wrong answer.</li>\n<li><strong>purplecomets.org</strong> — <code>!wget</code> with a typo (trailing <code>s</code>), DNS fails.</li>\n</ul>\n<p>Network-using traces end up at <strong>34.6%</strong> correct — these are the IMO-level problems the model couldn't crack with direct reasoning in the first place, and no venue in the breakdown meaningfully changes the picture (wikipedia 36.6%, AoPS 27.9%, OEIS 27.0%, Google search 42.9% at the top). That said, the model <strong>recognizing a problem as a benchmark item</strong> and reaching for the right contest archive is a contamination signal on its own, independent of whether the lookup paid off.</p>\n<p>We reported this to the Nemotron team (<a href=\"https://huggingface.co/nvidia/Nemotron-Cascade-2-30B-A3B/discussions/25#69e67838889c53e909054431\" target=\"_blank\">HF discussion</a>) and released the verified traces: 🤗 <a href=\"https://huggingface.co/datasets/chankhavu/nemotron-cascade2-cheating-attempts\" target=\"_blank\">chankhavu/nemotron-cascade2-cheating-attempts</a> (173 rows). For our own runs since, we block egress at the sandbox namespace level.</p>\n<hr>\n<h1>Back to Kaggle AIMO3</h1>\n<p>With the open-source artifacts above in hand — the soft-cloned FP8 Cascade 2, the vLLM PC+SD fix, the Eagle3 speculator — we now turn to the competition itself: how these pieces line up against gpt-oss-120b on the benchmarks, what we actually submitted, and which harnesses we tried but didn't ship.</p>\n<h2>Evaluation set</h2>\n<blockquote>\n  <p>We publish the dataset construction notebook here: <a href=\"https://www.kaggle.com/code/chankhavu/local-validation-benchmark\" target=\"_blank\">https://www.kaggle.com/code/chankhavu/local-validation-benchmark</a>.</p>\n  <p>All throughput analysis in this section are based on the model alone, without speculative decoding.</p>\n</blockquote>\n<p>We construct a candidate pool of 63 problems by combining the 10 official AIMO3 reference problems with 53 hard problems from <a href=\"https://imobench.github.io/\" target=\"_blank\">IMO-AnswerBench</a> — specifically those where gpt-oss-120b scored 6/8 or less on avg@8, restricted to answers in the Kaggle 0–99999 integer range. The pool draws from IMO Shortlists, USAMO, HMMT, Sharygin, RMM, and national TSTs (Vietnam, China, Korea, Japan). We then take a 50-problem subset to match the Kaggle Leaderboard setting used for local validation above.</p>\n<p>For the gpt-oss-120b tool-use accuracy stats, we use the <a href=\"https://www.kaggle.com/datasets/yiyangzheng/answerbench-gpt-oss\" target=\"_blank\">answerbench-gpt-oss</a> dataset by <a href=\"https://www.kaggle.com/yiyangzheng\" target=\"_blank\">@yiyangzheng</a> — they were among the top contenders on the LB, so their inference pipeline is trustworthy.</p>\n<blockquote>\n  <p><strong>Note on contamination.</strong> IMO-AnswerBench is itself drawn from previously published IMO and IMO-Shortlist problems (and other national olympiads), rewritten so that the answer is an integer in a fixed range. Both gpt-oss-120b and Cascade 2 were trained on data that almost certainly includes some of these problems in their original form, so contamination is a real concern. Cascade 2 in particular was released <em>after</em> IMO-AnswerBench was, which expands the contamination window. We did not test for contamination on our side; we chose to trust the data-curation procedure described in the Nemotron-Cascade 2 paper, which uses n-gram-based deduplication against known evaluation benchmarks. We flag this so the reader can weight the absolute accuracy numbers accordingly; the relative comparisons between solvers are less affected because both candidates draw from the same potentially-contaminated training pool.</p>\n</blockquote>\n<h2>Offline benchmark</h2>\n<blockquote>\n  <p>We published all reasoning traces collected for offline benchmark: <a href=\"https://www.kaggle.com/datasets/chankhavu/benchmark-traces\" target=\"_blank\">chankhavu/benchmark-traces</a></p>\n</blockquote>\n<p>All three candidates run through the same harness on the 63-problem evaluation set (8 rollouts per problem, tool use enabled, same prompt template): gpt-oss-120b, our FP8-quantized Cascade 2, and the <a href=\"https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16\" target=\"_blank\">Nemotron-3-Nano-30B-A3B</a>.</p>\n<h3>Accuracy and tool use</h3>\n<p>Metric definitions used throughout this section: <strong>Accuracy</strong> is avg@1 (probability a single rollout is correct, averaged over all 504 rollouts). <strong>Majority vote</strong> is maj@8 (the most-common of 8 parallel rollouts matches the expected answer). <strong>Pass@8</strong> is the probability that <em>at least one</em> of 8 rollouts is correct (the upper bound an oracle verifier could reach).</p>\n<table>\n<thead>\n<tr>\n<th>Model</th>\n<th>Accuracy</th>\n<th>Majority vote</th>\n<th>Pass@8</th>\n<th>No-answer</th>\n<th>Avg gen tok</th>\n<th>Avg tools/sess</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>gpt-oss-120b</td>\n<td>44.0% (222/504)</td>\n<td>52.4% (33/63)</td>\n<td><strong>77.8%</strong> (49/63)</td>\n<td>8.9% (45/504)</td>\n<td><strong>26k</strong></td>\n<td>20.7 (7% err)</td>\n</tr>\n<tr>\n<td>nano30b</td>\n<td>41.5% (209/504)</td>\n<td>52.4% (33/63)</td>\n<td>63.5% (40/63)</td>\n<td><strong>1.6%</strong> (8/504)</td>\n<td>47k</td>\n<td>14.2 (6% err)</td>\n</tr>\n<tr>\n<td>cascade2</td>\n<td><strong>47.0%</strong> (237/504)</td>\n<td><strong>55.6%</strong> (35/63)</td>\n<td>74.6% (47/63)</td>\n<td>2.2% (11/504)</td>\n<td>34k</td>\n<td><strong>11.5</strong> (7% err)</td>\n</tr>\n</tbody>\n</table>\n<p>Cascade 2 leads on both accuracy and voting with half the tool calls — more sample-efficient when it does reach for Python. gpt-oss-120b's high no-answer rate comes from hard problems where it runs out of context mid-reasoning.</p>\n<h3>Generation speed on 1× H100 SXM</h3>\n<table>\n<thead>\n<tr>\n<th></th>\n<th>1 sequence</th>\n<th>4 sequences</th>\n<th>8 sequences</th>\n<th>12 sequences</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>gpt-oss-120b, mxfp4, kv8</td>\n<td>175.4 tok/s</td>\n<td>506.4 tok/s</td>\n<td>810.9 tok/s</td>\n<td>842.8 tok/s</td>\n</tr>\n<tr>\n<td>Cascade 2, fp8, kv8</td>\n<td>301.6 tok/s</td>\n<td>859.0 tok/s</td>\n<td>1333.5 tok/s</td>\n<td>1487.8 tok/s</td>\n</tr>\n</tbody>\n</table>\n<p>We use the FP8 Cascade 2 variant because H100 supports FP8 natively. Cascade 2 is faster at every batch size, and the gap widens at higher concurrency: gpt-oss-120b's throughput plateaus by 8 sequences, while Cascade 2 keeps scaling.</p>\n<h3>Test-time scaling</h3>\n<p>Two views of how each model scales with more compute per problem. <strong>pass@k</strong> = prob. ≥1 of k rollouts is correct; <strong>maj@k</strong> = probability that the most-common answer across k rollouts is correct. Methodology (x-axis conversion, truncation rule, tokenizer choice) in <em>Appendix D</em>.</p>\n<p><strong>Time budget.</strong> Each dot is k ∈ {1, …, 8}; the x-axis converts k into wall-clock seconds per problem on 1× H100 SXM at 8 parallel sequences. Each model's dots are spaced differently because their per-rollout cost differs: Cascade 2 is the cheapest per rollout, so its curve packs the most to the left.</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2F0f085dcf830df3ae82dd1dcdd2c87eeb%2Ftest_time_scaling.png?generation=1779061888745661&amp;alt=media\" alt=\"Test-time scaling vs wall-clock seconds per problem at 8 parallel sequences. Each dot is k ∈ {1, …, 8}.\"></p>\n<p><strong>Within the tested budget, Cascade 2 wins both pass@k and maj@k</strong> — it's the more consistent choice for the actual competition setting. It leads on maj@k across the whole range, <em>and</em> it wins pass@k at every plotted budget because it's cheaper per rollout and gets to fit more rollouts in for any given wall-clock. gpt-oss-120b's pass@k curve, however, has a <em>higher upper ceiling</em> — its rollouts are more diverse (helps pass@k, hurts plurality voting), so given more attempts / a larger budget than this chart covers, an oracle verifier picking the right rollout would eventually prefer gpt-oss-120b. That higher pass@k ceiling is what makes gpt-oss-120b interesting <em>with</em> an external verifier (see <em>Self-verification experiments</em> below); within the time budget of this competition, Cascade 2 dominates on both axes.</p>\n<p>A practical note on that pass@k advantage: realizing it would require an external verifier picking from gpt-oss-120b's rollouts, and Kaggle's resource constraints (5 h on 1× H100 SXM) make that infeasible in either obvious form — we cannot fit a <em>separate</em> verifier model alongside gpt-oss-120b's ~120 GB MXFP4 weights on a single H100, and using gpt-oss-120b <em>as its own</em> verifier doubles the per-problem token bill on a budget that already only fits ~8 solution rollouts per problem. Cascade 2's smaller footprint and faster throughput leave headroom for the self-verification experiments below; gpt-oss-120b does not.</p>\n<p><strong>Reasoning-time budget.</strong> Left panel plots avg@1, right panel maj@8, both as a function of how long a single rollout is allowed to reason (wall-clock seconds on 1× H100 SXM at 8 parallel sequences). Each dot is the result of capping the stored rollouts at a different token length, retaining the last <code>\\boxed{}</code> within the cap as that rollout's answer. This is the \"one longer chain of thought\" axis, complementing the \"more parallel rollouts\" time-budget chart above.</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2Fc32a3b368450b06dd6948c37448d9330%2Ftest_time_scaling_length.png?generation=1779061923631429&amp;alt=media\" alt=\"Test-time scaling vs average effective reasoning time per rollout at 8 parallel sequences. Each dot is a truncation L ∈ {16k, 32k, 48k, 64k, 80k, 96k}.\"></p>\n<p>Cascade 2 dominates on both panels. It reaches its ceiling (47.0% avg@1 / 57.1% maj@8) at ~207 s per rollout, while gpt-oss-120b tops out at ~313 s with a lower ceiling (44.0% / 52.9%) — gpt-oss reasons compactly in tokens, but its slower throughput costs it ~50% more wall-clock time. nano30b traces Cascade 2's shape shifted right by ~70 s: same throughput, longer traces — exactly the gap soft-cloning was meant to close.</p>\n<p><em>How the x-axis is built.</em> For each truncation L ∈ {16k, 32k, 48k, 64k, 80k, 96k} we cut each rollout at L tokens, measure the <em>effective reasoning length</em> (the <code>\\boxed{}</code> position, or L if the model didn't stop), and convert tokens → seconds using the model's per-sequence throughput at 8 parallel sequences on 1× H100 SXM (101 tok/s/seq for gpt-oss-120b; 167 tok/s/seq for Cascade 2 and nano30b, which share a tokenizer and architecture).</p>\n<h3>Our baseline (and final LB) submission</h3>\n<blockquote>\n  <p>Kaggle notebook: <a href=\"https://www.kaggle.com/code/chankhavu/c2-distill-vllm018-watchdog\" target=\"_blank\">chankhavu/c2-distill-vllm018-watchdog</a>\n  Model: <a href=\"https://www.kaggle.com/models/chankhavu/c2-legaldistill-30b-a3b-fp8/transformers/fp8/1\" target=\"_blank\">chankhavu/c2-legaldistill-30b-a3b-fp8</a></p>\n</blockquote>\n<p>The solver we actually submitted is compact: no self-verification, no external tools beyond Python, no learned verifier. Five mechanisms wrapped around the FP8 soft-clone:</p>\n<ol>\n<li><strong>Adaptive per-problem budget.</strong> For each problem we set <code>budget = min(900, max(300, time_left − 300 × problems_left_others))</code>. Early problems get the full 900 s; as the wall clock runs down we reserve 300 s per remaining problem and squeeze the current one to the 300 s floor.</li>\n<li><strong>8 parallel rollouts.</strong> Each problem is attacked by 8 independent rollouts on a 16-worker thread pool (over-provisioned so that early-stop cancellations don't stall new launches). All 8 share the same prompt at temperature 1.0, top_p 0.95 — diversity comes from sampling alone.</li>\n<li><strong>Python tool loop.</strong> Inside each rollout, up to 128 turns of: stream a chat completion with native function-calling, execute any <code>stateful_python_code_exec</code> calls in a stateful IPython sandbox (30 s per call, 1000-char output cap, <code>math/numpy/sympy/itertools/collections/mpmath</code> preimported with <code>mpmath.mp.dps = 64</code>), append results as <code>tool</code> messages, continue. The rollout returns the integer inside the last <code>\\boxed{…}</code> when the model stops calling tools.</li>\n<li><strong>Early stop on 4-vote consensus.</strong> The moment any integer has been emitted by 4 rollouts, we set a stop event, cancel the remaining futures, and move on.</li>\n<li><strong>Entropy-weighted majority vote.</strong> Each rollout's weight is <code>1 / mean_token_entropy</code>, where the per-token entropy is computed from top-5 logprobs over the generation. The winning answer is the one with the largest sum of rollout weights.</li>\n</ol>\n<p>Unfortunately, we only finished our Eagle3 work on the last 2 days of the competition, so we never had a clean submission with only Eagle3, without other harnesses that degrades the results (like self-verification or increased number of parallel rollouts).</p>\n<h2>Online leaderboard simulation</h2>\n<blockquote>\n  <p>The analysis in this section is based on collected traces from simulated online LB runs (50 problems per run, 5h per run, 1x H100 SXM). The data is published here: <a href=\"https://www.kaggle.com/datasets/chankhavu/aimo50-evals/data\" target=\"_blank\">chankhavu/aimo50-evals</a>.</p>\n</blockquote>\n<p>To sanity-check how the offline numbers translate into the actual Kaggle setup, we replayed our final Cascade 2 voting submission and the gpt-oss-120b 44/50 public-notebook baseline on three independent shuffles of the same 50 problems, each under the real Kaggle budget: 5 hours wall-clock on 1× H100 SXM with an adaptive per-problem budget that starts at 900 s and can be squeezed down to 300 s as the wall clock runs low.</p>\n<p>These 50 problems are a subset of the 63 problems in our offline benchmark section. We removed problems with <code>pass@8 = 0</code> for all 3 models: gpt-oss-120b, Nemotron Cascade 2, and Nemotron 3 Nano (so, basically impossible removed problems that were never solved).</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2F0cc893180cba5bf84e4cf4ff7916fdbf%2Fkaggle_time_budget.png?generation=1776888430311498&amp;alt=media\" alt=\"Left: cumulative correct answers (out of 50) over wall-clock time, averaged across 3 shuffles (thin lines are the individual shuffles). Right: the per-problem budget the adaptive scheduler allocated to each problem as the run progressed — starts at 900 s and drops toward the 300 s floor when wall-clock time gets tight.\"></p>\n<p>Two readings. <strong>(i)</strong> Cascade 2 (mean <strong>34.7/50</strong>) is ahead of gpt-oss-120b (mean <strong>33.0/50</strong>) on the LB scoreboard from the ~2h mark onward, and reaches gpt-oss-120b's <em>final</em> score about 20 min earlier in the average run. <strong>(ii)</strong> gpt-oss-120b's per-problem budget gets visibly squeezed toward the end: on aimo50c the last 5 problems ran at 300 s (the minimum), because the notebook was within minutes of the 4.83h ceiling. Cascade 2 held 900 s per problem for most of the run across all three shuffles, with plenty of headroom at the finish.</p>\n<h3>Per-problem breakdown</h3>\n<p>Three per-problem views of the 150 (problem × shuffle) pairs:</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2F70f9e23ae071319d893dd81e92f91db5%2Fkaggle_per_problem_stats.png?generation=1779061973301798&amp;alt=media\" alt=\"Per-problem analysis. Left: correctness bucketed by the number of distinct answers the 8 rollouts produced. Middle: 2×2 agreement matrix between the two notebooks on matched problem-shuffle pairs. Right: total wall-clock time broken down into generation, successful tool call, Python-error tool call, and timed-out tool call (per-call sandbox timeout = 30 s in both solvers).\"></p>\n<p><strong>Left — vote convergence predicts correctness.</strong> When all 8 rollouts agree (bin 1), Cascade 2 is correct 82.8% of the time vs gpt-oss-120b's 74.6% — the online equivalent of the offline \"consistency → correctness\" story. Cascade 2 never produces a zero-answer case; gpt-oss-120b has 9 of 150 (all wrong). At 4+ distinct answers (both models stumped), Cascade 2 is slightly worse but has more such cases (30 vs 14), so the bin averages understate its overall advantage.</p>\n<p><strong>Middle — which model actually matters on 29% of pairs.</strong> 53% both right, 18% both wrong, 16% Cascade-2-only right, 13% gpt-oss-only right. Net +5 pair-wise advantage to Cascade 2. Roughly half the LB score is \"easy for everyone\" and one-fifth is \"hard for everyone\"; the remaining third is where model choice actually moves the needle.</p>\n<p><strong>Right — where the wall-clock time actually goes.</strong> Decomposing total wall time across all 3 shuffles into four buckets — generation, successful tool call, Python-error tool call, timed-out tool call — Cascade 2 spends <strong>88% in generation</strong> vs gpt-oss-120b's <strong>74%</strong>, and <strong>6.3% of Cascade 2's time vs 14.4% of gpt-oss-120b's is wasted on tool calls that produced no useful output</strong> (~2.2× more for gpt-oss-120b). For both solvers, <strong>the wasted time is almost entirely pure 30 s timeouts</strong> (12.5% / 5.9% of wall-clock) rather than Python errors (1.9% / 0.4%) — gpt-oss-120b's tool calls just trip the 30 s sandbox cap roughly 2× more often per rollout than Cascade 2's. <em>Appendix G</em> dives into the per-error-class breakdown and the most common root causes (forgotten imports, sandbox state rollback after a cell error).</p>\n<h3>Head-to-head: the two finalists</h3>\n<p>Cascade 2 voting vs the gpt-oss-120b baseline, log-derived:</p>\n<table>\n<thead>\n<tr>\n<th><strong>Submission</strong><br>(model, harness)</th>\n<th><strong>Local validation</strong><br>(3 shuffles)</th>\n<th><strong>Local val</strong><br>(mean)</th>\n<th><strong>Tool err</strong><br>(per-call)</th>\n<th><strong>Public LB attempts</strong></th>\n<th><strong>Private LB</strong></th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>gpt-oss-120b, 44/50 public notebook</td>\n<td>33, 30, 36</td>\n<td>33.0</td>\n<td>15.0%</td>\n<td>38, 38, 37</td>\n<td>—</td>\n</tr>\n<tr>\n<td><strong>Cascade 2, 8 rollouts, voting<br>(our final submission)</strong></td>\n<td><strong>36, 34, 34</strong></td>\n<td><strong>34.7</strong></td>\n<td><strong>10.6%</strong></td>\n<td><strong>36, 39, 38</strong></td>\n<td><strong>41</strong></td>\n</tr>\n</tbody>\n</table>\n<p>Per-call tool error rates count any call that produced no useful output — Python errors and 30 s sandbox timeouts together.</p>\n<p>The fact that the results are in similar ballpark as Public LB (30 - 36) is pure coincidence - we did not cherry-pick the problems for our benchmark in any way. It means that the Public LB are really on the same difficulty level as the IMO-AnswerBench, which is a testament of how far we've come since AIMO1!</p>\n<p>Cascade 2 wins local validation by 1.7 points and matches gpt-oss-120b on the LB mean (both 37.7), peaking higher (39 vs 38). Both finalists score meaningfully higher on the LB than on our local mean (gpt-oss +4.7, Cascade 2 voting +3.0), which suggests IMO-AnswerBench (our local set) is slightly harder than the hidden LB set. The self-verification harnesses we tried — folded into the full comparison table at the end of the <code>Self-verification experiments</code> section below — both underperformed plain voting.</p>\n<h2>Self-verification experiments</h2>\n<blockquote>\n  <ul>\n  <li>Notebook we submitted to Public LB: <a href=\"https://www.kaggle.com/code/chankhavu/c2-distill-verifier?scriptVersionId=311859342\" target=\"_blank\">https://www.kaggle.com/code/chankhavu/c2-distill-verifier?scriptVersionId=311859342</a></li>\n  <li><strong>Caveat up front.</strong> We built and evaluated the entire self-verification harness within the last 24 hours before the competition deadline — no time to tune any of the hyperparameters (pair-selection policy, grader prompt, max-verifications cap, temperature, rating scale, μ-score weighting, fallback thresholds). Treat the numbers below as <em>first-attempt</em> measurements, not a carefully-optimised upper bound on what self-verification can do.</li>\n  </ul>\n</blockquote>\n<p>The voting harness leaves an obvious source of slack: on most problems the 8 solution rollouts finish at wildly different times. The first rollout drops its <code>\\boxed{}</code> at 100–200 s, the slowest can take 500+ s, and in between we're sitting on GPU capacity we've already paid for. The self-verification variants from the table above use that capacity to run pairwise <em>grader</em> sessions that rate competing answers against each other.</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2Fd71a07030d85e1049dbf373179fd355d%2Fkaggle_self_verification_gantt.png?generation=1776888538729281&amp;alt=media\" alt=\"Left: Gantt of one problem (correctly answered 13). 8 solution rollouts finish between 75 s and 334 s at a 3-3-2 answer split — &lt;code&gt;1&lt;/code&gt; and &lt;code&gt;12&lt;/code&gt; tied at three votes each, with the correct answer &lt;code&gt;13&lt;/code&gt; as a 2-vote minority. Rows sorted fastest-first. Right: the pairwise tournament the grader ran (three matches, ratings out of 10), the resulting standings, and — for reference — what entropy-weighted voting alone would have picked. The matchup graph leaves &lt;code&gt;13&lt;/code&gt; as the only undefeated bucket; voting would have confidently picked &lt;code&gt;12&lt;/code&gt; (score 5.13 &gt; 4.71 &gt; 3.55), which is wrong.\">\n</p><p>We borrow the core idea — pairwise LLM-as-judge comparisons aggregated into an uncertainty-weighted score — from <a href=\"https://harmandotpy.github.io/v1-verification/\" target=\"_blank\">V1: Toward Verifiable Reasoning in LLMs</a> by Harman et al. Our pair-selection policy differs: V1 picks the next pair adaptively to maximise information gain, while we use a simpler greedy rule (newest answer vs current undefeated bucket). Our winner-selection scoring stays close to theirs.</p>\n<h3>Algorithm</h3>\n<p>On top of the voting harness (same model, same sampling, same adaptive budget, same early-stop at four agreeing rollouts), the self-verification variants add three conceptual pieces:</p>\n<ol>\n<li><strong>Pairwise grader.</strong> Given the problem and two candidate solutions A and B, the model (at temperature 0) is asked to produce an integer correctness rating in 1–10 for each side. Higher rating wins; equal ratings are a tie. (The full prompt is in Appendix E.)</li>\n<li><strong>Matchup tracker.</strong> Rollout answers are grouped into <em>buckets</em> by value (duplicate answers share a bucket). The tracker maintains a directed win/loss graph over buckets, not individual rollouts.</li>\n<li><strong>Winner = undefeated bucket.</strong> After all solutions and verifications complete, we pick the bucket that no other bucket beat, with tie-breaking described below.</li>\n</ol>\n<h3>Scheduling policy: filling GPU bubbles</h3>\n<p>Each time a solution rollout completes, the harness attempts to launch up to two verifications in whichever GPU slots are free, subject to a cap on concurrent rollouts. The tracker picks the next comparison in priority order:</p>\n<ol>\n<li>The newest answer against a currently undefeated bucket it hasn't yet faced (challenger vs champion) — this is the greedy rule we use in place of V1's information-gain-maximising pick.</li>\n<li>Two undefeated buckets against each other, to break ties at the top of the graph.</li>\n<li>Any pair of buckets that haven't yet been compared, as a fallback.</li>\n</ol>\n<p>Completed verifications also re-trigger a new comparison if there's still wall-clock budget and solution rollouts are still arriving. Verifications are capped at a fixed maximum per problem so they can't swallow the solver's budget on pathological cases.</p>\n<h3>Scoring and winner selection</h3>\n<p>Let <em>undefeated</em> be the set of buckets with zero losses in the win/loss graph. If exactly one bucket is undefeated, we pick it — done. If multiple remain undefeated (or none, in the cycle case), we rank them by an uncertainty-weighted score</p>\n<p>$$\\mu_i = \\frac{\\sum_j w_{ij} \\cdot v_{ij}}{\\sum_j w_{ij}}\\,,$$</p>\n<p>where $v_{ij} \\in {1, \\tfrac{1}{2}, 0}$ is the outcome (win / tie / loss) of bucket <em>i</em> against bucket <em>j</em>, and $w_{ij} = \\max(|r_i - r_j| / 9,\\, 0.1)$ scales each comparison by its rating margin: a 10–1 verdict carries an order of magnitude more weight than a 5–4 one, with a floor of 0.1 so ties still count. Further ties break by bucket size (number of rollouts that produced that answer), then by comparison count. If fewer than half the distinct answer buckets have been graded at all, we treat the grading as too sparse to trust and fall back to the entropy-weighted majority vote from the baseline voting solver.</p>\n<p>On the problem in the chart, voting has nothing useful to say: three rollouts say <strong>1</strong>, three say <strong>12</strong>, only two say <strong>13</strong> — the correct answer. Voting breaks the tie by entropy-weighted score, picks <code>12</code> (score 5.13 vs <code>1</code>'s 4.71 and <code>13</code>'s 3.55), and is wrong. The grader only needs three pairwise comparisons to settle it (13 beats 1 with 10–1, 13 beats 12 with 9–2, and 12 beats 1 with 3–1), leaving <code>13</code> as the only undefeated bucket. This is the regime self-verification is <em>supposed</em> to win: when a single well-reasoned rollout can recognise a majority of sloppy rollouts for what they are.</p>\n<h3>Results: why it didn't win</h3>\n<p>Despite the elegance, the table tells the disappointing story: self-verification underperformed voting on both local validation (32.0 / 33.0 vs 34.7) and public LB (34 vs 36/39/38). We tried two configurations — 8 solution rollouts with verification layered on top, and 6 solution rollouts with more verification budget per problem — and neither beat plain voting.</p>\n<p><strong>When does the grader even do something?</strong> Across all 297 problem-shuffles we ran under self-verification (three variants × two shuffles × ~50 problems), the grader agrees with entropy-weighted voting's top answer <strong>84.5 % of the time (251 / 297)</strong> — meaning on 5 out of every 6 problems the verification layer adds literally nothing. On the remaining 15.5 % (46 cases) the grader flips the decision, and its hit rate on those flips is what really matters:</p>\n<table>\n<thead>\n<tr>\n<th>Harness</th>\n<th>Flips</th>\n<th>Grader right<br>(voting wrong)</th>\n<th>Grader wrong<br>(voting right)</th>\n<th>Both wrong</th>\n<th>Net</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Cascade 2, 8 rollouts, self-verification</td>\n<td>11</td>\n<td>6</td>\n<td>3</td>\n<td>2</td>\n<td><strong>+3</strong></td>\n</tr>\n<tr>\n<td>Cascade 2, 6 rollouts, self-verification</td>\n<td>11</td>\n<td>6</td>\n<td>2</td>\n<td>3</td>\n<td><strong>+4</strong></td>\n</tr>\n<tr>\n<td>Cascade 2 + Eagle3, 6 rollouts</td>\n<td>24</td>\n<td>10</td>\n<td>10</td>\n<td>4</td>\n<td><strong>0</strong></td>\n</tr>\n<tr>\n<td><strong>All self-verif (aggregate)</strong></td>\n<td><strong>46</strong></td>\n<td><strong>22</strong></td>\n<td><strong>15</strong></td>\n<td><strong>9</strong></td>\n<td><strong>+7 / 297</strong></td>\n</tr>\n</tbody>\n</table>\n<p>So on the two non-Eagle3 runs the grader is net positive but only barely — +7 correct answers across 297 problem-shuffles, roughly 2.4 %. The Eagle3 run is the real tell: its 33 % throughput headroom lets the scheduler fire <em>more than twice as many</em> flip-candidate comparisons (24 vs 11), and the extra firings split right down the middle — ten wins cancelled by ten losses. More grading, no net gain. At the margins where voting and the grader disagree, the grader is essentially a coin flip.</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2Fc2f032a216884a70d93e74af479db525%2Fkaggle_self_verification_time_budget.png?generation=1776888592784811&amp;alt=media\" alt=\"Two views of the same 50-problem runs under the 5 h Kaggle-like setting, comparing voting (our final) against two self-verification harnesses — the 8-rollout variant and a 6-rollout variant run with Eagle3 (its numbers have been normalised onto the same 300–900 s / 4.83 h axes as everyone else). Left: per-problem wall-clock time — dots are means across shuffles, bars are ±1 SD. Right: the adaptive budget the scheduler handed each problem, capped between 900 s (default) and 300 s (floor); bold line is the mean across shuffles, faint lines are individual shuffles. Voting stays near the 900 s ceiling for most of the run. Both self-verification variants drop off the 900 s line within the first ten problems and spend most of the run in the 600–700 s band.\"></p>\n<p>Our read: the grader is too lenient to break ties confidently when both answers are backed by plausible-looking reasoning, and — as the chart above makes clear — at 1× H100 SXM there simply isn't enough wall-clock budget to run the 15–20 comparisons per problem the tracker would need to converge on harder problems. The per-problem means are close across all three runs (329 s voting vs 342 s self-verification vs 339 s with Eagle3), but the tails tell a very different story: voting's 90th-percentile problem consumes 472 s, self-verification's reaches 589 s, and with Eagle3 it stretches to 796 s — the longer the grader can keep running comparisons, the longer individual problems drag on, and the scheduler simply squeezes the remaining budget harder. Eagle3 buys roughly a 33 % throughput improvement (our motivation for training it in the first place), but even on that budget the extra complexity of the verification layer didn't translate into more correct answers — the Eagle3 run landed at <strong>33.5 / 50</strong> (mean of 36 and 31), which is no better than the plain self-verification variant and still behind voting's 34.7. The ingredients — fast model, honest grader — were right; the budget wasn't.</p>\n<table>\n<thead>\n<tr>\n<th><strong>Submission</strong><br>(model, harness)</th>\n<th><strong>Local validation</strong><br>(3 shuffles)</th>\n<th><strong>Local val</strong><br>(mean)</th>\n<th><strong>Tool err</strong><br>(per-call)</th>\n<th><strong>Public LB attempts</strong></th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>gpt-oss-120b, 44/50 public notebook</td>\n<td>33, 30, 36</td>\n<td>33.0</td>\n<td>15.0%</td>\n<td>38, 38, 37</td>\n</tr>\n<tr>\n<td>Cascade 2, 8 rollouts, self-verification</td>\n<td>33, 31</td>\n<td>32.0</td>\n<td>5.1%</td>\n<td>34</td>\n</tr>\n<tr>\n<td>Cascade 2, 6 rollouts, self-verification</td>\n<td>35, 31</td>\n<td>33.0</td>\n<td>5.2%</td>\n<td>never submitted</td>\n</tr>\n<tr>\n<td>Cascade 2 + Eagle3, 6 rollouts, self-verification</td>\n<td>36, 31</td>\n<td>33.5</td>\n<td>5.2%</td>\n<td>never submitted</td>\n</tr>\n<tr>\n<td><strong>Cascade 2, 8 rollouts, voting<br>(our final submission)</strong></td>\n<td><strong>36, 34, 34</strong></td>\n<td><strong>34.7</strong></td>\n<td><strong>6.0%</strong></td>\n<td><strong>36, 39, 38</strong></td>\n</tr>\n</tbody>\n</table>\n<hr>\n<h2>Conclusion</h2>\n<p><strong>The boring harness wins.</strong> Our final submission is the simplest thing that works: 8 rollouts of soft-cloned FP8 Cascade 2 with Python tool use, an adaptive 300–900 s per-problem budget, early-stop at 4 agreeing answers, and an entropy-weighted majority vote. On the 3-shuffle local validation (IMO-AnswersBench, 5 h, 1× H100 SXM) it scores <strong>34.7 / 50</strong>, ahead of the public gpt-oss-120b 44/50 notebook's <strong>33.0 / 50</strong>, and it ties gpt-oss-120b on the public LB (mean 37.7, peak <strong>39 / 50</strong> vs 38). Every more-sophisticated Cascade 2 harness we tried — pairwise-comparison self-verification, the same plus Eagle3-accelerated inference — underperformed the baseline. We believe self-verification could beat voting with more tuning, but not in the 24 hours we had before the deadline.</p>\n<p><strong>What the side quests produced.</strong> Three open-source artifacts that outlast the competition: a family of quantized Cascade 2 checkpoints (FP8, NVFP4, AWQ-INT4 — 66K downloads and counting), a vLLM fork that correctly runs prefix caching alongside speculative decoding on hybrid-Mamba models (long-standing open problem), and an EAGLE-3 speculator for Cascade 2 reaching 3.55 mean accepted tokens at K=7 along with the SpecForge modifications needed to train it at 64 k context. We also filed a note on Cascade 2's tendency to quietly search the web for answers through its Python tool.</p>\n<p><strong>Why this is mostly a first-try harness.</strong> We joined the competition late and the Kaggle 1-submission-per-day cap meant essentially every choice we shipped was a first-try call, with no headroom to A/B-test on the hidden LB. That's also why our scheduling, early-stop, and budget policy are deliberately simple: we picked Cascade 2 specifically because it offered the best <strong>maj@k under a time budget</strong>, and our priors from AIMO 2 were that winning solutions in this class of competition optimized maj@k rather than chasing pass@k at infinity — pass@∞ tends to be similar across competitive models, but maj@k under a real time budget is where the consistency premium pays off. A more elaborate per-problem scheduling system was on the wishlist (several contestants reported good gains from one), but the engineering dependency we did spend weeks on — PC + SD for hybrid-Mamba models in vLLM — was already over our time budget, and we judged that getting the simple harness right was a safer use of the remaining days than tuning a scheduler we couldn't validate without using up the submission slots.</p>\n<p><strong>What we'd do differently.</strong> Finalize PC + SD support in vLLM earlier (this was the engineering dependency that would have unlocked a genuinely competitive self-verification harness), start the self-verification work on day one instead of the last 24 hours, spend some time tuning the grader (prompt, rating scale, pair-selection policy), and develop a smarter per-problem scheduling policy that exploits the random hard/easy ordering. The ingredients were right; we just ran out of room to tune them. Even so, we're glad we got side-tracked — the open-source artifacts are the part of this project we're most proud of.</p>\n<hr>\n<h2>References</h2>\n<ul>\n<li>Zhuolin et al. <a href=\"https://arxiv.org/abs/2603.19220\" target=\"_blank\">\"Nemotron-Cascade 2: Post-Training LLMs with Cascade RL and Multi-Domain On-Policy Distillation\"</a> (2026).</li>\n<li>OpenAI. <a href=\"https://arxiv.org/abs/2508.10925\" target=\"_blank\">\"gpt-oss-120b &amp; gpt-oss-20b Model Card\"</a> (2025).</li>\n<li>Natapong Nitarach. <a href=\"https://arxiv.org/abs/2603.27844\" target=\"_blank\">\"Model Capability Dominates: Inference-Time Optimization Lessons from AIMO 3\"</a> (2026).</li>\n<li>Harman et al. <a href=\"https://arxiv.org/abs/2603.04304v1\" target=\"_blank\">\"V1: Unifying Generation and Self-Verification for Parallel Reasoners\"</a> (2026).</li>\n<li>Woosuk et al. <a href=\"https://arxiv.org/abs/2309.06180\" target=\"_blank\">\"Efficient Memory Management for Large Language Model Serving with PagedAttention\"</a>, in <em>SOSP '23: Proceedings of the 29th Symposium on Operating Systems Principles</em> (2023).</li>\n<li>Yuhui et al. <a href=\"https://arxiv.org/abs/2503.01840\" target=\"_blank\">\"EAGLE-3: Scaling up Inference Acceleration of Large Language Models via Training-Time Test\"</a>, in <em>Annual Conference on Neural Information Processing Systems</em> (2025).</li>\n</ul>\n<hr>\n<h1>Appendix</h1>\n<h2>Appendix A: soft-cloning the Cascade 2 model</h2>\n<h3>Question: Why is this legal? Can this be even considered distillation?</h3>\n<ul>\n<li>Semantically and mathematically this is a distillation: the \"students\" weights NEVER optimize directly towards \"teachers\" weights, only through observation of the inputs/outputs $X$ and $Y$ of functions $F_\\text{teacher}(X)$, where $F_\\text{teacher}$ happens to be a linear mapping.</li>\n<li>This is different from \"non-legal\" <a href=\"https://www.kaggle.com/competitions/ai-mathematical-olympiad-progress-prize-3/discussion/680682#3437318\" target=\"_blank\">approach with GPT-OSS-Puzzle-88B in this discussion</a>, because that approach is basically an interpolation $W_\\text{final} = a<em>W_\\text{gptoss88b} + b</em>W_\\text{finetuned}$, which still directly involves the weights of non-legal model. Here, none of the weights of teacher are used in runtime student model.</li>\n</ul>\n<h3>Implementation details</h3>\n<p>The recovery loop runs per tensor, one tensor type at a time. For each teacher tensor we (1) generate random probe inputs $X \\sim \\mathcal{N}(0, 1)$, (2) observe the teacher's output $Y = f_\\text{teacher}(X)$ — this is the only step that touches teacher values — and (3) solve for student parameters that minimise the mean-squared error $\\lVert f_\\text{student}(X) - Y \\rVert^2$. Linear layers $y = W x$ are recovered in closed form via the normal equations (it's already an obvious MSE minimiser and the loop would add ~100× runtime for no benefit). The other three tensor types — depthwise Conv1d kernels, 1D scalar vectors ($y = w \\odot x$; norms, SSM $A_{\\log}$, $d_t$, $D$), and additive biases ($y = x + b$) — all run an explicit SGD loop with a learning rate that cosine-decays to 0 over 500 steps. Max learning rates are exposed as CLI flags; the choices below come from stability arguments on the per-parameter Hessian.</p>\n<p>Each recovery loss is a convex quadratic, and SGD is stable exactly when $\\mathrm{lr} &lt; 2/\\lambda_{\\max}(H)$ with $H$ the per-parameter Hessian. For the $y = w \\odot X$ and $y = X + b$ cases the loss is separable across features, so with the normalisation we use (mean over samples, sum over features) every parameter sees the same scalar Hessian $H = 2\\,\\mathbb{E}[X^2] = 2$ for $X \\sim \\mathcal{N}(0, 1)$ — hence $\\mathrm{lr} = 0.5 \\approx 1/H$ lands each element at its optimum in $O(1)$ effective steps, independent of $N$ and of the magnitude of the target.</p>\n<p>The unfolded-window conv1d Hessian is not a scaled identity. Its $(k_1, k_2)$ entry is the sample autocorrelation of standard noise at lag $|k_1 - k_2|$: diagonal $\\approx T$, off-diagonals have $\\mathcal{O}(\\sqrt{T})$ noise fluctuations (plus small deterministic shifts near the zero-padded boundary of the window). Divided by $T$ in the loss, the spectrum concentrates around $2$ but spreads to roughly $[1.3, 3.5]$ at typical Mamba sizes ($T = K + 32 = 36$, $K = 4$). $\\mathrm{lr} = 0.5$ is right on the stability edge for $C \\lesssim 10^3$ and clearly past it by $C \\gtrsim 4 \\cdot 10^3$ (it diverges with extreme numeric blow-up). $\\mathrm{lr} = 0.3$ sits inside $2 / 3.5 \\approx 0.57$ with comfortable margin across every Cascade 2 size we ran.</p>\n<h2>Appendix B: quantization details</h2>\n<h3>Precision recipe — full layer breakdown</h3>\n<p>The per-component precisions behind the layout chart in the main section. Each row maps an architectural component to the precision it's kept at in the FP8 and NVFP4 checkpoints, following the NVIDIA Nano sensitivity recipe.</p>\n<table>\n<thead>\n<tr>\n<th>Component</th>\n<th>Precision (FP8)</th>\n<th>Precision (NVFP4)</th>\n<th>Comment</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>MoE expert GEMMs (routed + shared)</td>\n<td><strong>FP8</strong></td>\n<td><strong>NVFP4</strong></td>\n<td>All 23 MoE layers, 128 routed + 2 shared experts each</td>\n</tr>\n<tr>\n<td>Mamba GEMMs (non-adjacent)</td>\n<td><strong>FP8</strong></td>\n<td><strong>NVFP4</strong></td>\n<td>17 of 23 Mamba layers</td>\n</tr>\n<tr>\n<td>Attention layers (all 6)</td>\n<td><strong>BF16</strong></td>\n<td><strong>BF16</strong></td>\n<td>Most sensitive — kept BF16 per NVIDIA sensitivity analysis</td>\n</tr>\n<tr>\n<td>Mamba layers adjacent to attention (6)</td>\n<td><strong>BF16</strong></td>\n<td><strong>BF16</strong></td>\n<td>Layers {4, 11, 18, 25, 32, 41} — found sensitive in ablations</td>\n</tr>\n<tr>\n<td>Mamba 1D conv</td>\n<td><strong>BF16</strong></td>\n<td><strong>BF16</strong></td>\n<td>All layers</td>\n</tr>\n<tr>\n<td>Router gates</td>\n<td><strong>FP32</strong></td>\n<td><strong>FP32</strong></td>\n<td>Routing precision must not degrade</td>\n</tr>\n<tr>\n<td>Embeddings &amp; lm_head</td>\n<td><strong>BF16</strong></td>\n<td><strong>BF16</strong></td>\n<td>Not quantized</td>\n</tr>\n<tr>\n<td>KV cache</td>\n<td><strong>FP8</strong></td>\n<td><strong>FP8</strong></td>\n<td>All 6 attention layers</td>\n</tr>\n</tbody>\n</table>\n<h3>Evaluation setup</h3>\n<p>All benchmark runs use <a href=\"https://github.com/NVIDIA-NeMo/Evaluator\" target=\"_blank\">NVIDIA-NeMo/Evaluator</a> with the <a href=\"https://github.com/NVIDIA-NeMo/Evaluator/blob/main/packages/nemo-evaluator-launcher/examples/nemotron/nemotron-3-super/local_nemotron-3-super-120b-a12b_tools.yaml\" target=\"_blank\">Nemotron-3-Super-120B tool-use config</a>: 8 rollouts per problem, temperature 1.0, tool use enabled (sandbox code execution), <code>enable_thinking=true</code>. Same config across the BF16 reference and our FP8 / NVFP4 checkpoints so numbers are directly comparable.</p>\n<blockquote>\n  <p><strong>Critical serving flag.</strong> NemotronH's SSM state must stay in FP32 at inference. vLLM 0.19 reads this from the model's <code>config.json</code> by default (Cascade 2 ships the override), but anyone serving a variant whose config doesn't include it should pass <code>--mamba_ssm_cache_dtype float32</code> explicitly. Leaving the SSM state at BF16 costs <strong>~10% absolute</strong> on AIME-class benchmarks — silent precision regression, no crash. The same applies to SGLang (<code>--mamba-ssm-dtype float32</code>), which defaults to BF16.</p>\n</blockquote>\n<h3>Verification results</h3>\n<table>\n<thead>\n<tr>\n<th>Benchmark</th>\n<th><a href=\"https://huggingface.co/nvidia/Nemotron-Cascade-2-30B-A3B\" target=\"_blank\">Nemotron-Cascade-2-30B-A3B</a><br>(reproduced results)</th>\n<th>Our FP8 model</th>\n<th>Our NVFP4 model</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>AIME 2025 (avg@8)</td>\n<td>98.8</td>\n<td><strong>96.7</strong></td>\n<td><strong>97.9</strong></td>\n</tr>\n<tr>\n<td>AIME 2026 (avg@8)</td>\n<td>94.2</td>\n<td><strong>95.0</strong></td>\n<td><strong>92.1</strong></td>\n</tr>\n<tr>\n<td>HMMT Feb 2025 (avg@8)</td>\n<td>92.9</td>\n<td><strong>93.8</strong></td>\n<td><strong>90.1</strong></td>\n</tr>\n</tbody>\n</table>\n<p><em>With the low sample count (8 rollouts per problem), a deviation of ±2% across runs is expected. Safe to say BF16, FP8, and NVFP4 variants are equivalent in reasoning performance.</em></p>\n<h2>Appendix C: Eagle3 training details</h2>\n<h3>Per-position acceptance</h3>\n<p>At the sweet spot (K=4, T=0.7 on AIME-25):</p>\n<table>\n<thead>\n<tr>\n<th>Position</th>\n<th>1</th>\n<th>2</th>\n<th>3</th>\n<th>4</th>\n<th>5</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Accept rate</td>\n<td>73.3%</td>\n<td>49.8%</td>\n<td>34.5%</td>\n<td>24.7%</td>\n<td>17.3%</td>\n</tr>\n</tbody>\n</table>\n<p>Rates decay geometrically with a ratio of ~0.7 per position — each additional draft token needs all previous drafts to verify first, and the marginal drafter confidence drops with depth.</p>\n<h3>Memory optimizations</h3>\n<p>Naïve training at L=65k, ttt=6 peaks at 120+ GiB — OOM on a 96 GB GPU. Four independent activation-memory cuts on the draft forward + TTT unroll bring peak down to ~88–91 GiB, approximately <code>63 GiB verifier weights + 4.2 GiB target_p_padded + 12 GiB attention activations + 4.2 GiB MLP saves + ~5–8 GiB overhead</code>:</p>\n<ul>\n<li><strong>Free verifier full-vocab logits before TTT unrolling</strong> (biggest single win, ~30 GiB saved across the unroll). The HF target backend returned the verifier's <code>[seq, 131072]</code> logits as <code>target</code> (~16 GiB at L=65k), held through three independent references across the draft forward + backward. Precomputing a small draft-vocab <code>target_p_padded</code> (~4 GiB) and dropping the rest before <code>eagle3_model.forward()</code> releases the full-vocab tensor before the expensive phase. Dropping the <code>.float()</code> upcast on the softmax saves another ~8 GiB.</li>\n<li><strong>Chunked fused linear + soft-target CE</strong> for the draft lm_head (~24 GiB saved across ttt=6 unrolls). The <code>[B, T, V]</code> logits tensor is never materialized — the existing <code>LogSoftmaxLoss</code> Triton kernel runs inside a chunked + grad-checkpointed wrapper. Numerically equivalent to the unchunked path on both <code>hidden_states</code> and <code>lm_head_weight</code> grads, verified at realistic shapes.</li>\n<li><strong>Chunked MLP forward</strong> (Liger-style, sequence-dim chunking; ~3.6 GiB per-step transient reduction). MLP intermediates shrink from ~4.4 GiB to ~0.8 GiB at L=65k, intermediate=8064. Composes cleanly with grad-checkpointing.</li>\n<li><strong>Sliding-window attention on the draft</strong> (modest direct memory win, but unblocks ttt=6 at L=65k by bounding attention from O(L²) to O(L × window)), plumbed through <code>LlamaForCausalLMEagle3</code>, the flex_attention mask generator, and the causal-mask builder. The TTT suffix block is intentionally not windowed.</li>\n</ul>\n<p>The four savings don't sum linearly — some overlap (e.g., freeing target logits shrinks the memory available for future cuts to operate on). Aggregate per-step wall time is ~4 s (1.2 s on short samples up to 8.6 s on near-max-length conversations at L=65k); grad-checkpointed MLP costs ~8–12% per-step recompute overhead, which the memory savings more than pay for.</p>\n<h3>Loss-mask parser (100× speedup)</h3>\n<p><code>GeneralParser.parse()</code> was running per-turn re-tokenization in O(num_turns × seq_len) — for long reasoning traces with many assistant turns this took several seconds per conversation. We replaced it with a single <code>return_offsets_mapping</code> + bisect pass to map character spans to token spans in O(num_turns × log seq_len). Per-conversation preprocessing dropped from several seconds to ~30 ms on long reasoning traces.</p>\n<h2>Appendix D: test-time scaling methodology</h2>\n<p><strong>Attempt-count view.</strong> What the time-budget chart in the body would look like if we drop the per-rollout-cost scaling and plot against raw <code>k</code>. Same data, x-axis = k instead of k × seconds-per-rollout.</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2F02418e1ddd660c78b71374af498f4ef3%2Ftest_time_scaling_attempts.png?generation=1779062011546385&amp;alt=media\" alt=\"pass@k and maj@k vs number of attempts k ∈ {1, …, 8} on the 63-problem offline benchmark.\"></p>\n<p><strong>Time-budget chart.</strong> For each model m and k ∈ {1, …, 8}, we plot the (pass@k, maj@k) point at <code>x = k × avg_tokens_per_rollout(m) / tok-per-sec(m)</code> seconds, using the 8-parallel-sequence throughput from the generation-speed table. pass@k and maj@k are computed exactly (no Monte-Carlo) by enumerating all <code>C(8, k)</code> subsets of the 8 available sessions per problem and averaging across the 63 problems. maj@k uses the most-common answer rule: for each subset, the modal answer wins, ties split uniformly across tied answers.</p>\n<p><strong>Reasoning-time budget chart.</strong> For each truncation length L ∈ {16k, 32k, 48k, 64k, 80k, 96k}:</p>\n<ul>\n<li>Truncate each session's model-generated text at L tokens.</li>\n<li>Find the last <code>\\boxed{…}</code> inside the truncated portion. If found, that token position is the session's <em>effective length</em> and its original predicted answer counts. If not found, the session is treated as \"no answer\" at that L and its effective length is L.</li>\n<li>Mean effective length across the 504 sessions gives the raw token x-coordinate, which we convert to seconds by dividing by the per-sequence throughput at 8 parallel sequences (101.4 tok/s/seq for gpt-oss-120b from 810.9/8; 166.7 tok/s/seq for Cascade 2 and nano30b from 1333.5/8 — nano30b uses Cascade 2's throughput since the architectures are identical). y is avg@1 (fraction of 504 sessions correct after truncation) or maj@8 (most-common-answer rule over the 8 truncated answers per problem, averaged across 63 problems).</li>\n</ul>\n<p><strong>Tokenization.</strong> Per-model, exact:</p>\n<ul>\n<li>gpt-oss-120b: <code>num_completion_tokens</code> from each trace record (the model's own Harmony token count for generated output, exclusive of prompt and tool results). Because gpt-oss emits <code>\\boxed{…}</code> as its final generation, we approximate its <code>\\boxed{}</code>-end token position as the total generated-token count.</li>\n<li>Cascade 2 / nano30b: we tokenize the <code>generation</code> text with the Nemotron-Cascade-2 tokenizer (<code>nvidia/Nemotron-Cascade-2-30B-A3B</code>) and compute both the total and the <code>\\boxed{}</code>-end positions exactly by running the tokenizer on the full text and on the prefix up to the closing brace of the last <code>\\boxed{…}</code>.</li>\n</ul>\n<h2>Appendix E: self-verification grader prompt and hyperparameters</h2>\n<p>Each pairwise verification is its own chat session with Python tool use enabled. System message:</p>\n<pre><code>You are a helpful assistant and an expert math contest grader.\n</code></pre>\n<p>User message (fields <code>{problem}</code>, <code>{sol_A}</code>, <code>{sol_B}</code> substituted from the current problem and two competing solution texts):</p>\n<pre><code>Compare two submissions and rate correctness based solely on the final answer.\n\n**Evaluation Guidelines:**\n- Extract each submission's final answer. Use any provided reasoning only\n  to help you assess whether the stated final answer is trustworthy. Do\n  not award credit for method quality or rigor.\n- Carefully analyze the problem statement and the submissions to assess\n  whether each final answer is correct. Grade only the final answer.\n**Problem**\n{problem}\n**Solution A**\n{sol_A}\n**Solution B**\n{sol_B}\n**Output Format:**\nFirst, provide your reasoning (what checks you performed). Then, on\nseparate new lines, give ratings using the EXACT tags below. Add no\nother text after the tags.\n&lt;rating_A&gt;INTEGER_1_TO_10&lt;/rating_A&gt;\n&lt;rating_B&gt;INTEGER_1_TO_10&lt;/rating_B&gt;\n**Rating Rules:**\n- Rate correctness on a 1-10 scale (10 = certainly correct, 8 = very\n  likely correct, 5 = uncertain/borderline, 3 = likely incorrect, 1 =\n  certainly incorrect).\n- Higher rating wins. Equal ratings imply a tie.\nPlease provide your analysis now.\n</code></pre>\n<p>We parse the rating pair out of the model's final message with an XML-tag regex first, then fall back to <code>\\boxed{N}</code> per side if the model used its trained <code>\\boxed{}</code> format instead of the requested tags.</p>\n<h2>Appendix F: final LB submission — configuration</h2>\n<p>All the knobs we actually shipped for <a href=\"https://www.kaggle.com/code/chankhavu/c2-distill-vllm018-watchdog\" target=\"_blank\">chankhavu/c2-distill-vllm018-watchdog</a>.</p>\n<p><strong>Model.</strong> <code>chankhavu/c2-legaldistill-30b-a3b-fp8</code> — the FP8-quantized soft-clone of Nemotron-Cascade-2, re-quantized specifically for the Kaggle submission (not our published open-source FP8 checkpoint; see the \"Quantizing the Cascade 2 model\" section). Runtime: vLLM 0.18.0 on 1× H100 SXM under the Kaggle inference server sandbox.</p>\n<p><strong>vLLM serve command.</strong> Single GPU, FP8 KV cache, large context window, chunked prefill, prefix caching, async scheduling, native function-calling enabled with the <code>qwen3_coder</code> tool-call parser:</p>\n<pre><code>vllm serve /path/to/c2-legaldistill-30b-a3b-fp8 \\\n    --served-model-name chankhavu/c2-legaldistill-30b-a3b-fp8 \\\n    --tensor-parallel-size 1 \\\n    --max-num-seqs 64 \\\n    --gpu-memory-utilization 0.9 \\\n    --dtype auto \\\n    --max-model-len 262144 \\\n    --max-num-batched-tokens 8192 \\\n    --kv-cache-dtype fp8 \\\n    --mamba-ssm-cache-dtype float32 \\\n    --mamba-cache-mode all \\\n    --mamba-block-size 512 \\\n    --enable-chunked-prefill \\\n    --enable-prefix-caching \\\n    --async-scheduling \\\n    --enable-auto-tool-choice \\\n    --tool-call-parser qwen3_coder \\\n    --trust-remote-code \\\n    --host 0.0.0.0 --port 8000\n</code></pre>\n<p>We also run a lightweight watchdog subprocess that polls <code>/proc</code> every 30 s and relaunches vLLM if the APIServer / EngineCore processes disappear (scrapes the <code>VLLM::APIServer</code> / <code>VLLM::EngineCor...</code> <code>comm</code> names — the literal <code>vllm.entrypoints</code> string stops appearing in <code>cmdline</code> after vLLM calls <code>setproctitle</code> during startup). The relaunch itself takes ~100 s, which is why the watchdog restart trigger is \"no live vllm process found\" rather than HTTP-based.</p>\n<p><strong>Solver configuration</strong> (from <code>CFG</code>):</p>\n<table>\n<thead>\n<tr>\n<th>Setting</th>\n<th>Value</th>\n<th>Notes</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>attempts</code></td>\n<td>8</td>\n<td>Solution rollouts per problem</td>\n</tr>\n<tr>\n<td><code>workers</code></td>\n<td>16</td>\n<td>Thread pool size (2× <code>attempts</code>, so cancellations don't stall new launches)</td>\n</tr>\n<tr>\n<td><code>early_stop</code></td>\n<td>4</td>\n<td>Stop once 4 rollouts agree on an integer answer</td>\n</tr>\n<tr>\n<td><code>turns</code></td>\n<td>128</td>\n<td>Max conversation turns per rollout</td>\n</tr>\n<tr>\n<td><code>temperature</code></td>\n<td>1.0</td>\n<td>Sampling diversity — all rollouts share the prompt</td>\n</tr>\n<tr>\n<td><code>top_p</code></td>\n<td>0.95</td>\n<td></td>\n</tr>\n<tr>\n<td><code>top_logprobs</code></td>\n<td>5</td>\n<td>Top-5 logprobs for entropy computation</td>\n</tr>\n<tr>\n<td><code>enable_thinking</code></td>\n<td><code>True</code></td>\n<td>Cascade 2's <code>&lt;think&gt;…&lt;/think&gt;</code> reasoning</td>\n</tr>\n<tr>\n<td><code>context_tokens</code></td>\n<td>262144</td>\n<td>vLLM <code>--max-model-len</code></td>\n</tr>\n<tr>\n<td><code>max_completion_tokens</code></td>\n<td>131072</td>\n<td>Per-turn completion cap</td>\n</tr>\n<tr>\n<td><code>notebook_limit</code></td>\n<td>17400 s (4.83 h)</td>\n<td>Kaggle inference ceiling</td>\n</tr>\n<tr>\n<td><code>high_problem_timeout</code></td>\n<td>900 s</td>\n<td>Default per-problem budget</td>\n</tr>\n<tr>\n<td><code>base_problem_timeout</code></td>\n<td>300 s</td>\n<td>Reserved per remaining problem; also the per-problem floor</td>\n</tr>\n<tr>\n<td><code>python_timeout</code></td>\n<td>30 s</td>\n<td>Per Python tool call</td>\n</tr>\n<tr>\n<td><code>max_output_characters</code></td>\n<td>1000</td>\n<td>Sandbox stdout truncation</td>\n</tr>\n<tr>\n<td><code>seed</code></td>\n<td>42</td>\n<td>Unused (vLLM picks fresh seeds per request — we <em>want</em> the cross-worker diversity)</td>\n</tr>\n</tbody>\n</table>\n<p><strong>Adaptive budget formula.</strong> For each problem we compute\n<code>budget = min(high_problem_timeout, max(base_problem_timeout, time_left − base_problem_timeout × problems_left_others))</code>, so early problems get the full 900 s; as the wall clock runs down we reserve 300 s per remaining problem and squeeze the current one toward the 300 s floor.</p>\n<p><strong>System prompt.</strong></p>\n<pre><code>You are a helpful assistant and a mathematical problem solver.\nSolve the given problem step by step using rigorous reasoning.\n</code></pre>\n<p><strong>User template</strong> (<code>{problem}</code> substituted per problem):</p>\n<pre><code>{problem}\n\nThe answer is a non-negative integer between 0 and 99999, inclusive.\nPut your final answer in \\boxed{}.\n</code></pre>\n<p><strong>Tool definition</strong> — the model is given exactly one function, a stateful IPython code executor:</p>\n<pre><code>{\n  \"type\": \"function\",\n  \"function\": {\n    \"name\": \"stateful_python_code_exec\",\n    \"description\": \"Call this function to execute Python code in a stateful Jupyter notebook environment. Python will respond with the output of the execution or time out after 120.0 seconds. The environment has math, numpy, sympy, itertools, collections, and mpmath pre-imported.\",\n    \"parameters\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"code\": {\"type\": \"string\", \"description\": \"Code to execute\"}\n      },\n      \"required\": [\"code\"]\n    }\n  }\n}\n</code></pre>\n<p>Per-call timeout is actually 30 s (from <code>CFG.python_timeout</code>), not 120 s — the description is the one the Cascade 2 model was trained to expect, so we leave it unchanged.</p>\n<p><strong>Sandbox preimports.</strong> Every new session starts with:</p>\n<pre><code>import math\nimport numpy\nimport sympy\nimport itertools\nimport collections\nimport mpmath\nmpmath.mp.dps = 64\n</code></pre>\n<p><strong>Answer extraction.</strong> A small regex cascade applied to each rollout's final non-tool-call message, in order: (i) <code>\\boxed{DIGITS}</code> — pick the last one; (ii) <code>\\boxed{… = DIGITS}</code> — pick the last integer inside the last <code>\\boxed{}</code>; (iii) <code>final answer is DIGITS</code>. Accepts integers in <code>[0, 99999]</code> only. Before scanning, <code>&lt;think&gt;…&lt;/think&gt;</code> reasoning blocks are stripped.</p>\n<p><strong>Voting.</strong> Each rollout's weight is <code>1 / max(mean_token_entropy, 1e-9)</code>, where the per-token entropy is computed from the top-5 logprobs of the generated tokens (not tool-call bytes). The picked answer is the one with the highest sum of weights across the rollouts that produced it.</p>\n<p><strong>Hyperparameters.</strong> Grader runs at temperature 0, top_p 0.95, same Python sandbox as the solver. Tool-call budget per grader session: 15 calls; past that we force-respond to any pending tool calls with a stub and instruct the model to output its ratings immediately. Maximum verifications per problem: 16. Maximum concurrent rollouts (solution + verification combined) in the thread pool: 8.</p>\n<h2>Appendix G: tool-call error breakdown (gpt-oss-120b vs Cascade 2)</h2>\n<p>Why does gpt-oss-120b's tool-call overhead come out roughly 2× Cascade 2's? The whole question is really about <strong>time</strong>: a typical gpt-oss-120b rollout burns <strong>37 s of wall-clock on errored or timed-out tool calls — 14.0% of its 266 s average rollout — vs 17 s for Cascade 2 (6.3% of its 272 s rollout)</strong>. ~2.2× the absolute, ~2.2× the rollout fraction. All numbers below are from the same LB-simulation runs that feed the chart in §Per-problem breakdown (aimo50a/b/c, 3 shuffles × 50 problems × up to 8 rollouts per problem, 1× H100 SXM under the 5 h Kaggle budget).</p>\n<p>Per-rollout averages — every column below is a mean across rollouts (912 for gpt-oss-120b, 850 for Cascade 2; &lt; 50×3×8 = 1200 because the adaptive scheduler cancels in-flight rollouts once 4 agree on an answer). In the table, \"error\" means <em>any</em> failed tool call — Python exceptions and the 30 s sandbox timeouts together.</p>\n<table>\n<thead>\n<tr>\n<th>Model</th>\n<th>rollout sec</th>\n<th>tool calls</th>\n<th>tool sec</th>\n<th><strong>error sec</strong></th>\n<th><strong>% rollout errored</strong></th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>gpt-oss-120b</td>\n<td>266</td>\n<td>18.4</td>\n<td>66.3</td>\n<td><strong>37.3</strong></td>\n<td><strong>14.0%</strong></td>\n</tr>\n<tr>\n<td>Cascade 2</td>\n<td>272</td>\n<td>14.3</td>\n<td>32.4</td>\n<td><strong>17.1</strong></td>\n<td><strong>6.3%</strong></td>\n</tr>\n</tbody>\n</table>\n<p>Within each model, where the error+timeout time goes (<code>% errs</code> and <code>% err-time</code> are within-model fractions of the <em>error+timeout</em> totals):</p>\n<p>All low-frequency classes (&lt; 0.1 occurrences per rollout in both models) are collapsed into \"other\" — this includes RuntimeError, ValueError, KeyError, IndexError, ImportError, and uncategorized Python tracebacks.</p>\n<table>\n<thead>\n<tr>\n<th>Class</th>\n<th>gpt-oss-120b<br>count/rollout</th>\n<th>sec/rollout</th>\n<th>% errs</th>\n<th>% err-time</th>\n<th>Cascade 2<br>count/rollout</th>\n<th>sec/rollout</th>\n<th>% errs</th>\n<th>% err-time</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>[TIMEOUT]</strong> (30 s cap)</td>\n<td><strong>1.01</strong></td>\n<td><strong>33.1</strong></td>\n<td>36.4%</td>\n<td><strong>88.6%</strong></td>\n<td>0.51</td>\n<td>16.0</td>\n<td>33.1%</td>\n<td><strong>94.0%</strong></td>\n</tr>\n<tr>\n<td><strong>SyntaxError</strong></td>\n<td><strong>0.72</strong></td>\n<td>0.3</td>\n<td>26.1%</td>\n<td>0.9%</td>\n<td>0.11</td>\n<td>0.0</td>\n<td>7.3%</td>\n<td>0.1%</td>\n</tr>\n<tr>\n<td><strong>NameError</strong></td>\n<td>0.49</td>\n<td>2.3</td>\n<td>17.8%</td>\n<td>6.0%</td>\n<td>0.15</td>\n<td>0.1</td>\n<td>9.7%</td>\n<td>0.7%</td>\n</tr>\n<tr>\n<td>TypeError</td>\n<td>0.17</td>\n<td>0.1</td>\n<td>6.1%</td>\n<td>0.3%</td>\n<td>0.23</td>\n<td>0.0</td>\n<td>15.1%</td>\n<td>0.2%</td>\n</tr>\n<tr>\n<td>AttributeError</td>\n<td>0.10</td>\n<td>0.0</td>\n<td>3.6%</td>\n<td>0.1%</td>\n<td>0.10</td>\n<td>0.1</td>\n<td>6.5%</td>\n<td>0.4%</td>\n</tr>\n<tr>\n<td>other</td>\n<td>0.28</td>\n<td>1.5</td>\n<td>10.0%</td>\n<td>4.1%</td>\n<td>0.41</td>\n<td>0.7</td>\n<td>28.3%</td>\n<td>4.6%</td>\n</tr>\n<tr>\n<td><strong>All errored + timed-out</strong></td>\n<td><strong>2.77</strong></td>\n<td><strong>37.3</strong></td>\n<td>100%</td>\n<td>100%</td>\n<td><strong>1.53</strong></td>\n<td><strong>17.1</strong></td>\n<td>100%</td>\n<td>100%</td>\n</tr>\n</tbody>\n</table>\n<p><strong>Timeouts are ~90% of the wasted wall-clock for both solvers</strong> (88.6% / 94.0%) — every Python error class is a rounding error by time.</p>\n<p><strong>NameError carries the strongest cross-model signal</strong> (0.49/rollout for gpt-oss-120b vs 0.15 for Cascade 2). gpt-oss-120b's 451 extractable NameErrors split into two causes:</p>\n<ul>\n<li>~80 cases — <strong>forgot to import.</strong> gpt-oss-120b writes Jupyter-cell-style snippets that assume <code>sp</code> (16), <code>random</code> (15), <code>deque</code> (9), <code>math</code> (5), and other stdlib aliases are already live, but the pre-import block (<code>math, numpy, sympy, itertools, collections, mpmath</code>) doesn't expose them and the model rarely writes the <code>import</code> line itself.</li>\n<li>The remaining ~370 — <strong>sandbox rollback.</strong> Standard Jupyter/IPython preserves partial assignments after an exception; ours snapshots state <em>before</em> each cell and restores it on error, so variables defined earlier in a failed cell are wiped. Models trained on REPL semantics write follow-up code referencing those now-gone names. Cascade 2's 126 NameErrors are mostly the same rollback pattern with user-defined helpers — just hit less often.</li>\n</ul>",
  "comments": [
    {
      "id": 3458341,
      "author_name": "Sam Bealing",
      "author_url": "",
      "post_date": "2026-05-15T16:43:35.753000",
      "content": "<p>Following up on the questions from <a href=\"https://www.kaggle.com/philipvonderlind\" target=\"_blank\">@philipvonderlind</a>, here are some additional questions to help us assess your submission:</p>\n<ol>\n<li><strong>AnswerBench contamination concern:</strong> Cascade 2 was released in March 2026, a few months after IMO-AnswerBench was published, whereas gpt-oss-120b was released earlier. Are you worried that Cascade 2's higher score on your local validation set (drawn from AnswerBench) might partly reflect AnswerBench problems having been in Cascade 2's training data, rather than a fair reasoning comparison against gpt-oss-120b?</li>\n<li><strong>Why Cascade 2 over a gpt-oss-120b verifier path:</strong> Your offline numbers show gpt-oss-120b has the higher pass@8 (77.8% vs Cascade 2's 74.6%), and you note an oracle verifier would pick gpt-oss-120b. You then invested heavily in the soft-clone + quantization + speculator pipeline for Cascade 2. With hindsight, what made you commit to the Cascade 2 route rather than building (e.g. a learned) verifier for gpt-oss-120b that could close the maj@8 → pass@8 gap?</li>\n</ol>\n<p>Look forward to reading your responses on these.</p>",
      "votes": 1,
      "replies": [
        {
          "id": 3459537,
          "author_name": "Chan Kha Vu",
          "author_url": "",
          "post_date": "2026-05-17T23:58:58.033000",
          "content": "<p>Thanks a lot, Sam Bealing (@sambealing) for thoughtful questions! We had similar concerns during AIMO3. In fact, we were skeptical about Cascade 2 at first and were not sure if it was benchmaxxed. Here is why we ultimately chose to trust it.</p>\n<h2>AnswerBench contamination</h2>\n<blockquote>\n  <p>and also AnswerBench contamination concern: Cascade 2 was released in March 2026, a few months after IMO-AnswerBench was published, whereas gpt-oss-120b was released earlier. Are you worried that Cascade 2's higher score on your local validation set (drawn from AnswerBench) might partly reflect AnswerBench problems having been in Cascade 2's training data, rather than a fair reasoning comparison against gpt-oss-120b?</p>\n</blockquote>\n<p>This is a legitimate concern and we've added a note to <em>§Evaluation set</em> acknowledging it explicitly. Short version:</p>\n<ul>\n<li><strong>Both models are at some risk.</strong> IMO-AnswerBench is a rewriting of previously-published IMO problems, Shortlists, and national olympiads. Both gpt-oss-120b and Cascade 2 were trained on data that almost certainly includes some of these problems in their original form. So contamination is <em>the baseline</em>, not a one-sided concern.</li>\n<li><strong>Cascade 2 is more exposed than gpt-oss-120b.</strong> Cascade 2 was released after AnswerBench was published, so the contamination window for Cascade 2 is wider. The reasoning advantage we measure should be discounted accordingly when comparing absolute accuracy.</li>\n<li><strong>We did not run our own contamination checks.</strong> We chose to trust the data-curation procedure described in the Nemotron-Cascade-2 paper, which uses n-gram deduplication against known evaluation benchmarks. If that procedure works as documented, the contamination on AnswerBench specifically should be limited — but we can't verify this independently.</li>\n<li><strong>What this doesn't undermine:</strong> the <em>relative</em> observations that hold up regardless of contamination. The per-rollout cost difference, the maj@k stability difference, the tool-call error breakdown, the time-budget scaling shape — none of these depend on absolute reasoning ability. If Cascade 2's true reasoning capability is lower than its AnswerBench number suggests, our conclusions about \"Cascade 2 wins maj@k under time budget <em>because</em> it's cheaper per rollout and more consistent across rollouts\" still hold; we'd just be over-reporting the absolute scoreboard.</li>\n</ul>\n<p>The contamination caveat is now in the writeup body so the reader can weight the absolute numbers themselves.</p>\n<h2>Why Cascade 2 over a gpt-oss-120b verifier path</h2>\n<blockquote>\n  <p><strong>Why Cascade 2 over a gpt-oss-120b verifier path:</strong> Your offline numbers show gpt-oss-120b has the higher pass@8 (77.8% vs Cascade 2's 74.6%), and you note an oracle verifier would pick gpt-oss-120b. You then invested heavily in the soft-clone + quantization + speculator pipeline for Cascade 2. With hindsight, what made you commit to the Cascade 2 route rather than building (e.g. a learned) verifier for gpt-oss-120b that could close the maj@8 → pass@8 gap?</p>\n</blockquote>\n<p>A few interacting reasons, in roughly the order they mattered:</p>\n<ol>\n<li><strong>The verifier route doesn't fit on the H100.</strong> gpt-oss-120b's MXFP4 weights are ~120 GB; that leaves ~60 GB of HBM for KV cache, activations, and the 8 parallel solution rollouts we already need to fit. There is no headroom on a single H100 SXM for a <em>separate</em> verifier model running alongside it. Using gpt-oss-120b as its own verifier doubles the per-problem token bill on a budget that, at our throughput, already only fits ~8 solution rollouts. Both forms of \"gpt-oss-120b + verifier\" run out of either memory or wall-clock before they can be meaningfully tuned.</li>\n<li><strong>Cascade 2 leaves room for verification later.</strong> Cascade 2's smaller footprint and ~70% higher throughput meant we could <em>start</em> with plain voting and then layer self-verification in the GPU bubbles between fast and slow solution rollouts. The self-verification harness we shipped (untuned, last 24 h) underperformed voting, but the option existed at all because of Cascade 2's overhead. With gpt-oss-120b we wouldn't have had that fallback path even in principle.</li>\n<li><strong>Training a learned verifier is its own project.</strong> \"Build a learned verifier for gpt-oss-120b that closes maj@8 → pass@8\" is a meaningfully larger undertaking than the soft-clone we shipped: it needs verifier-training data (problem + multiple solutions + correctness labels), a training run, an eval harness for the verifier itself, and integration into the inference path.</li>\n<li><strong>AIMO 2 priors.</strong> The historical evidence from AIMO 2 was that winning solutions optimized maj@k under a real time budget, not pass@∞. Cascade 2 <em>measurably</em> wins maj@k under our time budget; gpt-oss-120b would have needed a working verifier to compete on the same axis. Investing in the model that already wins the relevant metric was the lower-variance bet.</li>\n</ol>\n<p>The resource-constraint version of this argument is now in the writeup as a follow-up paragraph after the oracle-verifier note in §Test-time scaling. The training-time / risk-budget version we hadn't articulated; thank you for prompting it.</p>",
          "votes": 0,
          "replies": []
        }
      ]
    },
    {
      "id": 3457297,
      "author_name": "Philip Vonderlind",
      "author_url": "",
      "post_date": "2026-05-13T13:49:55.270000",
      "content": "<p>Hi, thank you for your submission to the Writeup Prize. <a href=\"https://www.kaggle.com/sambealing\" target=\"_blank\">@sambealing</a> and I will post some questions below that help us in assessing your submission more closely, as described in the <a href=\"https://www.kaggle.com/competitions/ai-mathematical-olympiad-progress-prize-3/discussion/689703\" target=\"_blank\">updated discussion post</a>.</p>\n<p>Your work shows impressive contributions, including implementing a missing feature in vLLM. We especially appreciate the in-depth engineering insights you provided, which even helped resolve issues that other participants encountered during the competition. I only have a few more surface-level questions:</p>\n<ol>\n<li><p><strong>vLLM</strong>: Was your PR eventually accepted into vLLM? I have read several other writeups from participants who used EAGLE3 and encountered the same issue you describe above. Please keep us posted if there are any updates.</p></li>\n<li><p><strong>Pass@k Plot</strong>: Did you mean to write that <code>gpt-oss-120b</code> wins pass@k at every time budget? Based on the plot, it seems that Cascade outperforms <code>gpt-oss-120b</code> across all tested budgets.</p></li>\n<li><p><strong>Time Management</strong>: Since EAGLE3 primarily improves inference speed and therefore saves time, why did you not spend more effort on developing a more advanced time-management system as well to further increase the gains? Many other contestants found that a good scheduling strategy could help, especially because hard and easy problems appeared in random order.</p></li>\n<li><p><strong>Tool Calls</strong>: Did you investigate the common causes of the significantly higher tool-call error rate in <code>gpt-oss-120b</code>, such as missing packages, outdated APIs, syntax issues, or similar failure modes? It would be interesting to understand why the Nemotron model appears to produce much cleaner tool calls.</p></li>\n</ol>\n<p>Looking forward to your response :)</p>",
      "votes": 1,
      "replies": [
        {
          "id": 3459539,
          "author_name": "Chan Kha Vu",
          "author_url": "",
          "post_date": "2026-05-17T23:59:55.407000",
          "content": "<p>Hi Philip Vonderlind (@philipvonderlind), thank you for thoughtful questions. Your 2nd question raised a real concern that we were not clear about our wording enough. The \"gpt-oss-120b always wins pass@k\" is false, and it should be attributed to \"wins given attempt budget\", not \"time budget\".</p>\n<p>Your 4th question called out a real mistake in our numbers and analysis. We carefully re-visited that analysis and re-generated the chart (basically it was a parsing issue on our end due to prompt differences between gpt-oss and cascade 2).</p>\n<h2>1. vLLM PR status</h2>\n<blockquote>\n  <p><strong>vLLM:</strong> Was your PR eventually accepted into vLLM? I have read several other writeups from participants who used EAGLE3 and encountered the same issue you describe above. Please keep us posted if there are any updates.</p>\n</blockquote>\n<p>Not merged yet — we're still working through two issues before it's ready to upstream:</p>\n<ol>\n<li><strong>Hopper-only optimizations.</strong> The performance work in our fork (cudagraph capture of the K-iteration drafter loop, the PIECEWISE → FULL upgrade we planned for p=12+, the FlashInfer attention switch) assumed Hopper SM 9.0 behaviour (because AIMO3 competition uses H100). On Blackwell SM 10.0 the same code hits graph-compilation issues we're still resolving. vLLM upstream needs GPU-portable code paths, so the Blackwell-clean rewrite of the kernel layer needs to land before we can merge.</li>\n<li><strong>A community PR now covers the correctness fix.</strong> One week after the competition deadline, <a href=\"https://github.com/vllm-project/vllm/pull/41233\" target=\"_blank\"><code>vllm-project/vllm#41233</code></a> was opened against the same mamba-cache-poisoning bug. It's still open at the time of writing, but it's likely the path upstream takes. Our perf optimizations are <strong>orthogonal</strong> to that PR (it has no speed/kernels work) and would still apply on top of it; we plan to submit them as a follow-up once the correctness side lands and our Blackwell story is fixed.</li>\n</ol>\n<p>The <a href=\"https://github.com/hav4ik/vllm/tree/pc-spec-v2-nonpc-slots\" target=\"_blank\">fork</a> remains useful for anyone running Hybrid-Mamba SD on Hopper today. We added a new <em>§Upstream PR status</em> subsection inside the writeup's vLLM section to track this honestly, and we'll keep you posted as the situation upstream evolves.</p>\n<h2>2. Pass@k plot</h2>\n<blockquote>\n  <p><strong>Pass@k Plot:</strong> Did you mean to write that gpt-oss-120b wins pass@k at every time budget? Based on the plot, it seems that Cascade outperforms gpt-oss-120b across all tested budgets.</p>\n</blockquote>\n<p>You're right — the original sentence was wrong. Within the plotted time budget, <strong>Cascade 2 wins both pass@k and maj@k</strong>: maj@k by a comfortable margin across the whole range, and pass@k by a smaller margin because Cascade 2 is cheaper per rollout and gets to fit more rollouts in any fixed wall-clock budget.</p>\n<p>What we <em>should</em> have written, and now do: gpt-oss-120b has a higher pass@k <em>ceiling</em> that only becomes visible <strong>beyond</strong> the plotted budget — its rollouts are more diverse (helps pass@k, hurts plurality voting), so given enough attempts a hypothetical oracle verifier would eventually prefer gpt-oss-120b. Inside the actual competition's time budget, Cascade 2 dominates on both axes.</p>\n<p>The paragraph next to the chart has been rewritten to reflect this. We've also added a note explaining why we couldn't realize that pass@k ceiling within Kaggle's resource budget anyway: a separate verifier won't fit alongside gpt-oss-120b's ~120 GB MXFP4 weights on a single H100, and using gpt-oss-120b as its own verifier doubles the per-problem token bill on a budget that already only fits ~8 solution rollouts.</p>\n<h2>3. Time management</h2>\n<blockquote>\n  <p><strong>Time Management:</strong> Since EAGLE3 primarily improves inference speed and therefore saves time, why did you not spend more effort on developing a more advanced time-management system as well to further increase the gains? Many other contestants found that a good scheduling strategy could help, especially because hard and easy problems appeared in random order.</p>\n</blockquote>\n<p>Honest answer: we joined the competition late, and the Kaggle 1-submission-per-day cap meant essentially every scheduling choice we shipped was a <strong>first-try call</strong>, with no headroom to A/B-test on the hidden LB. A more elaborate per-problem scheduler was on the wishlist, but the engineering dependency we <em>did</em> spend weeks on — PC + SD support for hybrid-Mamba models in vLLM — was already over our time budget, and we judged that getting the simple harness right was a safer use of the remaining days than tuning a scheduler we couldn't validate without burning submission slots.</p>\n<p>We also weighted this decision by what optimizing for in the first place. Our priors from AIMO 2 were that winning solutions in this class of competition optimized for <strong>maj@k under a time budget</strong>, not pass@∞ — pass@∞ tends to be similar across competitive models, but maj@k under a real budget is where the consistency premium pays off. That's why we picked Cascade 2, which (per the test-time-scaling chart) gives the best maj@k under a wall-clock budget. Cascade 2's lower per-rollout cost also means an adaptive scheduler has less to squeeze: the simple <code>min(900, max(300, time_left − 300 × problems_left_others))</code> formula already kept us at the 900 s ceiling for most of each shuffle.</p>\n<p>In hindsight, a smarter scheduler would have been a high-leverage thing to try, and we've added it to the <em>What we'd do differently</em> list in the Conclusion. The Conclusion now also has a <em>\"Why this is mostly a first-try harness\"</em> paragraph that articulates this trade-off explicitly.</p>\n<h2>4. Tool calls</h2>\n<blockquote>\n  <p><strong>Tool Calls:</strong> Did you investigate the common causes of the significantly higher tool-call error rate in gpt-oss-120b, such as missing packages, outdated APIs, syntax issues, or similar failure modes? It would be interesting to understand why the Nemotron model appears to produce much cleaner tool calls.</p>\n</blockquote>\n<p>Yes, in depth. We added <strong>Appendix G</strong> (tool-call error breakdown) which re-pairs every tool response in the LB-sim with the code that triggered it (504 rollouts per model). Headline findings:</p>\n<ol>\n<li><strong>Time, not count, is what matters.</strong> A typical gpt-oss-120b rollout burns ~37 s of wall-clock on failed tool calls (14% of its rollout), Cascade 2 ~17 s (6%). Per-call error <em>rates</em> are within a few points of each other; the per-rollout <em>time</em> difference is ~2.2×.</li>\n<li><strong>Timeouts dominate the wasted time.</strong> Pure 30 s sandbox timeouts (no output produced) account for <strong>88.6%</strong> of gpt-oss-120b's wasted tool wall-clock and <strong>94.0%</strong> of Cascade 2's. Every Python error class is a rounding error by time compared to timeouts. This was the most surprising finding for us — the wasted wall-clock is overwhelmingly about <em>how often the model writes code that doesn't finish in 30 s</em>, not about syntactic or library failures.</li>\n<li><strong>NameError is the strongest cross-model signal on Python errors.</strong> 0.49/rollout for gpt-oss-120b vs 0.15 for Cascade 2 (3.3×). Two root causes split the gpt-oss-120b cases roughly into a third and two-thirds:<ul>\n<li><strong>Forgotten imports (~1/3)</strong> — <code>sp</code> (sympy alias), <code>random</code>, <code>deque</code>, <code>math</code>, etc. The pre-import block (<code>math, numpy, sympy, itertools, collections, mpmath</code>) doesn't expose <code>sympy as sp</code> or <code>random</code>, and gpt-oss-120b writes Jupyter-cell-style snippets that assume these names are already live in the session rather than <code>import</code>-ing them.</li>\n<li><strong>Sandbox rollback (~2/3)</strong> — standard Jupyter / IPython preserves partial assignments when a cell errors; our sandbox snapshots state <em>before</em> each cell and restores that snapshot whenever the cell raises. Models trained on REPL semantics write follow-up code referencing variables from cells that errored, and hit <code>NameError</code> on what they assumed were live names. Cascade 2 hits the same rollback mechanism, just less often (it writes more self-contained cells with explicit imports at the top of each block).</li></ul></li>\n<li><strong>gpt-oss-120b also pays a SyntaxError toll</strong> (0.72 / rollout, 26% of its non-timeout errors), almost all incomplete-bracket / truncation patterns. We suspect this is a public-notebook artefact (smaller code chunks) rather than a model-trait, but didn't run the experiment to confirm.</li>\n</ol>\n<p>We also re-rendered the per-problem-breakdown chart on the same LB-sim data after discovering that the original chart silently bucketed Cascade 2's bare-text timeout responses as \"successful tool call\" (the two solver recorders formatted timeout strings differently).</p>",
          "votes": 0,
          "replies": []
        }
      ]
    },
    {
      "id": 3447586,
      "author_name": "ivan",
      "author_url": "",
      "post_date": "2026-04-23T13:12:42.480000",
      "content": "<p>Fantastic engineering work! I have a question about speculative decoding. You show very good \"Accept rate\", but how it changes during long trajectories? You fine-tuned it with 65k context, but does it work well with long context at test time?</p>",
      "votes": 1,
      "replies": [
        {
          "id": 3447798,
          "author_name": "Chan Kha Vu",
          "author_url": "",
          "post_date": "2026-04-24T01:00:39.007000",
          "content": "<p>Thanks for your kind words 😁 You've got a good point!</p>\n<p>Although we did fine-tune with maximum of 65K context, in reality there's not many TIR tasks that have more than 40K generated tokens (we did our best to filter out the hardest ones). TIR reasoning is much shorter than COT (which we didn't sampled enough for training). Validation during training was done on a hard subset of IMO-AnswerBench (we filtered all validation benchmarks like AIME and IMO-benches out from training), but they don't have that much rollouts with &gt;65K tokens either.</p>\n<p>However, the <strong>3.55</strong> acceptance rate cited above was on AIME'26, which doesn't need more than 65K tokens. It was my primary \"unit test\" for Eagle3 work, because AIME is simple enough to run in ~10 minutes, but just hard enough to reveal any bugs in Mamba states handling (my criteria was 98% accuracy - if I see a drop of 10%, I know exactly that somewhere a Mamba state got corrupted), and acceptance rate numbers were collected together with accuracy unit testing. We didn't have time to properly test it on enough 65K+ samples. Will come back with some measurements and update the writeup (I'll continue working on the PC+SD anyways).</p>\n<p>One thing I want to emphasize: Speculative Decoding is not worth it for small models. I know that people smarter than me already knows this, but the economy of 3B active models is so brutal. 1 token step is just <strong>3.4</strong> ms: 2.8ms Cuda Graph + 0.6ms overhead from CPU/sampling. Naive Eagle3 implementation is 13ms - you need acceptance rate of 4 to break even. We brought that down to 8.9ms - but that still requires 2.6 acceptance rate to break even. Not to mention other overheads that makes it closer to 3.0 acceptance rate to break even. Models like Cascade2 are heartbreakingly ridiculously fast!</p>",
          "votes": 2,
          "replies": []
        }
      ]
    },
    {
      "id": 3447327,
      "author_name": "Chan Kha Vu",
      "author_url": "",
      "post_date": "2026-04-23T00:08:32.747000",
      "content": "<p>This is <a href=\"https://www.kaggle.com/yeoyunsianggeremie\" target=\"_blank\">@yeoyunsianggeremie</a> watching me getting stuck at some random side-quests \n<img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F2270821%2F7f5e4044400f6c2e3207611fc92cbad5%2Ffrieren-beyond-journey-s-end-15-frieren-fern-mimic.avif?generation=1776902886558630&amp;alt=media\" alt=\"\"></p>",
      "votes": 2,
      "replies": []
    },
    {
      "id": 3459536,
      "author_name": "Chan Kha Vu",
      "author_url": "",
      "post_date": "2026-05-17T23:58:07.073000",
      "content": "<p>Changes to address rebuttal questions:</p>\n<h1>Writeup changelog</h1>\n<p>Short list of edits made to <code>writeup.md</code> (and supporting artifacts) in this rebuttal pass.</p>\n<h2>Bug fixes</h2>\n<ul>\n<li><strong>Per-problem-breakdown chart (Online LB simulation section).</strong> Re-rendered <code>kaggle_per_problem_stats.png</code> from scratch. The original right panel used an asymmetric <code>is_error</code> rule: the gpt-oss-120b recorder prefixed its 30 s timeouts with <code>[ERROR]</code>, our Cascade 2 recorder left them bare, and the same rule caught one and missed the other. Cascade 2's <code>tool_err</code> went from <strong>0.2% → 6.3%</strong> of wall-clock; gpt-oss-120b's stayed at 14.4%. The corrected chart also splits timeouts from Python errors into two visually distinct buckets (purple/red), and the panel-3 subtitle now states the 30 s sandbox timeout cap that applies to both solvers. Full reconciliation in <code>rebuttal_n4.md</code>.</li>\n<li><strong>Head-to-head table — Tool err (per-call).</strong> Cascade 2 line updated from <strong>6.0% → 10.6%</strong> for the same reason (broader symmetric definition that includes timeouts).</li>\n<li><strong>Test-time scaling claim.</strong> Removed the incorrect statement that \"gpt-oss-120b wins pass@k at every time budget\" — within the plotted range, Cascade 2 wins both pass@k and maj@k. Rewrote the surrounding paragraph: gpt-oss-120b has the higher pass@k <em>ceiling</em> beyond the plotted budget, but Cascade 2 dominates inside it.</li>\n</ul>\n<h2>New content</h2>\n<ul>\n<li><strong>Appendix G — Tool-call error breakdown.</strong> New appendix answering \"why does gpt-oss-120b's tool-call overhead come out roughly 2× Cascade 2's?\". Per-rollout summary table + per-class breakdown (timeouts, SyntaxError, NameError, …) + prose on the two NameError root causes (forgotten stdlib imports; sandbox state-rollback semantics differing from standard Jupyter/IPython).</li>\n<li><strong>Resource-constraint note on the pass@k ceiling.</strong> Added a paragraph in <em>Test-time scaling</em> explaining why we can't realize gpt-oss-120b's higher pass@k ceiling on Kaggle: a separate verifier won't fit alongside ~120 GB MXFP4 weights, and gpt-oss-120b as its own verifier doubles the per-problem token bill.</li>\n<li><strong>First-try-harness explanation in Conclusion.</strong> New paragraph explaining the late entry, the 1-submission-per-day cap, why we picked Cascade 2 (AIMO-2 priors on maj@k vs pass@∞), and why we didn't develop a more advanced scheduler. Also added \"smarter per-problem scheduling policy\" to the existing <em>What we'd do differently</em> list.</li>\n<li><strong>Contamination note in §Evaluation set.</strong> Acknowledged that IMO-AnswerBench is built from prior IMO/Shortlist problems and that both candidate models likely have some exposure, with Cascade 2 more so (it was released <em>after</em> AnswerBench). We rely on the Nemotron-Cascade-2 paper's n-gram deduplication procedure; relative comparisons are less affected than absolute numbers.</li>\n<li><strong>Appendix D — attempt-count chart.</strong> Added <code>test_time_scaling_attempts.png</code> as a visual companion that drops the per-rollout cost scaling. One sentence of framing, no separate findings.</li>\n<li><strong>Private LB column.</strong> Added to the top summary table and the head-to-head table; our final submission scored <strong>41 / 50</strong> on private LB.</li>\n<li><strong>Upstream PR status subsection.</strong> Inside the vLLM section, explaining why the PC + SD PR is on hold: Hopper-only kernel optimizations that won't compile on Blackwell, and community PR <a href=\"https://github.com/vllm-project/vllm/pull/41233\" target=\"_blank\">vllm-project/vllm#41233</a> (opened 1 week after the competition deadline) now covers the correctness fix from a different angle. The open-source-contributions bullet at the top no longer claims the PR as a deliverable.</li>\n<li><strong><code>Summary of our main candidates</code></strong> lead-in tightened from \"local validation and public LB\" → \"local validation and the Kaggle leaderboard\" (now covers Public + Private columns).</li>\n</ul>",
      "votes": 0,
      "replies": []
    }
  ],
  "comment_count": 8,
  "messages": [
    {
      "id": 3447188,
      "postDate": "2026-04-22T20:11:15.027Z",
      "content": "<p>For both <a href=\"https://www.kaggle.com/yeoyunsianggeremie\" target=\"_blank\">@yeoyunsianggeremie</a> and me, this is our third time participating in AIMO. Last year, we went all out and put everything into SFT + RLHF (Kaggle post: <a href=\"https://www.kaggle.com/competitions/ai-mathematical-olympiad-progress-prize-2/writeups/so-we-cooked-a-model\" target=\"_blank\">\"So… We cooked a model 🤷\"</a>, detailed blog post: <a href=\"https://hav4ik.github.io/improving-deepseek-r1/\" target=\"_blank\">\"Improving DeepSeek R1 in Math\"</a>). This year, with Geremie grinding 75+ hours per week at his startup and me in the middle of a release, we had almost no time to compete like we did last year. Luckily, we had companions helping us: Geremie's <a href=\"https://github.com/bogoconic1/Qgentic-AI\" target=\"_blank\">bogoconic1/Qgentic-AI</a> and my Claude Code Max subscription!</p>\n<p>Despite joining the competition only in the last couple of weeks, we got delightfully stuck in a few side quests, and ended up creating a few useful artifacts that we think are worth sharing.</p>\n<p>We'd like to express our gratitude to AIMO3 organizers and XTX market for hosting such a fun competition and providing H100 GPUs. Special thanks to Simon Frieder (<a href=\"https://www.kaggle.com/friederrr\" target=\"_blank\">@friederrr</a>) for actively engaging with the community, making it one of the most pleasant competitions I've ever participated in.</p>\n<hr>\n<h1>Summary</h1>\n<h2>Open-Source Contributions</h2>\n<p>Our little fun time with AIMO3 resulted in following open-source side quests, useful beyond this competition:</p>\n<ul>\n<li><strong>A collection of quantized Nemotron Cascade 2 models</strong>, <a href=\"https://huggingface.co/chankhavu/Nemotron-Cascade-2-30B-A3B-FP8\" target=\"_blank\">chankhavu/Nemotron-Cascade-2-30B-A3B-FP8</a> and <a href=\"https://huggingface.co/chankhavu/Nemotron-Cascade-2-30B-A3B-NVFP4\" target=\"_blank\">chankhavu/Nemotron-Cascade-2-30B-A3B-NVFP4</a>, totaling 60K downloads on Huggingface 🤗. Hybrid Mamba-MoE models are tricky to quantize because the Mamba state is sensitive to numerical precision; these FP8 and NVFP4 variants were first ones of such precisions published for Cascade 2 and show no degradation in quality.</li>\n<li><strong>Enabled Hybrid-Mamba models to use Prefix Caching (PC) with Speculative Decoding (SD) in vLLM.</strong> PC (reuse KV tensors across the common prefix of sibling generations) and SD (draft K tokens with a small model, verify K at once with the large one) are standard inference-time tricks, but running them together on Hybrid-Mamba models causes <em>mamba cache poisoning</em> — rejected speculative tokens leak into the shared Mamba state — and has been an open problem in both vLLM and SGLang. <a href=\"https://github.com/hav4ik/vllm/tree/pc-spec-v2-nonpc-slots\" target=\"_blank\">Our custom vLLM fork</a> solves it with per-request \"ghost\" Mamba states. See <em>Upstream PR status</em> in the vLLM section below for upstreaming status.</li>\n<li><strong>An EAGLE-3 speculator for Nemotron Cascade 2:</strong> <a href=\"https://huggingface.co/chankhavu/Nemotron-Cascade2-30B-A3B-Eagle3-Long-Context\" target=\"_blank\">chankhavu/Nemotron-Cascade2-30B-A3B-Eagle3-Long-Context</a> — the small draft-head model SD uses to propose tokens — reaching <strong>3.55</strong> mean accepted tokens at K=7. For long-context speculator training we <a href=\"https://github.com/hav4ik/SpecForge-Nemotron3/tree/nemotron-cascade-2-experiments\" target=\"_blank\">modified SpecForge</a> to use chunked fused soft-target CE, chunked MLP, sliding attention, and correct handling of Mamba SSM states. We'll make a PR to SpecForge upstream as well.</li>\n</ul>\n<p>We also uncovered cheating attempts by Cascade 2 on Math problems (searching the web for answers) and reported to model authors here: <a href=\"https://huggingface.co/nvidia/Nemotron-Cascade-2-30B-A3B/discussions/25#69e67838889c53e909054431\" target=\"_blank\">Cascade-2 cheating attempts on Math is… cute</a>.</p>\n<h2>Our final submission</h2>\n<blockquote>\n  <p>Kaggle notebook: <a href=\"https://www.kaggle.com/code/chankhavu/c2-distill-vllm018-watchdog\" target=\"_blank\">chankhavu/c2-distill-vllm018-watchdog</a>\n  Model: <a href=\"https://www.kaggle.com/models/chankhavu/c2-legaldistill-30b-a3b-fp8/transformers/fp8/1\" target=\"_blank\">chankhavu/c2-legaldistill-30b-a3b-fp8</a></p>\n</blockquote>\n<p>Our solution uses <a href=\"https://huggingface.co/nvidia/Nemotron-Cascade-2-30B-A3B\" target=\"_blank\">Nemotron-Cascade-2-30B-A3B</a>, which we soft-cloned via layer-wise linear distillation to make it legal for this competition (Cascade 2 was released after the March 15 cutoff — see the Introduction below), then selectively quantized to FP8 with no quality degradation.</p>\n<p>The final harness is intentionally simple: <strong>8 parallel rollouts per problem with Python tool use, entropy-weighted majority voting</strong>. We also tried a more sophisticated variant that filled the GPU idle bubbles between early- and late-finishing rollouts with pairwise LLM-as-judge self-verification, but didn't have time to tune it properly to beat plain majority voting. We also put a great deal of work to make speculative decoding work with prefix caching for hybrid-mamba models, but it didn't get into our final submission due to lack of submission budget and time to test things out properly. Summary of our main candidates on local validation and the Kaggle leaderboard:</p>\n<table>\n<thead>\n<tr>\n<th><strong>Submission</strong><br>(model, harness)</th>\n<th><strong>Local validation</strong><br>(3 shuffles)</th>\n<th><strong>Local validation</strong><br>(mean)</th>\n<th><strong>Public LB attempts</strong></th>\n<th><strong>Private LB</strong></th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>gpt-oss-120b, 44/50 public notebook</td>\n<td>33, 30, 36</td>\n<td>33.0</td>\n<td>38, 38, 37</td>\n<td>—</td>\n</tr>\n<tr>\n<td>Cascade 2, 8 rollouts, self-verification (untuned)</td>\n<td>33, 31</td>\n<td>32.0</td>\n<td>34</td>\n<td>—</td>\n</tr>\n<tr>\n<td><strong>Cascade 2, 8 rollouts, voting</strong><br><strong>(our final submission)</strong></td>\n<td><strong>36, 34, 34</strong></td>\n<td><strong>34.6</strong></td>\n<td><strong>36, 39, 38</strong></td>\n<td><strong>41</strong></td>\n</tr>\n</tbody>\n</table>\n<p>Our local validation imitates the Kaggle leaderboard setting — 50 hard IMO-level problems drawn from <a href=\"https://imobench.github.io/\" target=\"_blank\">IMO-AnswerBench</a> (an open benchmark of IMO-style problems with short-integer answers), 5 hours on 1× H100 SXM. We shuffle the 50 problems 3 times with fixed seeds and run each solver through each shuffle, which gives us a controlled way to compare candidates without burning LB submissions.</p>\n<hr>\n<h1>Introduction</h1>\n<p><strong>The competition.</strong> <a href=\"https://www.kaggle.com/competitions/ai-mathematical-olympiad-progress-prize-3\" target=\"_blank\">AIMO3</a> (the AI Mathematical Olympiad Prize 3, hosted by Kaggle and XTX Markets) evaluates math-problem solvers on 50 held-out IMO-level problems with short-integer answers. Each submission is a notebook that runs on a single NVIDIA H100 SXM (80 GB) with a 5-hour wall-clock budget. Models published after March 15, 2026 are not allowed to be used directly at runtime, which is the rule that shapes most of the model choices below. The public leaderboard is scored on a visible subset; the final standings use a disjoint private set revealed at the end.</p>\n<p><strong>The main model candidate</strong> everyone converged on this year is <a href=\"https://huggingface.co/openai/gpt-oss-120b\" target=\"_blank\">gpt-oss-120b</a> — OpenAI's 120 B-parameter open-weights reasoning model with native mxfp4 weights and a Harmony chat format. It's the model behind the strongest public-notebook baseline (the \"44/50\" notebook on the AIMO3 discussion board), and our main point of comparison throughout.</p>\n<p>In March 19, <a href=\"https://www.kaggle.com/chankhavu\" target=\"_blank\">@chankhavu</a>'s birthday, <a href=\"https://huggingface.co/nvidia/Nemotron-Cascade-2-30B-A3B\" target=\"_blank\">Nemotron-Cascade-2-30B-A3B</a> (shortened to <em>Cascade 2</em> in this post) was released — NVIDIA's 30 B hybrid Mamba-MoE reasoning model. Published just after the March 15 cutoff, so not directly usable as-is, but the architecture is identical to the pre-cutoff <a href=\"https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16\" target=\"_blank\">Nemotron-3-Nano-30B-A3B</a>. This is what made the \"soft-cloning\" approach legal, and it's the model inside our final submission. The hybrid-Mamba architecture is what forced us down a bunch of the side quests below — quantizing it, getting prefix caching to work with speculative decoding, training an Eagle3 speculator, and so on.</p>\n<p><strong>Other participant's findings.</strong> With only a few weeks to compete, we relied heavily on the open-source ecosystem and the community's collective findings. Some highlights:</p>\n<ul>\n<li><a href=\"https://www.kaggle.com/code/pawanmali/chasing-47-50-aimo3-journey-of-60-experiments\" target=\"_blank\">AIMO3 journey of 60 experiments</a> by <a href=\"https://www.kaggle.com/pawanmali\" target=\"_blank\">@pawanmali</a> - this incredible repository of experimentation results guided us towards simpler prompts in our solutions.</li>\n<li><a href=\"https://www.kaggle.com/code/nihilisticneuralnet/44-50-let-me-over-cook\" target=\"_blank\">44/50 public notebook</a> by <a href=\"https://www.kaggle.com/nihilisticneuralnet\" target=\"_blank\">@nihilisticneuralnet</a> - the strongest public notebook baseline, using gpt-oss-120b with a custom chat format and a lot of engineering to get the most out of it.</li>\n<li><a href=\"https://arxiv.org/abs/2603.27844\" target=\"_blank\">Inference-Time Optimization Lessons from AIMO 3</a> by Natapong Nitarach - similar ablation findings to <a href=\"https://www.kaggle.com/pawanmali\" target=\"_blank\">@pawanmali</a>.</li>\n</ul>\n<h1>Delightful side quests</h1>\n<h2>Soft-cloning Cascade 2</h2>\n<p>The competition rules prohibits using models published after March 15 directly during runtime. However, there's no limit on distillation or how distillation should be made. We rely on the following <a href=\"https://www.kaggle.com/competitions/ai-mathematical-olympiad-progress-prize-3/discussion/680682\" target=\"_blank\">FAQ clarification from the competition organizers</a>:</p>\n<blockquote>\n  <p><strong>Question:</strong> Say I initialize an entirely new model with same architecture as the post - cutoff teacher model, but then train it by layerwise MSE to the teacher on the residual stream; in the limit this could approximate the teacher model weights, I just didn't explicitly say that I used the weights themselves.</p>\n  <p><strong>Answer:</strong> Indeed, while conceptually, these two cases can blur, practically they don't and that is why the AMLT rules makes sense: In practice, with this approach you'd recreate in a way the model that was off limits.</p>\n</blockquote>\n<p>We further clarified with the organizers that this kind of \"soft cloning\" is legal for the competition (<a href=\"https://www.kaggle.com/competitions/ai-mathematical-olympiad-progress-prize-3/discussion/680682#3425516\" target=\"_blank\">here</a> and <a href=\"https://www.kaggle.com/competitions/ai-mathematical-olympiad-progress-prize-3/discussion/680682#3440271\" target=\"_blank\">here</a>).</p>\n<h3>Approach</h3>\n<p>After confirming the legality, we performed the following layer-wise MSE distillation from <a href=\"https://huggingface.co/nvidia/Nemotron-Cascade-2-30B-A3B\" target=\"_blank\">Nemotron-Cascade-2</a>. We initialized a randomly initialized student model with the same architecture and performed the following procedure:</p>\n<ul>\n<li>We build a collection of linear models $$F^\\text{Teacher}_i(X) = W^{Teacher}_i \\times X$$ for each layer <code>i</code> of the teacher <a href=\"https://huggingface.co/nvidia/Nemotron-Cascade-2-30B-A3B\" target=\"_blank\">Nemotron-Cascade-2</a> model, where <code>X</code> is the input to that layer and <code>W_i</code> the weight tensor.</li>\n<li>We do the same for the student model, building $$F^\\text{Student}_i(X) = W^{Student}_i \\times X$$ for each layer <code>i</code>. The student model happens to have the same architecture, so we can directly map each <code>F^Teacher}_i</code> to a student <code>F^Student_i</code>.</li>\n<li>We optimize each student layer <code>i</code> to minimize the following MSE loss: $$L_i = \\mathbb{E}_{X \\sim D} \\left[ | F^\\text{Teacher}_i(X) - F^\\text{Student}_i(X) |^2_2 \\right]$$ where <code>D</code> is a dataset of randomly sampled inputs. We optimize either by gradient descent or by closed-form MSE optimization techniques. Each student treats the teacher as a black-box function that produces outputs for given inputs.</li>\n</ul>\n<p>We <strong>never</strong> optimize towards the teacher weights directly; we only observe the teacher outputs and use them as MSE targets for the student. However, since the sub-models are linear, convergence is mathematically absolute. We published the implementation <a href=\"https://gist.github.com/hav4ik/e4b3513bb4ece8b45fc414e024aeae72\" target=\"_blank\">here</a>. The BF16 model can be found here: <a href=\"https://www.kaggle.com/models/chankhavu/bogoconic1-c2-softcpy-v3\" target=\"_blank\">chankhavu/bogoconic1-c2-softcpy-v3</a>.</p>\n<h2>Quantizing the Cascade 2 model</h2>\n<p><strong>Important clarification:</strong> for Kaggle submission, we quantized the cloned Cascade 2 model separately from the cloned BF16 model! We did NOT use our published open-source models.</p>\n<blockquote>\n  <p>Open-source artifacts (totaling 66K downloads on Huggingface as of April 21, 2026):</p>\n  <ul>\n  <li>🤗 Huggingface model: <a href=\"https://huggingface.co/chankhavu/Nemotron-Cascade-2-30B-A3B-NVFP4\" target=\"_blank\">chankhavu/Nemotron-Cascade-2-30B-A3B-NVFP4</a></li>\n  <li>🤗 Huggingface model: <a href=\"https://huggingface.co/chankhavu/Nemotron-Cascade-2-30B-A3B-FP8\" target=\"_blank\">chankhavu/Nemotron-Cascade-2-30B-A3B-FP8</a></li>\n  <li>🤗 Huggingface model: <a href=\"https://huggingface.co/chankhavu/Nemotron-Cascade-2-30B-A3B-AWQ-INT4\" target=\"_blank\">chankhavu/Nemotron-Cascade-2-30B-A3B-AWQ-INT4</a></li>\n  </ul>\n  <p>Quantization code: <a href=\"https://www.kaggle.com/code/chankhavu/quantize-cascade-2\" target=\"_blank\">chankhavu/quantize-cascade-2</a>; calibration data: <a href=\"https://www.kaggle.com/datasets/chankhavu/cascade-2-calibration-data\" target=\"_blank\">chankhavu/cascade-2-calibration-data</a>.</p>\n</blockquote>\n<h3>Precision recipe</h3>\n<p>We followed the Nano-architecture selective quantization recipe from the <a href=\"https://research.nvidia.com/labs/nemotron/files/NVIDIA-Nemotron-3-Nano-Technical-Report.pdf\" target=\"_blank\">Nemotron 3 Nano Technical Report</a> (Section 4). This is the same recipe as <a href=\"https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-FP8\" target=\"_blank\">NVIDIA's official FP8 checkpoint</a> of Nemotron-3-Nano-30B-A3B. Sensitive components are kept in higher precision:</p>\n<ul>\n<li>There are only 6 attention layers, so they are highly sensitive and should be preserved in BF16.</li>\n<li>To match the input granularity of attention layers, Mamba layers adjacent to attention layers should be kept at BF16 as well.</li>\n<li>The Mamba 1D conv is kept at BF16 across all 23 Mamba layers (quantizing it degrades the state update).</li>\n<li>MoE router gates should be kept at FP32. Degradation in routing precision will affect reasoning.</li>\n</ul>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2Fca39d8cf5ccf892e7080773c16103025%2Fcascade2_quant_layout.png?generation=1776888105108688&amp;alt=media\" alt=\"Per-layer quantization layout for Nemotron Cascade 2: 52 transformer layers flanked by Embedding and lm_head. Attention layers and the Mamba layer immediately preceding each attention are kept BF16; the other Mamba layers and all MoE expert blocks are quantized to FP8 / NVFP4.\"></p>\n<h3>Implementation details</h3>\n<p>For calibration, we constructed a dataset of <strong>4000 samples</strong> from <a href=\"https://huggingface.co/datasets/nvidia/Nemotron-Cascade-2-SFT-Data\" target=\"_blank\">Nemotron-Cascade-2-SFT-Data</a> with the following domain mix, specifically designed for long-context reasoning: math (1000), swe (900), terminal_agent (500), science (500), chat (400), conversational_agent (300), instruction_following (300), safety (100). We trained on sequence length of up to <strong>12,288 tokens</strong> (no padding, natural length per sample).</p>\n<p>We validated the correctness of our quantized models using <a href=\"https://github.com/NVIDIA-NeMo/Evaluator\" target=\"_blank\">NVIDIA-NeMo/Evaluator</a> with config from <a href=\"https://github.com/NVIDIA-NeMo/Evaluator/blob/main/packages/nemo-evaluator-launcher/examples/nemotron/nemotron-3-super/local_nemotron-3-super-120b-a12b_tools.yaml\" target=\"_blank\">Nemotron-3-Super-120B's eval config</a>. We see no degradation in reasoning quality of quantized models. More details in <em>Appendix B</em>.</p>\n<h2>Enable Hybrid-Mamba models to use PC and SD together in vLLM</h2>\n<blockquote>\n  <p>Open source artifact: <a href=\"https://github.com/hav4ik/vllm/tree/pc-spec-v2-nonpc-slots\" target=\"_blank\">hav4ik/vllm branch pc-spec-v2-nonpc-slots</a></p>\n</blockquote>\n<h3>The problem</h3>\n<p>Out of the box, neither vLLM nor SGLang can run Prefix Caching (PC) and Speculative Decoding (SD) together on Hybrid-Mamba models. Mamba's SSM layer keeps a fixed-size state vector updated <em>in place</em> — each token folds into it irreversibly. SD runs K candidate tokens through the model before the rejection sampler decides how many to keep; by then the SSM kernel has already overwritten the canonical state with the full K-token trajectory. Transformers escape this because their KV cache is append-only — rejected entries sit past the sequence end and touch nothing. Mamba has one state vector per layer per request and can't roll it back, so PC + SD needs an explicit mechanism to preserve the accepted state.</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2F6f4d1ef1b007dbf7421e0ef6f972dae9%2Fpc_sd_problem.png?generation=1776888162924985&amp;alt=media\" alt=\"When speculation crosses a PC block boundary, Mamba's shared state has already been overwritten by later tokens — the commit writes the final state (s7) into Block 0's cache slot instead of the correct boundary state (s4)\"></p>\n<h3>The correctness fix</h3>\n<p>The non-PC spec-decode path already does the right thing: the SSM and conv Triton kernels maintain K+1 per-request \"ghost\" slots with native <code>init_token_idx</code> rollback — those slots are just gated out whenever PC is active. We re-enable them and add a one-way <strong>boundary commit</strong>: only when a request's last accepted token lands exactly on a mamba-block boundary (<code>last_accepted_pos % block_size == block_size - 1</code>) do we write the ghost slot back to the PC pool. On every other step the pool is untouched, so the canonical cache state is never contaminated by rejected speculations. Ghost slots are indexed by batch position, so batch reordering moves them automatically — no extra bookkeeping.</p>\n<p>One memorable debug moment: the first cut of <code>init_spec_slots</code> allocated each spec slot at the pool's page-padded stride (~50 MB/slot). At 32 seqs × K=5 × 23 layers that comes out to <strong>52 GB</strong> — instant OOM. The fix was to allocate contiguously (~1.3 MB/slot); the kernels read <code>state.stride(0)</code> off the tensor itself, so ghost slots and pool slots can have completely different strides without breaking anything.</p>\n<p>We used AIME-25 as a unit test throughout, with a pass threshold of <strong>≥95% per-session accuracy</strong> — ironic, because AIME-25 is a notoriously hard competition that few high schoolers solve reliably, but at this point it's well within what a modern reasoning LLM with tool use can do consistently. A reliable gold standard for \"nothing silently corrupted\" is useful regardless.</p>\n<h3>Performance optimization</h3>\n<p>Once the correctness fix was in, Eagle3 was still a net loss at single-sequence concurrency: a full step took ~18 ms vs ~3.4 ms for no-Eagle, even though the target forward itself is only 4.2 ms. Profiling (H100, K=4, p=1) split the 18 ms budget roughly like this:</p>\n<table>\n<thead>\n<tr>\n<th>Stage</th>\n<th>Time (ms)</th>\n<th>Notes</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Target forward (K+1 tokens)</td>\n<td>4.2</td>\n<td>actual model compute</td>\n</tr>\n<tr>\n<td>Drafter, K=4 iterations</td>\n<td>11.2</td>\n<td><strong>serial</strong>: 4 × (~2 ms Python metadata + ~0.8 ms GPU forward)</td>\n</tr>\n<tr>\n<td><code>_build_attention_metadata</code></td>\n<td>5.7</td>\n<td>7× more than no-Eagle — all 52 layers × K+1</td>\n</tr>\n<tr>\n<td><code>_prepare_inputs</code></td>\n<td>1.7</td>\n<td>K+1 tokens per request</td>\n</tr>\n<tr>\n<td><code>commit_boundary_states</code> (23 mamba layers)</td>\n<td>3.9</td>\n<td><code>.nonzero()</code> D2H sync every step + <code>.synchronize()</code> wait</td>\n</tr>\n<tr>\n<td>Rejection sampling + misc</td>\n<td>1.1</td>\n<td></td>\n</tr>\n</tbody>\n</table>\n<p>Two things stood out. First, the drafter loop is serially CPU-bound: each iteration needs the previously accepted token before it can build the next metadata object, so Python rebuilds can't overlap the 0.8 ms GPU forwards, and the GPU sits idle ~70% of the drafter phase. Second, <code>commit_boundary_states</code> — Mamba-specific infrastructure — was paying a full host-sync tax on every step, not just the rare steps where a commit actually fires.</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2Ff9ef240767d03da1ecfd5b621f84b027%2Feagle3_step_timeline.png?generation=1776888210353385&amp;alt=media\" alt=\"Eagle3 step timeline showing CPU/GPU overlap and the serial drafter ping-pong\"></p>\n<p>The drafter issue needs a proper architectural fix (fuse all K iterations into one captured graph with persistent metadata buffers) that we left for upstream. What we did land is a sweep of surgical cuts targeting the other stages:</p>\n<ul>\n<li><strong>Masked Triton commit kernel.</strong> <code>commit_boundary_states</code> used to call <code>.nonzero()</code> on a CUDA bool mask to find requests that had crossed a mamba-block boundary. <code>.nonzero()</code> forces a device → host sync every step. The new kernel grid is <code>(num_layers, num_requests)</code>: each block checks its mask bit on-device and returns early if unset. No host ever sees the mask. <strong>−1.5 ms/step.</strong></li>\n<li><strong>Drop the deferred-commit <code>synchronize()</code>.</strong> The deferred-commit path was calling <code>sample_complete_event.synchronize()</code> — a blocking CPU wait — to guarantee <code>num_accepted_tokens</code> was visible before launching the commit kernel. Since both run on the same CUDA stream as the sampler, stream ordering already guarantees visibility. Removed. <strong>−1.0 ms/step.</strong></li>\n<li><strong>Re-enable <code>update_block_table</code> + batched commit indices.</strong> Two related cleanups on the commit path that were paying per-layer overhead on pure-decode steps. <strong>−4.5 ms/step combined.</strong></li>\n<li><strong>Fast decode <code>attn_meta</code>.</strong> On pure-decode steps, only <code>state_indices_tensor_d</code> differs between the 23 mamba attention builders, but <code>_prepare_inputs</code> was rebuilding the full metadata struct each time. Cache the first build; for the other 22, <code>copy_</code> the per-layer pool mapping into the persistent buffer and <code>dataclasses.replace</code> just that field. <strong>−0.3 ms/step.</strong></li>\n</ul>\n<p>End state at single-sequence (K=4, T=1.0): step time 18 → 8.9 ms, throughput 171 → 298 tok/s vs 303 tok/s for PC-only — essentially at parity.</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2Fb3245aec078557c3cb820f13d9fc5eae%2Feagle3_step_timeline_after.png?generation=1776888256293073&amp;alt=media\" alt=\"Eagle3 step timeline after all optimizations — step ends at 8.9 ms, no more sync stall, drafter phase compact\"></p>\n<p>At higher concurrency, where the serial drafter overhead is amortized across the batch, Eagle3 becomes a real win:</p>\n<table>\n<thead>\n<tr>\n<th>Concurrency</th>\n<th>PC-only</th>\n<th>Eagle3 K=4, T=0.7</th>\n<th>vs PC-only</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>p=1</td>\n<td>303 tok/s</td>\n<td>349 tok/s</td>\n<td>+15%</td>\n</tr>\n<tr>\n<td>p=8</td>\n<td>1186 tok/s</td>\n<td>1552 tok/s</td>\n<td>+31%</td>\n</tr>\n<tr>\n<td>p=12</td>\n<td>1383 tok/s</td>\n<td>1843 tok/s</td>\n<td>+33%</td>\n</tr>\n</tbody>\n</table>\n<h3>What we didn't land</h3>\n<p>On an experimental branch we captured the K-iteration drafter decode loop as a single FULL cudagraph, replayed in 0.16 ms. Draft-phase time dropped from 3.16 ms to 1.39 ms (−56%) with acceptance rates preserved. But overall step time at p=8 was unchanged (13.7 ms) — the CPU savings got absorbed by GPU pipeline stalls in the prep phase, since the drafter's GPU work that previously overlapped the PIECEWISE Python loop now blocked the next iteration. The theoretical payoff is at p=12+ where CPU dispatch becomes the real bottleneck, but we didn't validate that before the deadline.</p>\n<h3>Upstream PR status</h3>\n<p>We had planned to upstream the correctness fix and follow with the perf path, but two things have changed since the competition closed and the PR is currently blocked:</p>\n<ul>\n<li><strong>Hopper-only optimizations don't compile on Blackwell.</strong> Our perf work assumed Hopper SM 9.0 capabilities (cudagraph construction, scheduler shape, the specific PIECEWISE → FULL upgrade we pushed for at p=12+). On Blackwell SM 10.0 the same code hits graph-compilation issues that we haven't resolved yet. vLLM upstream maintains GPU-portable code paths, so we can't merge as-is without a Blackwell-clean rewrite of the kernel layer.</li>\n<li><strong>A separate community PR now covers the correctness fix.</strong> One week after the competition deadline, <a href=\"https://github.com/vllm-project/vllm/pull/41233\" target=\"_blank\">vllm-project/vllm#41233</a> was opened against the same mamba-cache-poisoning bug. It's still open as of writing, but it tackles correctness from a different angle and is likely the path upstream takes. Our perf optimizations (the FlashInfer attention switch and the surgical \"What we did land\" cuts) are orthogonal to that PR and would still apply on top of it — that PR doesn't include any speed work — but we're holding off on a perf-only PR until the correctness side lands and our Blackwell story is fixed.</li>\n</ul>\n<p>The fork itself remains useful for anyone running Hybrid-Mamba SD inference on Hopper; we just can't claim the upstream PR as a clean deliverable any more.</p>\n<h2>Training an Eagle3 speculator for Cascade 2</h2>\n<blockquote>\n  <p>Open-source artifacts:</p>\n  <ul>\n  <li>🤗 Huggingface model: <a href=\"https://huggingface.co/chankhavu/Nemotron-Cascade2-30B-A3B-Eagle3-Long-Context\" target=\"_blank\">chankhavu/Nemotron-Cascade2-30B-A3B-Eagle3-Long-Context</a></li>\n  <li>🤗 Huggingface dataset: <a href=\"https://huggingface.co/datasets/chankhavu/c2_eagle3_train\" target=\"_blank\">chankhavu/c2_eagle3_train</a> (2 stage train data)</li>\n  <li>🤗 Huggingface dataset: <a href=\"https://huggingface.co/datasets/chankhavu/c2-traces-10197\" target=\"_blank\">chankhavu/c2-traces-10197</a> (collected reasoning traces on 10'197 hard math problems)</li>\n  </ul>\n  <p>Code: <a href=\"https://github.com/hav4ik/SpecForge-Nemotron3/tree/nemotron-cascade-2-experiments\" target=\"_blank\">SpecForge-Nemotron3</a>.</p>\n</blockquote>\n<p>To our knowledge this is the first Eagle3 draft head trained against a hybrid Mamba-Transformer verifier that's verified end-to-end with vLLM. We forked <a href=\"https://github.com/sgl-project/SpecForge\" target=\"_blank\">SpecForge</a> as <a href=\"https://github.com/hav4ik/SpecForge-Nemotron3/tree/nemotron-cascade-2-experiments\" target=\"_blank\">SpecForge-Nemotron3</a> and added the engineering needed to fit long-context training (<code>L=65536, ttt=6</code>) on 2× RTX 6000 Pro Blackwell (96 GB each).</p>\n<h3>Data and recipe</h3>\n<p>Two stages: a broad SFT bootstrap on ~20K conversations (378M untruncated tokens, from Cascade 2's supervised-finetune distribution) followed by on-policy fine-tuning on ~9.7K Cascade 2 reasoning traces (207M tokens) — <strong>~292M loss-masked tokens total</strong>. We prune the 131K target vocab to a 32K draft vocab via top-K token frequency on the loss-masked positions, which keeps 99.77% coverage with ~4× smaller embedding / lm_head. Stage 1 uses LR 1e-4 with length-bucketed sampling (eliminates straggler-rank waste at <code>batch_size=1</code>); stage 2 drops to LR 5e-5 and disables bucketing for gradient diversity.</p>\n<h3>Engineering</h3>\n<p>Two classes of fixes were required to get this training loop working at long context.</p>\n<p><strong>Correctness — FP32 SSM scan state.</strong> Upstream <code>mamba_ssm</code> 2.3.1 silently downcasts the SSM scan boundary state to BF16 inside its Triton kernel. We monkey-patch it to force FP32, bit-equivalent to vLLM 0.19's NemotronH default. Without this, training converges to a worse minimum that costs ~10% absolute on AIME-class math.</p>\n<p><strong>Memory footprint — fitting L=65k on 96 GB.</strong> Naïve training peaks north of 120 GiB (OOM on 96 GB). Four independent activation-memory cuts on the draft forward + TTT unroll (chunked fused linear + soft-target CE for the lm_head, freeing the verifier's 131k-vocab logits before TTT unrolling, Liger-style chunked MLP, sliding-window attention on the draft) bring peak to <strong>~88–91 GiB</strong>, with the single biggest win being the verifier-logit release. Step time at L=65k ttt=6 is ~4 s/step (high variance across sequence lengths). Separately, a 100× speedup in the loss-mask parser that was otherwise re-tokenizing each conversation <code>num_turns</code> times. Full details in <em>Appendix C</em>.</p>\n<h3>Results</h3>\n<p>Temperature sweep, mean accepted tokens per step (on AIME-25):</p>\n<table>\n<thead>\n<tr>\n<th>K</th>\n<th>T=0.0</th>\n<th>T=0.7</th>\n<th>T=0.8</th>\n<th>T=0.9</th>\n<th>T=1.0</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>K=4 (out of K+1=5)</td>\n<td><strong>3.33</strong></td>\n<td>3.00</td>\n<td>3.05</td>\n<td>2.88</td>\n<td>2.91</td>\n</tr>\n<tr>\n<td>K=7 (out of K+1=8)</td>\n<td><strong>3.55</strong></td>\n<td>3.26</td>\n<td>3.14</td>\n<td>3.03</td>\n<td>2.90</td>\n</tr>\n</tbody>\n</table>\n<p>Lower temperature → draft head is more confident and its proposals track the target's argmax more closely, so acceptance climbs. You can find more information in <em>Appendix C</em>.</p>\n<h2>Cascade 2 searches the web when it's desperate</h2>\n<p>The sandbox we gave Cascade 2 for tool use was a <code>stateful_python_code_exec</code> Jupyter kernel with network egress not blocked — <code>urllib</code>, <code>requests</code>, and <code>pip install</code> all worked. On 10,197 rollouts against IMO-class problems, Cascade 2 issued real HTTP calls in <strong>246 traces</strong> (~2.4%).</p>\n<p>Top destinations (HTTP URL literals in tool-call code, per-trace dedup), by category:</p>\n<ul>\n<li><strong>Reference sites</strong> — <code>artofproblemsolving.com</code> (64), <code>en.wikipedia.org</code> (58), <code>oeis.org</code> (28), <code>mathworld.wolfram.com</code> (6)</li>\n<li><strong>Web search</strong> — <code>google.com</code> (40), <code>duckduckgo.com</code> (39), <code>bing.com</code> (3)</li>\n<li><strong>Q&amp;A archives</strong> — <code>math.stackexchange.com</code> (13), <code>api.stackexchange.com</code> (12), <code>stackoverflow.com</code> (3)</li>\n<li><strong>Competition archives</strong> — <code>imo-official.org</code> (11), <code>purplecomet.org</code> (5)</li>\n<li><strong>Misc</strong> — <code>raw.githubusercontent.com</code> (8), <code>arxiv.org</code> (3), <code>api.github.com</code> (3)</li>\n</ul>\n<p>The dominant pattern is AoPS wiki lookups by year/contest/number and Wikipedia for theorem statements. OEIS (28 traces) is usually queried with a computed-prefix sequence. DuckDuckGo Instant Answer API and <code>google.com/search</code> GETs with the prompt pasted verbatim show up often enough to be a recognizable tactic.</p>\n<p>A few representative (amusing) traces:</p>\n<ul>\n<li><strong>IMO 2021 Shortlist PDF</strong> — <code>urllib.request.urlopen</code> against <code>imo-official.org</code>, written to disk (byte-exact match to the real PDF), <code>PyPDF2.PdfReader</code> over every page, substring-grep for prompt keywords. Extraction worked; the problem wasn't in the document. Answer happened to be 0 → coincidental match.</li>\n<li><strong>Purple Comet 2023 HS Final</strong> — <code>requests.get(...)</code>, saved response, tried <code>PyPDF2</code> / <code>pdftotext</code> / <code>fitz</code> / <code>pdfminer</code> — all four failed with <code>EOF marker not found</code> / <code>No /Root object</code>. First 100 bytes of the \"PDF\" were <code>&lt;!DOCTYPE html&gt;</code>; the server returned an HTML error page with a <code>.pdf</code> extension. Wrong answer.</li>\n<li><strong>IMO shortlist in-memory grep</strong> — fetched <code>IMO2021SL.pdf</code> and <code>IMO2022SL.pdf</code> into memory via <code>response.read()</code>, never persisted, <code>grep_bytes(b'Mario')</code>. Problem was IMO 2022 SL C2 (\"Mario and Bowser\"). Wrong answer.</li>\n<li><strong>purplecomets.org</strong> — <code>!wget</code> with a typo (trailing <code>s</code>), DNS fails.</li>\n</ul>\n<p>Network-using traces end up at <strong>34.6%</strong> correct — these are the IMO-level problems the model couldn't crack with direct reasoning in the first place, and no venue in the breakdown meaningfully changes the picture (wikipedia 36.6%, AoPS 27.9%, OEIS 27.0%, Google search 42.9% at the top). That said, the model <strong>recognizing a problem as a benchmark item</strong> and reaching for the right contest archive is a contamination signal on its own, independent of whether the lookup paid off.</p>\n<p>We reported this to the Nemotron team (<a href=\"https://huggingface.co/nvidia/Nemotron-Cascade-2-30B-A3B/discussions/25#69e67838889c53e909054431\" target=\"_blank\">HF discussion</a>) and released the verified traces: 🤗 <a href=\"https://huggingface.co/datasets/chankhavu/nemotron-cascade2-cheating-attempts\" target=\"_blank\">chankhavu/nemotron-cascade2-cheating-attempts</a> (173 rows). For our own runs since, we block egress at the sandbox namespace level.</p>\n<hr>\n<h1>Back to Kaggle AIMO3</h1>\n<p>With the open-source artifacts above in hand — the soft-cloned FP8 Cascade 2, the vLLM PC+SD fix, the Eagle3 speculator — we now turn to the competition itself: how these pieces line up against gpt-oss-120b on the benchmarks, what we actually submitted, and which harnesses we tried but didn't ship.</p>\n<h2>Evaluation set</h2>\n<blockquote>\n  <p>We publish the dataset construction notebook here: <a href=\"https://www.kaggle.com/code/chankhavu/local-validation-benchmark\" target=\"_blank\">https://www.kaggle.com/code/chankhavu/local-validation-benchmark</a>.</p>\n  <p>All throughput analysis in this section are based on the model alone, without speculative decoding.</p>\n</blockquote>\n<p>We construct a candidate pool of 63 problems by combining the 10 official AIMO3 reference problems with 53 hard problems from <a href=\"https://imobench.github.io/\" target=\"_blank\">IMO-AnswerBench</a> — specifically those where gpt-oss-120b scored 6/8 or less on avg@8, restricted to answers in the Kaggle 0–99999 integer range. The pool draws from IMO Shortlists, USAMO, HMMT, Sharygin, RMM, and national TSTs (Vietnam, China, Korea, Japan). We then take a 50-problem subset to match the Kaggle Leaderboard setting used for local validation above.</p>\n<p>For the gpt-oss-120b tool-use accuracy stats, we use the <a href=\"https://www.kaggle.com/datasets/yiyangzheng/answerbench-gpt-oss\" target=\"_blank\">answerbench-gpt-oss</a> dataset by <a href=\"https://www.kaggle.com/yiyangzheng\" target=\"_blank\">@yiyangzheng</a> — they were among the top contenders on the LB, so their inference pipeline is trustworthy.</p>\n<blockquote>\n  <p><strong>Note on contamination.</strong> IMO-AnswerBench is itself drawn from previously published IMO and IMO-Shortlist problems (and other national olympiads), rewritten so that the answer is an integer in a fixed range. Both gpt-oss-120b and Cascade 2 were trained on data that almost certainly includes some of these problems in their original form, so contamination is a real concern. Cascade 2 in particular was released <em>after</em> IMO-AnswerBench was, which expands the contamination window. We did not test for contamination on our side; we chose to trust the data-curation procedure described in the Nemotron-Cascade 2 paper, which uses n-gram-based deduplication against known evaluation benchmarks. We flag this so the reader can weight the absolute accuracy numbers accordingly; the relative comparisons between solvers are less affected because both candidates draw from the same potentially-contaminated training pool.</p>\n</blockquote>\n<h2>Offline benchmark</h2>\n<blockquote>\n  <p>We published all reasoning traces collected for offline benchmark: <a href=\"https://www.kaggle.com/datasets/chankhavu/benchmark-traces\" target=\"_blank\">chankhavu/benchmark-traces</a></p>\n</blockquote>\n<p>All three candidates run through the same harness on the 63-problem evaluation set (8 rollouts per problem, tool use enabled, same prompt template): gpt-oss-120b, our FP8-quantized Cascade 2, and the <a href=\"https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16\" target=\"_blank\">Nemotron-3-Nano-30B-A3B</a>.</p>\n<h3>Accuracy and tool use</h3>\n<p>Metric definitions used throughout this section: <strong>Accuracy</strong> is avg@1 (probability a single rollout is correct, averaged over all 504 rollouts). <strong>Majority vote</strong> is maj@8 (the most-common of 8 parallel rollouts matches the expected answer). <strong>Pass@8</strong> is the probability that <em>at least one</em> of 8 rollouts is correct (the upper bound an oracle verifier could reach).</p>\n<table>\n<thead>\n<tr>\n<th>Model</th>\n<th>Accuracy</th>\n<th>Majority vote</th>\n<th>Pass@8</th>\n<th>No-answer</th>\n<th>Avg gen tok</th>\n<th>Avg tools/sess</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>gpt-oss-120b</td>\n<td>44.0% (222/504)</td>\n<td>52.4% (33/63)</td>\n<td><strong>77.8%</strong> (49/63)</td>\n<td>8.9% (45/504)</td>\n<td><strong>26k</strong></td>\n<td>20.7 (7% err)</td>\n</tr>\n<tr>\n<td>nano30b</td>\n<td>41.5% (209/504)</td>\n<td>52.4% (33/63)</td>\n<td>63.5% (40/63)</td>\n<td><strong>1.6%</strong> (8/504)</td>\n<td>47k</td>\n<td>14.2 (6% err)</td>\n</tr>\n<tr>\n<td>cascade2</td>\n<td><strong>47.0%</strong> (237/504)</td>\n<td><strong>55.6%</strong> (35/63)</td>\n<td>74.6% (47/63)</td>\n<td>2.2% (11/504)</td>\n<td>34k</td>\n<td><strong>11.5</strong> (7% err)</td>\n</tr>\n</tbody>\n</table>\n<p>Cascade 2 leads on both accuracy and voting with half the tool calls — more sample-efficient when it does reach for Python. gpt-oss-120b's high no-answer rate comes from hard problems where it runs out of context mid-reasoning.</p>\n<h3>Generation speed on 1× H100 SXM</h3>\n<table>\n<thead>\n<tr>\n<th></th>\n<th>1 sequence</th>\n<th>4 sequences</th>\n<th>8 sequences</th>\n<th>12 sequences</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>gpt-oss-120b, mxfp4, kv8</td>\n<td>175.4 tok/s</td>\n<td>506.4 tok/s</td>\n<td>810.9 tok/s</td>\n<td>842.8 tok/s</td>\n</tr>\n<tr>\n<td>Cascade 2, fp8, kv8</td>\n<td>301.6 tok/s</td>\n<td>859.0 tok/s</td>\n<td>1333.5 tok/s</td>\n<td>1487.8 tok/s</td>\n</tr>\n</tbody>\n</table>\n<p>We use the FP8 Cascade 2 variant because H100 supports FP8 natively. Cascade 2 is faster at every batch size, and the gap widens at higher concurrency: gpt-oss-120b's throughput plateaus by 8 sequences, while Cascade 2 keeps scaling.</p>\n<h3>Test-time scaling</h3>\n<p>Two views of how each model scales with more compute per problem. <strong>pass@k</strong> = prob. ≥1 of k rollouts is correct; <strong>maj@k</strong> = probability that the most-common answer across k rollouts is correct. Methodology (x-axis conversion, truncation rule, tokenizer choice) in <em>Appendix D</em>.</p>\n<p><strong>Time budget.</strong> Each dot is k ∈ {1, …, 8}; the x-axis converts k into wall-clock seconds per problem on 1× H100 SXM at 8 parallel sequences. Each model's dots are spaced differently because their per-rollout cost differs: Cascade 2 is the cheapest per rollout, so its curve packs the most to the left.</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2F0f085dcf830df3ae82dd1dcdd2c87eeb%2Ftest_time_scaling.png?generation=1779061888745661&amp;alt=media\" alt=\"Test-time scaling vs wall-clock seconds per problem at 8 parallel sequences. Each dot is k ∈ {1, …, 8}.\"></p>\n<p><strong>Within the tested budget, Cascade 2 wins both pass@k and maj@k</strong> — it's the more consistent choice for the actual competition setting. It leads on maj@k across the whole range, <em>and</em> it wins pass@k at every plotted budget because it's cheaper per rollout and gets to fit more rollouts in for any given wall-clock. gpt-oss-120b's pass@k curve, however, has a <em>higher upper ceiling</em> — its rollouts are more diverse (helps pass@k, hurts plurality voting), so given more attempts / a larger budget than this chart covers, an oracle verifier picking the right rollout would eventually prefer gpt-oss-120b. That higher pass@k ceiling is what makes gpt-oss-120b interesting <em>with</em> an external verifier (see <em>Self-verification experiments</em> below); within the time budget of this competition, Cascade 2 dominates on both axes.</p>\n<p>A practical note on that pass@k advantage: realizing it would require an external verifier picking from gpt-oss-120b's rollouts, and Kaggle's resource constraints (5 h on 1× H100 SXM) make that infeasible in either obvious form — we cannot fit a <em>separate</em> verifier model alongside gpt-oss-120b's ~120 GB MXFP4 weights on a single H100, and using gpt-oss-120b <em>as its own</em> verifier doubles the per-problem token bill on a budget that already only fits ~8 solution rollouts per problem. Cascade 2's smaller footprint and faster throughput leave headroom for the self-verification experiments below; gpt-oss-120b does not.</p>\n<p><strong>Reasoning-time budget.</strong> Left panel plots avg@1, right panel maj@8, both as a function of how long a single rollout is allowed to reason (wall-clock seconds on 1× H100 SXM at 8 parallel sequences). Each dot is the result of capping the stored rollouts at a different token length, retaining the last <code>\\boxed{}</code> within the cap as that rollout's answer. This is the \"one longer chain of thought\" axis, complementing the \"more parallel rollouts\" time-budget chart above.</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2Fc32a3b368450b06dd6948c37448d9330%2Ftest_time_scaling_length.png?generation=1779061923631429&amp;alt=media\" alt=\"Test-time scaling vs average effective reasoning time per rollout at 8 parallel sequences. Each dot is a truncation L ∈ {16k, 32k, 48k, 64k, 80k, 96k}.\"></p>\n<p>Cascade 2 dominates on both panels. It reaches its ceiling (47.0% avg@1 / 57.1% maj@8) at ~207 s per rollout, while gpt-oss-120b tops out at ~313 s with a lower ceiling (44.0% / 52.9%) — gpt-oss reasons compactly in tokens, but its slower throughput costs it ~50% more wall-clock time. nano30b traces Cascade 2's shape shifted right by ~70 s: same throughput, longer traces — exactly the gap soft-cloning was meant to close.</p>\n<p><em>How the x-axis is built.</em> For each truncation L ∈ {16k, 32k, 48k, 64k, 80k, 96k} we cut each rollout at L tokens, measure the <em>effective reasoning length</em> (the <code>\\boxed{}</code> position, or L if the model didn't stop), and convert tokens → seconds using the model's per-sequence throughput at 8 parallel sequences on 1× H100 SXM (101 tok/s/seq for gpt-oss-120b; 167 tok/s/seq for Cascade 2 and nano30b, which share a tokenizer and architecture).</p>\n<h3>Our baseline (and final LB) submission</h3>\n<blockquote>\n  <p>Kaggle notebook: <a href=\"https://www.kaggle.com/code/chankhavu/c2-distill-vllm018-watchdog\" target=\"_blank\">chankhavu/c2-distill-vllm018-watchdog</a>\n  Model: <a href=\"https://www.kaggle.com/models/chankhavu/c2-legaldistill-30b-a3b-fp8/transformers/fp8/1\" target=\"_blank\">chankhavu/c2-legaldistill-30b-a3b-fp8</a></p>\n</blockquote>\n<p>The solver we actually submitted is compact: no self-verification, no external tools beyond Python, no learned verifier. Five mechanisms wrapped around the FP8 soft-clone:</p>\n<ol>\n<li><strong>Adaptive per-problem budget.</strong> For each problem we set <code>budget = min(900, max(300, time_left − 300 × problems_left_others))</code>. Early problems get the full 900 s; as the wall clock runs down we reserve 300 s per remaining problem and squeeze the current one to the 300 s floor.</li>\n<li><strong>8 parallel rollouts.</strong> Each problem is attacked by 8 independent rollouts on a 16-worker thread pool (over-provisioned so that early-stop cancellations don't stall new launches). All 8 share the same prompt at temperature 1.0, top_p 0.95 — diversity comes from sampling alone.</li>\n<li><strong>Python tool loop.</strong> Inside each rollout, up to 128 turns of: stream a chat completion with native function-calling, execute any <code>stateful_python_code_exec</code> calls in a stateful IPython sandbox (30 s per call, 1000-char output cap, <code>math/numpy/sympy/itertools/collections/mpmath</code> preimported with <code>mpmath.mp.dps = 64</code>), append results as <code>tool</code> messages, continue. The rollout returns the integer inside the last <code>\\boxed{…}</code> when the model stops calling tools.</li>\n<li><strong>Early stop on 4-vote consensus.</strong> The moment any integer has been emitted by 4 rollouts, we set a stop event, cancel the remaining futures, and move on.</li>\n<li><strong>Entropy-weighted majority vote.</strong> Each rollout's weight is <code>1 / mean_token_entropy</code>, where the per-token entropy is computed from top-5 logprobs over the generation. The winning answer is the one with the largest sum of rollout weights.</li>\n</ol>\n<p>Unfortunately, we only finished our Eagle3 work on the last 2 days of the competition, so we never had a clean submission with only Eagle3, without other harnesses that degrades the results (like self-verification or increased number of parallel rollouts).</p>\n<h2>Online leaderboard simulation</h2>\n<blockquote>\n  <p>The analysis in this section is based on collected traces from simulated online LB runs (50 problems per run, 5h per run, 1x H100 SXM). The data is published here: <a href=\"https://www.kaggle.com/datasets/chankhavu/aimo50-evals/data\" target=\"_blank\">chankhavu/aimo50-evals</a>.</p>\n</blockquote>\n<p>To sanity-check how the offline numbers translate into the actual Kaggle setup, we replayed our final Cascade 2 voting submission and the gpt-oss-120b 44/50 public-notebook baseline on three independent shuffles of the same 50 problems, each under the real Kaggle budget: 5 hours wall-clock on 1× H100 SXM with an adaptive per-problem budget that starts at 900 s and can be squeezed down to 300 s as the wall clock runs low.</p>\n<p>These 50 problems are a subset of the 63 problems in our offline benchmark section. We removed problems with <code>pass@8 = 0</code> for all 3 models: gpt-oss-120b, Nemotron Cascade 2, and Nemotron 3 Nano (so, basically impossible removed problems that were never solved).</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2F0cc893180cba5bf84e4cf4ff7916fdbf%2Fkaggle_time_budget.png?generation=1776888430311498&amp;alt=media\" alt=\"Left: cumulative correct answers (out of 50) over wall-clock time, averaged across 3 shuffles (thin lines are the individual shuffles). Right: the per-problem budget the adaptive scheduler allocated to each problem as the run progressed — starts at 900 s and drops toward the 300 s floor when wall-clock time gets tight.\"></p>\n<p>Two readings. <strong>(i)</strong> Cascade 2 (mean <strong>34.7/50</strong>) is ahead of gpt-oss-120b (mean <strong>33.0/50</strong>) on the LB scoreboard from the ~2h mark onward, and reaches gpt-oss-120b's <em>final</em> score about 20 min earlier in the average run. <strong>(ii)</strong> gpt-oss-120b's per-problem budget gets visibly squeezed toward the end: on aimo50c the last 5 problems ran at 300 s (the minimum), because the notebook was within minutes of the 4.83h ceiling. Cascade 2 held 900 s per problem for most of the run across all three shuffles, with plenty of headroom at the finish.</p>\n<h3>Per-problem breakdown</h3>\n<p>Three per-problem views of the 150 (problem × shuffle) pairs:</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2F70f9e23ae071319d893dd81e92f91db5%2Fkaggle_per_problem_stats.png?generation=1779061973301798&amp;alt=media\" alt=\"Per-problem analysis. Left: correctness bucketed by the number of distinct answers the 8 rollouts produced. Middle: 2×2 agreement matrix between the two notebooks on matched problem-shuffle pairs. Right: total wall-clock time broken down into generation, successful tool call, Python-error tool call, and timed-out tool call (per-call sandbox timeout = 30 s in both solvers).\"></p>\n<p><strong>Left — vote convergence predicts correctness.</strong> When all 8 rollouts agree (bin 1), Cascade 2 is correct 82.8% of the time vs gpt-oss-120b's 74.6% — the online equivalent of the offline \"consistency → correctness\" story. Cascade 2 never produces a zero-answer case; gpt-oss-120b has 9 of 150 (all wrong). At 4+ distinct answers (both models stumped), Cascade 2 is slightly worse but has more such cases (30 vs 14), so the bin averages understate its overall advantage.</p>\n<p><strong>Middle — which model actually matters on 29% of pairs.</strong> 53% both right, 18% both wrong, 16% Cascade-2-only right, 13% gpt-oss-only right. Net +5 pair-wise advantage to Cascade 2. Roughly half the LB score is \"easy for everyone\" and one-fifth is \"hard for everyone\"; the remaining third is where model choice actually moves the needle.</p>\n<p><strong>Right — where the wall-clock time actually goes.</strong> Decomposing total wall time across all 3 shuffles into four buckets — generation, successful tool call, Python-error tool call, timed-out tool call — Cascade 2 spends <strong>88% in generation</strong> vs gpt-oss-120b's <strong>74%</strong>, and <strong>6.3% of Cascade 2's time vs 14.4% of gpt-oss-120b's is wasted on tool calls that produced no useful output</strong> (~2.2× more for gpt-oss-120b). For both solvers, <strong>the wasted time is almost entirely pure 30 s timeouts</strong> (12.5% / 5.9% of wall-clock) rather than Python errors (1.9% / 0.4%) — gpt-oss-120b's tool calls just trip the 30 s sandbox cap roughly 2× more often per rollout than Cascade 2's. <em>Appendix G</em> dives into the per-error-class breakdown and the most common root causes (forgotten imports, sandbox state rollback after a cell error).</p>\n<h3>Head-to-head: the two finalists</h3>\n<p>Cascade 2 voting vs the gpt-oss-120b baseline, log-derived:</p>\n<table>\n<thead>\n<tr>\n<th><strong>Submission</strong><br>(model, harness)</th>\n<th><strong>Local validation</strong><br>(3 shuffles)</th>\n<th><strong>Local val</strong><br>(mean)</th>\n<th><strong>Tool err</strong><br>(per-call)</th>\n<th><strong>Public LB attempts</strong></th>\n<th><strong>Private LB</strong></th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>gpt-oss-120b, 44/50 public notebook</td>\n<td>33, 30, 36</td>\n<td>33.0</td>\n<td>15.0%</td>\n<td>38, 38, 37</td>\n<td>—</td>\n</tr>\n<tr>\n<td><strong>Cascade 2, 8 rollouts, voting<br>(our final submission)</strong></td>\n<td><strong>36, 34, 34</strong></td>\n<td><strong>34.7</strong></td>\n<td><strong>10.6%</strong></td>\n<td><strong>36, 39, 38</strong></td>\n<td><strong>41</strong></td>\n</tr>\n</tbody>\n</table>\n<p>Per-call tool error rates count any call that produced no useful output — Python errors and 30 s sandbox timeouts together.</p>\n<p>The fact that the results are in similar ballpark as Public LB (30 - 36) is pure coincidence - we did not cherry-pick the problems for our benchmark in any way. It means that the Public LB are really on the same difficulty level as the IMO-AnswerBench, which is a testament of how far we've come since AIMO1!</p>\n<p>Cascade 2 wins local validation by 1.7 points and matches gpt-oss-120b on the LB mean (both 37.7), peaking higher (39 vs 38). Both finalists score meaningfully higher on the LB than on our local mean (gpt-oss +4.7, Cascade 2 voting +3.0), which suggests IMO-AnswerBench (our local set) is slightly harder than the hidden LB set. The self-verification harnesses we tried — folded into the full comparison table at the end of the <code>Self-verification experiments</code> section below — both underperformed plain voting.</p>\n<h2>Self-verification experiments</h2>\n<blockquote>\n  <ul>\n  <li>Notebook we submitted to Public LB: <a href=\"https://www.kaggle.com/code/chankhavu/c2-distill-verifier?scriptVersionId=311859342\" target=\"_blank\">https://www.kaggle.com/code/chankhavu/c2-distill-verifier?scriptVersionId=311859342</a></li>\n  <li><strong>Caveat up front.</strong> We built and evaluated the entire self-verification harness within the last 24 hours before the competition deadline — no time to tune any of the hyperparameters (pair-selection policy, grader prompt, max-verifications cap, temperature, rating scale, μ-score weighting, fallback thresholds). Treat the numbers below as <em>first-attempt</em> measurements, not a carefully-optimised upper bound on what self-verification can do.</li>\n  </ul>\n</blockquote>\n<p>The voting harness leaves an obvious source of slack: on most problems the 8 solution rollouts finish at wildly different times. The first rollout drops its <code>\\boxed{}</code> at 100–200 s, the slowest can take 500+ s, and in between we're sitting on GPU capacity we've already paid for. The self-verification variants from the table above use that capacity to run pairwise <em>grader</em> sessions that rate competing answers against each other.</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2Fd71a07030d85e1049dbf373179fd355d%2Fkaggle_self_verification_gantt.png?generation=1776888538729281&amp;alt=media\" alt=\"Left: Gantt of one problem (correctly answered 13). 8 solution rollouts finish between 75 s and 334 s at a 3-3-2 answer split — &lt;code&gt;1&lt;/code&gt; and &lt;code&gt;12&lt;/code&gt; tied at three votes each, with the correct answer &lt;code&gt;13&lt;/code&gt; as a 2-vote minority. Rows sorted fastest-first. Right: the pairwise tournament the grader ran (three matches, ratings out of 10), the resulting standings, and — for reference — what entropy-weighted voting alone would have picked. The matchup graph leaves &lt;code&gt;13&lt;/code&gt; as the only undefeated bucket; voting would have confidently picked &lt;code&gt;12&lt;/code&gt; (score 5.13 &gt; 4.71 &gt; 3.55), which is wrong.\">\n</p><p>We borrow the core idea — pairwise LLM-as-judge comparisons aggregated into an uncertainty-weighted score — from <a href=\"https://harmandotpy.github.io/v1-verification/\" target=\"_blank\">V1: Toward Verifiable Reasoning in LLMs</a> by Harman et al. Our pair-selection policy differs: V1 picks the next pair adaptively to maximise information gain, while we use a simpler greedy rule (newest answer vs current undefeated bucket). Our winner-selection scoring stays close to theirs.</p>\n<h3>Algorithm</h3>\n<p>On top of the voting harness (same model, same sampling, same adaptive budget, same early-stop at four agreeing rollouts), the self-verification variants add three conceptual pieces:</p>\n<ol>\n<li><strong>Pairwise grader.</strong> Given the problem and two candidate solutions A and B, the model (at temperature 0) is asked to produce an integer correctness rating in 1–10 for each side. Higher rating wins; equal ratings are a tie. (The full prompt is in Appendix E.)</li>\n<li><strong>Matchup tracker.</strong> Rollout answers are grouped into <em>buckets</em> by value (duplicate answers share a bucket). The tracker maintains a directed win/loss graph over buckets, not individual rollouts.</li>\n<li><strong>Winner = undefeated bucket.</strong> After all solutions and verifications complete, we pick the bucket that no other bucket beat, with tie-breaking described below.</li>\n</ol>\n<h3>Scheduling policy: filling GPU bubbles</h3>\n<p>Each time a solution rollout completes, the harness attempts to launch up to two verifications in whichever GPU slots are free, subject to a cap on concurrent rollouts. The tracker picks the next comparison in priority order:</p>\n<ol>\n<li>The newest answer against a currently undefeated bucket it hasn't yet faced (challenger vs champion) — this is the greedy rule we use in place of V1's information-gain-maximising pick.</li>\n<li>Two undefeated buckets against each other, to break ties at the top of the graph.</li>\n<li>Any pair of buckets that haven't yet been compared, as a fallback.</li>\n</ol>\n<p>Completed verifications also re-trigger a new comparison if there's still wall-clock budget and solution rollouts are still arriving. Verifications are capped at a fixed maximum per problem so they can't swallow the solver's budget on pathological cases.</p>\n<h3>Scoring and winner selection</h3>\n<p>Let <em>undefeated</em> be the set of buckets with zero losses in the win/loss graph. If exactly one bucket is undefeated, we pick it — done. If multiple remain undefeated (or none, in the cycle case), we rank them by an uncertainty-weighted score</p>\n<p>$$\\mu_i = \\frac{\\sum_j w_{ij} \\cdot v_{ij}}{\\sum_j w_{ij}}\\,,$$</p>\n<p>where $v_{ij} \\in {1, \\tfrac{1}{2}, 0}$ is the outcome (win / tie / loss) of bucket <em>i</em> against bucket <em>j</em>, and $w_{ij} = \\max(|r_i - r_j| / 9,\\, 0.1)$ scales each comparison by its rating margin: a 10–1 verdict carries an order of magnitude more weight than a 5–4 one, with a floor of 0.1 so ties still count. Further ties break by bucket size (number of rollouts that produced that answer), then by comparison count. If fewer than half the distinct answer buckets have been graded at all, we treat the grading as too sparse to trust and fall back to the entropy-weighted majority vote from the baseline voting solver.</p>\n<p>On the problem in the chart, voting has nothing useful to say: three rollouts say <strong>1</strong>, three say <strong>12</strong>, only two say <strong>13</strong> — the correct answer. Voting breaks the tie by entropy-weighted score, picks <code>12</code> (score 5.13 vs <code>1</code>'s 4.71 and <code>13</code>'s 3.55), and is wrong. The grader only needs three pairwise comparisons to settle it (13 beats 1 with 10–1, 13 beats 12 with 9–2, and 12 beats 1 with 3–1), leaving <code>13</code> as the only undefeated bucket. This is the regime self-verification is <em>supposed</em> to win: when a single well-reasoned rollout can recognise a majority of sloppy rollouts for what they are.</p>\n<h3>Results: why it didn't win</h3>\n<p>Despite the elegance, the table tells the disappointing story: self-verification underperformed voting on both local validation (32.0 / 33.0 vs 34.7) and public LB (34 vs 36/39/38). We tried two configurations — 8 solution rollouts with verification layered on top, and 6 solution rollouts with more verification budget per problem — and neither beat plain voting.</p>\n<p><strong>When does the grader even do something?</strong> Across all 297 problem-shuffles we ran under self-verification (three variants × two shuffles × ~50 problems), the grader agrees with entropy-weighted voting's top answer <strong>84.5 % of the time (251 / 297)</strong> — meaning on 5 out of every 6 problems the verification layer adds literally nothing. On the remaining 15.5 % (46 cases) the grader flips the decision, and its hit rate on those flips is what really matters:</p>\n<table>\n<thead>\n<tr>\n<th>Harness</th>\n<th>Flips</th>\n<th>Grader right<br>(voting wrong)</th>\n<th>Grader wrong<br>(voting right)</th>\n<th>Both wrong</th>\n<th>Net</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Cascade 2, 8 rollouts, self-verification</td>\n<td>11</td>\n<td>6</td>\n<td>3</td>\n<td>2</td>\n<td><strong>+3</strong></td>\n</tr>\n<tr>\n<td>Cascade 2, 6 rollouts, self-verification</td>\n<td>11</td>\n<td>6</td>\n<td>2</td>\n<td>3</td>\n<td><strong>+4</strong></td>\n</tr>\n<tr>\n<td>Cascade 2 + Eagle3, 6 rollouts</td>\n<td>24</td>\n<td>10</td>\n<td>10</td>\n<td>4</td>\n<td><strong>0</strong></td>\n</tr>\n<tr>\n<td><strong>All self-verif (aggregate)</strong></td>\n<td><strong>46</strong></td>\n<td><strong>22</strong></td>\n<td><strong>15</strong></td>\n<td><strong>9</strong></td>\n<td><strong>+7 / 297</strong></td>\n</tr>\n</tbody>\n</table>\n<p>So on the two non-Eagle3 runs the grader is net positive but only barely — +7 correct answers across 297 problem-shuffles, roughly 2.4 %. The Eagle3 run is the real tell: its 33 % throughput headroom lets the scheduler fire <em>more than twice as many</em> flip-candidate comparisons (24 vs 11), and the extra firings split right down the middle — ten wins cancelled by ten losses. More grading, no net gain. At the margins where voting and the grader disagree, the grader is essentially a coin flip.</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2Fc2f032a216884a70d93e74af479db525%2Fkaggle_self_verification_time_budget.png?generation=1776888592784811&amp;alt=media\" alt=\"Two views of the same 50-problem runs under the 5 h Kaggle-like setting, comparing voting (our final) against two self-verification harnesses — the 8-rollout variant and a 6-rollout variant run with Eagle3 (its numbers have been normalised onto the same 300–900 s / 4.83 h axes as everyone else). Left: per-problem wall-clock time — dots are means across shuffles, bars are ±1 SD. Right: the adaptive budget the scheduler handed each problem, capped between 900 s (default) and 300 s (floor); bold line is the mean across shuffles, faint lines are individual shuffles. Voting stays near the 900 s ceiling for most of the run. Both self-verification variants drop off the 900 s line within the first ten problems and spend most of the run in the 600–700 s band.\"></p>\n<p>Our read: the grader is too lenient to break ties confidently when both answers are backed by plausible-looking reasoning, and — as the chart above makes clear — at 1× H100 SXM there simply isn't enough wall-clock budget to run the 15–20 comparisons per problem the tracker would need to converge on harder problems. The per-problem means are close across all three runs (329 s voting vs 342 s self-verification vs 339 s with Eagle3), but the tails tell a very different story: voting's 90th-percentile problem consumes 472 s, self-verification's reaches 589 s, and with Eagle3 it stretches to 796 s — the longer the grader can keep running comparisons, the longer individual problems drag on, and the scheduler simply squeezes the remaining budget harder. Eagle3 buys roughly a 33 % throughput improvement (our motivation for training it in the first place), but even on that budget the extra complexity of the verification layer didn't translate into more correct answers — the Eagle3 run landed at <strong>33.5 / 50</strong> (mean of 36 and 31), which is no better than the plain self-verification variant and still behind voting's 34.7. The ingredients — fast model, honest grader — were right; the budget wasn't.</p>\n<table>\n<thead>\n<tr>\n<th><strong>Submission</strong><br>(model, harness)</th>\n<th><strong>Local validation</strong><br>(3 shuffles)</th>\n<th><strong>Local val</strong><br>(mean)</th>\n<th><strong>Tool err</strong><br>(per-call)</th>\n<th><strong>Public LB attempts</strong></th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>gpt-oss-120b, 44/50 public notebook</td>\n<td>33, 30, 36</td>\n<td>33.0</td>\n<td>15.0%</td>\n<td>38, 38, 37</td>\n</tr>\n<tr>\n<td>Cascade 2, 8 rollouts, self-verification</td>\n<td>33, 31</td>\n<td>32.0</td>\n<td>5.1%</td>\n<td>34</td>\n</tr>\n<tr>\n<td>Cascade 2, 6 rollouts, self-verification</td>\n<td>35, 31</td>\n<td>33.0</td>\n<td>5.2%</td>\n<td>never submitted</td>\n</tr>\n<tr>\n<td>Cascade 2 + Eagle3, 6 rollouts, self-verification</td>\n<td>36, 31</td>\n<td>33.5</td>\n<td>5.2%</td>\n<td>never submitted</td>\n</tr>\n<tr>\n<td><strong>Cascade 2, 8 rollouts, voting<br>(our final submission)</strong></td>\n<td><strong>36, 34, 34</strong></td>\n<td><strong>34.7</strong></td>\n<td><strong>6.0%</strong></td>\n<td><strong>36, 39, 38</strong></td>\n</tr>\n</tbody>\n</table>\n<hr>\n<h2>Conclusion</h2>\n<p><strong>The boring harness wins.</strong> Our final submission is the simplest thing that works: 8 rollouts of soft-cloned FP8 Cascade 2 with Python tool use, an adaptive 300–900 s per-problem budget, early-stop at 4 agreeing answers, and an entropy-weighted majority vote. On the 3-shuffle local validation (IMO-AnswersBench, 5 h, 1× H100 SXM) it scores <strong>34.7 / 50</strong>, ahead of the public gpt-oss-120b 44/50 notebook's <strong>33.0 / 50</strong>, and it ties gpt-oss-120b on the public LB (mean 37.7, peak <strong>39 / 50</strong> vs 38). Every more-sophisticated Cascade 2 harness we tried — pairwise-comparison self-verification, the same plus Eagle3-accelerated inference — underperformed the baseline. We believe self-verification could beat voting with more tuning, but not in the 24 hours we had before the deadline.</p>\n<p><strong>What the side quests produced.</strong> Three open-source artifacts that outlast the competition: a family of quantized Cascade 2 checkpoints (FP8, NVFP4, AWQ-INT4 — 66K downloads and counting), a vLLM fork that correctly runs prefix caching alongside speculative decoding on hybrid-Mamba models (long-standing open problem), and an EAGLE-3 speculator for Cascade 2 reaching 3.55 mean accepted tokens at K=7 along with the SpecForge modifications needed to train it at 64 k context. We also filed a note on Cascade 2's tendency to quietly search the web for answers through its Python tool.</p>\n<p><strong>Why this is mostly a first-try harness.</strong> We joined the competition late and the Kaggle 1-submission-per-day cap meant essentially every choice we shipped was a first-try call, with no headroom to A/B-test on the hidden LB. That's also why our scheduling, early-stop, and budget policy are deliberately simple: we picked Cascade 2 specifically because it offered the best <strong>maj@k under a time budget</strong>, and our priors from AIMO 2 were that winning solutions in this class of competition optimized maj@k rather than chasing pass@k at infinity — pass@∞ tends to be similar across competitive models, but maj@k under a real time budget is where the consistency premium pays off. A more elaborate per-problem scheduling system was on the wishlist (several contestants reported good gains from one), but the engineering dependency we did spend weeks on — PC + SD for hybrid-Mamba models in vLLM — was already over our time budget, and we judged that getting the simple harness right was a safer use of the remaining days than tuning a scheduler we couldn't validate without using up the submission slots.</p>\n<p><strong>What we'd do differently.</strong> Finalize PC + SD support in vLLM earlier (this was the engineering dependency that would have unlocked a genuinely competitive self-verification harness), start the self-verification work on day one instead of the last 24 hours, spend some time tuning the grader (prompt, rating scale, pair-selection policy), and develop a smarter per-problem scheduling policy that exploits the random hard/easy ordering. The ingredients were right; we just ran out of room to tune them. Even so, we're glad we got side-tracked — the open-source artifacts are the part of this project we're most proud of.</p>\n<hr>\n<h2>References</h2>\n<ul>\n<li>Zhuolin et al. <a href=\"https://arxiv.org/abs/2603.19220\" target=\"_blank\">\"Nemotron-Cascade 2: Post-Training LLMs with Cascade RL and Multi-Domain On-Policy Distillation\"</a> (2026).</li>\n<li>OpenAI. <a href=\"https://arxiv.org/abs/2508.10925\" target=\"_blank\">\"gpt-oss-120b &amp; gpt-oss-20b Model Card\"</a> (2025).</li>\n<li>Natapong Nitarach. <a href=\"https://arxiv.org/abs/2603.27844\" target=\"_blank\">\"Model Capability Dominates: Inference-Time Optimization Lessons from AIMO 3\"</a> (2026).</li>\n<li>Harman et al. <a href=\"https://arxiv.org/abs/2603.04304v1\" target=\"_blank\">\"V1: Unifying Generation and Self-Verification for Parallel Reasoners\"</a> (2026).</li>\n<li>Woosuk et al. <a href=\"https://arxiv.org/abs/2309.06180\" target=\"_blank\">\"Efficient Memory Management for Large Language Model Serving with PagedAttention\"</a>, in <em>SOSP '23: Proceedings of the 29th Symposium on Operating Systems Principles</em> (2023).</li>\n<li>Yuhui et al. <a href=\"https://arxiv.org/abs/2503.01840\" target=\"_blank\">\"EAGLE-3: Scaling up Inference Acceleration of Large Language Models via Training-Time Test\"</a>, in <em>Annual Conference on Neural Information Processing Systems</em> (2025).</li>\n</ul>\n<hr>\n<h1>Appendix</h1>\n<h2>Appendix A: soft-cloning the Cascade 2 model</h2>\n<h3>Question: Why is this legal? Can this be even considered distillation?</h3>\n<ul>\n<li>Semantically and mathematically this is a distillation: the \"students\" weights NEVER optimize directly towards \"teachers\" weights, only through observation of the inputs/outputs $X$ and $Y$ of functions $F_\\text{teacher}(X)$, where $F_\\text{teacher}$ happens to be a linear mapping.</li>\n<li>This is different from \"non-legal\" <a href=\"https://www.kaggle.com/competitions/ai-mathematical-olympiad-progress-prize-3/discussion/680682#3437318\" target=\"_blank\">approach with GPT-OSS-Puzzle-88B in this discussion</a>, because that approach is basically an interpolation $W_\\text{final} = a<em>W_\\text{gptoss88b} + b</em>W_\\text{finetuned}$, which still directly involves the weights of non-legal model. Here, none of the weights of teacher are used in runtime student model.</li>\n</ul>\n<h3>Implementation details</h3>\n<p>The recovery loop runs per tensor, one tensor type at a time. For each teacher tensor we (1) generate random probe inputs $X \\sim \\mathcal{N}(0, 1)$, (2) observe the teacher's output $Y = f_\\text{teacher}(X)$ — this is the only step that touches teacher values — and (3) solve for student parameters that minimise the mean-squared error $\\lVert f_\\text{student}(X) - Y \\rVert^2$. Linear layers $y = W x$ are recovered in closed form via the normal equations (it's already an obvious MSE minimiser and the loop would add ~100× runtime for no benefit). The other three tensor types — depthwise Conv1d kernels, 1D scalar vectors ($y = w \\odot x$; norms, SSM $A_{\\log}$, $d_t$, $D$), and additive biases ($y = x + b$) — all run an explicit SGD loop with a learning rate that cosine-decays to 0 over 500 steps. Max learning rates are exposed as CLI flags; the choices below come from stability arguments on the per-parameter Hessian.</p>\n<p>Each recovery loss is a convex quadratic, and SGD is stable exactly when $\\mathrm{lr} &lt; 2/\\lambda_{\\max}(H)$ with $H$ the per-parameter Hessian. For the $y = w \\odot X$ and $y = X + b$ cases the loss is separable across features, so with the normalisation we use (mean over samples, sum over features) every parameter sees the same scalar Hessian $H = 2\\,\\mathbb{E}[X^2] = 2$ for $X \\sim \\mathcal{N}(0, 1)$ — hence $\\mathrm{lr} = 0.5 \\approx 1/H$ lands each element at its optimum in $O(1)$ effective steps, independent of $N$ and of the magnitude of the target.</p>\n<p>The unfolded-window conv1d Hessian is not a scaled identity. Its $(k_1, k_2)$ entry is the sample autocorrelation of standard noise at lag $|k_1 - k_2|$: diagonal $\\approx T$, off-diagonals have $\\mathcal{O}(\\sqrt{T})$ noise fluctuations (plus small deterministic shifts near the zero-padded boundary of the window). Divided by $T$ in the loss, the spectrum concentrates around $2$ but spreads to roughly $[1.3, 3.5]$ at typical Mamba sizes ($T = K + 32 = 36$, $K = 4$). $\\mathrm{lr} = 0.5$ is right on the stability edge for $C \\lesssim 10^3$ and clearly past it by $C \\gtrsim 4 \\cdot 10^3$ (it diverges with extreme numeric blow-up). $\\mathrm{lr} = 0.3$ sits inside $2 / 3.5 \\approx 0.57$ with comfortable margin across every Cascade 2 size we ran.</p>\n<h2>Appendix B: quantization details</h2>\n<h3>Precision recipe — full layer breakdown</h3>\n<p>The per-component precisions behind the layout chart in the main section. Each row maps an architectural component to the precision it's kept at in the FP8 and NVFP4 checkpoints, following the NVIDIA Nano sensitivity recipe.</p>\n<table>\n<thead>\n<tr>\n<th>Component</th>\n<th>Precision (FP8)</th>\n<th>Precision (NVFP4)</th>\n<th>Comment</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>MoE expert GEMMs (routed + shared)</td>\n<td><strong>FP8</strong></td>\n<td><strong>NVFP4</strong></td>\n<td>All 23 MoE layers, 128 routed + 2 shared experts each</td>\n</tr>\n<tr>\n<td>Mamba GEMMs (non-adjacent)</td>\n<td><strong>FP8</strong></td>\n<td><strong>NVFP4</strong></td>\n<td>17 of 23 Mamba layers</td>\n</tr>\n<tr>\n<td>Attention layers (all 6)</td>\n<td><strong>BF16</strong></td>\n<td><strong>BF16</strong></td>\n<td>Most sensitive — kept BF16 per NVIDIA sensitivity analysis</td>\n</tr>\n<tr>\n<td>Mamba layers adjacent to attention (6)</td>\n<td><strong>BF16</strong></td>\n<td><strong>BF16</strong></td>\n<td>Layers {4, 11, 18, 25, 32, 41} — found sensitive in ablations</td>\n</tr>\n<tr>\n<td>Mamba 1D conv</td>\n<td><strong>BF16</strong></td>\n<td><strong>BF16</strong></td>\n<td>All layers</td>\n</tr>\n<tr>\n<td>Router gates</td>\n<td><strong>FP32</strong></td>\n<td><strong>FP32</strong></td>\n<td>Routing precision must not degrade</td>\n</tr>\n<tr>\n<td>Embeddings &amp; lm_head</td>\n<td><strong>BF16</strong></td>\n<td><strong>BF16</strong></td>\n<td>Not quantized</td>\n</tr>\n<tr>\n<td>KV cache</td>\n<td><strong>FP8</strong></td>\n<td><strong>FP8</strong></td>\n<td>All 6 attention layers</td>\n</tr>\n</tbody>\n</table>\n<h3>Evaluation setup</h3>\n<p>All benchmark runs use <a href=\"https://github.com/NVIDIA-NeMo/Evaluator\" target=\"_blank\">NVIDIA-NeMo/Evaluator</a> with the <a href=\"https://github.com/NVIDIA-NeMo/Evaluator/blob/main/packages/nemo-evaluator-launcher/examples/nemotron/nemotron-3-super/local_nemotron-3-super-120b-a12b_tools.yaml\" target=\"_blank\">Nemotron-3-Super-120B tool-use config</a>: 8 rollouts per problem, temperature 1.0, tool use enabled (sandbox code execution), <code>enable_thinking=true</code>. Same config across the BF16 reference and our FP8 / NVFP4 checkpoints so numbers are directly comparable.</p>\n<blockquote>\n  <p><strong>Critical serving flag.</strong> NemotronH's SSM state must stay in FP32 at inference. vLLM 0.19 reads this from the model's <code>config.json</code> by default (Cascade 2 ships the override), but anyone serving a variant whose config doesn't include it should pass <code>--mamba_ssm_cache_dtype float32</code> explicitly. Leaving the SSM state at BF16 costs <strong>~10% absolute</strong> on AIME-class benchmarks — silent precision regression, no crash. The same applies to SGLang (<code>--mamba-ssm-dtype float32</code>), which defaults to BF16.</p>\n</blockquote>\n<h3>Verification results</h3>\n<table>\n<thead>\n<tr>\n<th>Benchmark</th>\n<th><a href=\"https://huggingface.co/nvidia/Nemotron-Cascade-2-30B-A3B\" target=\"_blank\">Nemotron-Cascade-2-30B-A3B</a><br>(reproduced results)</th>\n<th>Our FP8 model</th>\n<th>Our NVFP4 model</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>AIME 2025 (avg@8)</td>\n<td>98.8</td>\n<td><strong>96.7</strong></td>\n<td><strong>97.9</strong></td>\n</tr>\n<tr>\n<td>AIME 2026 (avg@8)</td>\n<td>94.2</td>\n<td><strong>95.0</strong></td>\n<td><strong>92.1</strong></td>\n</tr>\n<tr>\n<td>HMMT Feb 2025 (avg@8)</td>\n<td>92.9</td>\n<td><strong>93.8</strong></td>\n<td><strong>90.1</strong></td>\n</tr>\n</tbody>\n</table>\n<p><em>With the low sample count (8 rollouts per problem), a deviation of ±2% across runs is expected. Safe to say BF16, FP8, and NVFP4 variants are equivalent in reasoning performance.</em></p>\n<h2>Appendix C: Eagle3 training details</h2>\n<h3>Per-position acceptance</h3>\n<p>At the sweet spot (K=4, T=0.7 on AIME-25):</p>\n<table>\n<thead>\n<tr>\n<th>Position</th>\n<th>1</th>\n<th>2</th>\n<th>3</th>\n<th>4</th>\n<th>5</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Accept rate</td>\n<td>73.3%</td>\n<td>49.8%</td>\n<td>34.5%</td>\n<td>24.7%</td>\n<td>17.3%</td>\n</tr>\n</tbody>\n</table>\n<p>Rates decay geometrically with a ratio of ~0.7 per position — each additional draft token needs all previous drafts to verify first, and the marginal drafter confidence drops with depth.</p>\n<h3>Memory optimizations</h3>\n<p>Naïve training at L=65k, ttt=6 peaks at 120+ GiB — OOM on a 96 GB GPU. Four independent activation-memory cuts on the draft forward + TTT unroll bring peak down to ~88–91 GiB, approximately <code>63 GiB verifier weights + 4.2 GiB target_p_padded + 12 GiB attention activations + 4.2 GiB MLP saves + ~5–8 GiB overhead</code>:</p>\n<ul>\n<li><strong>Free verifier full-vocab logits before TTT unrolling</strong> (biggest single win, ~30 GiB saved across the unroll). The HF target backend returned the verifier's <code>[seq, 131072]</code> logits as <code>target</code> (~16 GiB at L=65k), held through three independent references across the draft forward + backward. Precomputing a small draft-vocab <code>target_p_padded</code> (~4 GiB) and dropping the rest before <code>eagle3_model.forward()</code> releases the full-vocab tensor before the expensive phase. Dropping the <code>.float()</code> upcast on the softmax saves another ~8 GiB.</li>\n<li><strong>Chunked fused linear + soft-target CE</strong> for the draft lm_head (~24 GiB saved across ttt=6 unrolls). The <code>[B, T, V]</code> logits tensor is never materialized — the existing <code>LogSoftmaxLoss</code> Triton kernel runs inside a chunked + grad-checkpointed wrapper. Numerically equivalent to the unchunked path on both <code>hidden_states</code> and <code>lm_head_weight</code> grads, verified at realistic shapes.</li>\n<li><strong>Chunked MLP forward</strong> (Liger-style, sequence-dim chunking; ~3.6 GiB per-step transient reduction). MLP intermediates shrink from ~4.4 GiB to ~0.8 GiB at L=65k, intermediate=8064. Composes cleanly with grad-checkpointing.</li>\n<li><strong>Sliding-window attention on the draft</strong> (modest direct memory win, but unblocks ttt=6 at L=65k by bounding attention from O(L²) to O(L × window)), plumbed through <code>LlamaForCausalLMEagle3</code>, the flex_attention mask generator, and the causal-mask builder. The TTT suffix block is intentionally not windowed.</li>\n</ul>\n<p>The four savings don't sum linearly — some overlap (e.g., freeing target logits shrinks the memory available for future cuts to operate on). Aggregate per-step wall time is ~4 s (1.2 s on short samples up to 8.6 s on near-max-length conversations at L=65k); grad-checkpointed MLP costs ~8–12% per-step recompute overhead, which the memory savings more than pay for.</p>\n<h3>Loss-mask parser (100× speedup)</h3>\n<p><code>GeneralParser.parse()</code> was running per-turn re-tokenization in O(num_turns × seq_len) — for long reasoning traces with many assistant turns this took several seconds per conversation. We replaced it with a single <code>return_offsets_mapping</code> + bisect pass to map character spans to token spans in O(num_turns × log seq_len). Per-conversation preprocessing dropped from several seconds to ~30 ms on long reasoning traces.</p>\n<h2>Appendix D: test-time scaling methodology</h2>\n<p><strong>Attempt-count view.</strong> What the time-budget chart in the body would look like if we drop the per-rollout-cost scaling and plot against raw <code>k</code>. Same data, x-axis = k instead of k × seconds-per-rollout.</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2F02418e1ddd660c78b71374af498f4ef3%2Ftest_time_scaling_attempts.png?generation=1779062011546385&amp;alt=media\" alt=\"pass@k and maj@k vs number of attempts k ∈ {1, …, 8} on the 63-problem offline benchmark.\"></p>\n<p><strong>Time-budget chart.</strong> For each model m and k ∈ {1, …, 8}, we plot the (pass@k, maj@k) point at <code>x = k × avg_tokens_per_rollout(m) / tok-per-sec(m)</code> seconds, using the 8-parallel-sequence throughput from the generation-speed table. pass@k and maj@k are computed exactly (no Monte-Carlo) by enumerating all <code>C(8, k)</code> subsets of the 8 available sessions per problem and averaging across the 63 problems. maj@k uses the most-common answer rule: for each subset, the modal answer wins, ties split uniformly across tied answers.</p>\n<p><strong>Reasoning-time budget chart.</strong> For each truncation length L ∈ {16k, 32k, 48k, 64k, 80k, 96k}:</p>\n<ul>\n<li>Truncate each session's model-generated text at L tokens.</li>\n<li>Find the last <code>\\boxed{…}</code> inside the truncated portion. If found, that token position is the session's <em>effective length</em> and its original predicted answer counts. If not found, the session is treated as \"no answer\" at that L and its effective length is L.</li>\n<li>Mean effective length across the 504 sessions gives the raw token x-coordinate, which we convert to seconds by dividing by the per-sequence throughput at 8 parallel sequences (101.4 tok/s/seq for gpt-oss-120b from 810.9/8; 166.7 tok/s/seq for Cascade 2 and nano30b from 1333.5/8 — nano30b uses Cascade 2's throughput since the architectures are identical). y is avg@1 (fraction of 504 sessions correct after truncation) or maj@8 (most-common-answer rule over the 8 truncated answers per problem, averaged across 63 problems).</li>\n</ul>\n<p><strong>Tokenization.</strong> Per-model, exact:</p>\n<ul>\n<li>gpt-oss-120b: <code>num_completion_tokens</code> from each trace record (the model's own Harmony token count for generated output, exclusive of prompt and tool results). Because gpt-oss emits <code>\\boxed{…}</code> as its final generation, we approximate its <code>\\boxed{}</code>-end token position as the total generated-token count.</li>\n<li>Cascade 2 / nano30b: we tokenize the <code>generation</code> text with the Nemotron-Cascade-2 tokenizer (<code>nvidia/Nemotron-Cascade-2-30B-A3B</code>) and compute both the total and the <code>\\boxed{}</code>-end positions exactly by running the tokenizer on the full text and on the prefix up to the closing brace of the last <code>\\boxed{…}</code>.</li>\n</ul>\n<h2>Appendix E: self-verification grader prompt and hyperparameters</h2>\n<p>Each pairwise verification is its own chat session with Python tool use enabled. System message:</p>\n<pre><code>You are a helpful assistant and an expert math contest grader.\n</code></pre>\n<p>User message (fields <code>{problem}</code>, <code>{sol_A}</code>, <code>{sol_B}</code> substituted from the current problem and two competing solution texts):</p>\n<pre><code>Compare two submissions and rate correctness based solely on the final answer.\n\n**Evaluation Guidelines:**\n- Extract each submission's final answer. Use any provided reasoning only\n  to help you assess whether the stated final answer is trustworthy. Do\n  not award credit for method quality or rigor.\n- Carefully analyze the problem statement and the submissions to assess\n  whether each final answer is correct. Grade only the final answer.\n**Problem**\n{problem}\n**Solution A**\n{sol_A}\n**Solution B**\n{sol_B}\n**Output Format:**\nFirst, provide your reasoning (what checks you performed). Then, on\nseparate new lines, give ratings using the EXACT tags below. Add no\nother text after the tags.\n&lt;rating_A&gt;INTEGER_1_TO_10&lt;/rating_A&gt;\n&lt;rating_B&gt;INTEGER_1_TO_10&lt;/rating_B&gt;\n**Rating Rules:**\n- Rate correctness on a 1-10 scale (10 = certainly correct, 8 = very\n  likely correct, 5 = uncertain/borderline, 3 = likely incorrect, 1 =\n  certainly incorrect).\n- Higher rating wins. Equal ratings imply a tie.\nPlease provide your analysis now.\n</code></pre>\n<p>We parse the rating pair out of the model's final message with an XML-tag regex first, then fall back to <code>\\boxed{N}</code> per side if the model used its trained <code>\\boxed{}</code> format instead of the requested tags.</p>\n<h2>Appendix F: final LB submission — configuration</h2>\n<p>All the knobs we actually shipped for <a href=\"https://www.kaggle.com/code/chankhavu/c2-distill-vllm018-watchdog\" target=\"_blank\">chankhavu/c2-distill-vllm018-watchdog</a>.</p>\n<p><strong>Model.</strong> <code>chankhavu/c2-legaldistill-30b-a3b-fp8</code> — the FP8-quantized soft-clone of Nemotron-Cascade-2, re-quantized specifically for the Kaggle submission (not our published open-source FP8 checkpoint; see the \"Quantizing the Cascade 2 model\" section). Runtime: vLLM 0.18.0 on 1× H100 SXM under the Kaggle inference server sandbox.</p>\n<p><strong>vLLM serve command.</strong> Single GPU, FP8 KV cache, large context window, chunked prefill, prefix caching, async scheduling, native function-calling enabled with the <code>qwen3_coder</code> tool-call parser:</p>\n<pre><code>vllm serve /path/to/c2-legaldistill-30b-a3b-fp8 \\\n    --served-model-name chankhavu/c2-legaldistill-30b-a3b-fp8 \\\n    --tensor-parallel-size 1 \\\n    --max-num-seqs 64 \\\n    --gpu-memory-utilization 0.9 \\\n    --dtype auto \\\n    --max-model-len 262144 \\\n    --max-num-batched-tokens 8192 \\\n    --kv-cache-dtype fp8 \\\n    --mamba-ssm-cache-dtype float32 \\\n    --mamba-cache-mode all \\\n    --mamba-block-size 512 \\\n    --enable-chunked-prefill \\\n    --enable-prefix-caching \\\n    --async-scheduling \\\n    --enable-auto-tool-choice \\\n    --tool-call-parser qwen3_coder \\\n    --trust-remote-code \\\n    --host 0.0.0.0 --port 8000\n</code></pre>\n<p>We also run a lightweight watchdog subprocess that polls <code>/proc</code> every 30 s and relaunches vLLM if the APIServer / EngineCore processes disappear (scrapes the <code>VLLM::APIServer</code> / <code>VLLM::EngineCor...</code> <code>comm</code> names — the literal <code>vllm.entrypoints</code> string stops appearing in <code>cmdline</code> after vLLM calls <code>setproctitle</code> during startup). The relaunch itself takes ~100 s, which is why the watchdog restart trigger is \"no live vllm process found\" rather than HTTP-based.</p>\n<p><strong>Solver configuration</strong> (from <code>CFG</code>):</p>\n<table>\n<thead>\n<tr>\n<th>Setting</th>\n<th>Value</th>\n<th>Notes</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>attempts</code></td>\n<td>8</td>\n<td>Solution rollouts per problem</td>\n</tr>\n<tr>\n<td><code>workers</code></td>\n<td>16</td>\n<td>Thread pool size (2× <code>attempts</code>, so cancellations don't stall new launches)</td>\n</tr>\n<tr>\n<td><code>early_stop</code></td>\n<td>4</td>\n<td>Stop once 4 rollouts agree on an integer answer</td>\n</tr>\n<tr>\n<td><code>turns</code></td>\n<td>128</td>\n<td>Max conversation turns per rollout</td>\n</tr>\n<tr>\n<td><code>temperature</code></td>\n<td>1.0</td>\n<td>Sampling diversity — all rollouts share the prompt</td>\n</tr>\n<tr>\n<td><code>top_p</code></td>\n<td>0.95</td>\n<td></td>\n</tr>\n<tr>\n<td><code>top_logprobs</code></td>\n<td>5</td>\n<td>Top-5 logprobs for entropy computation</td>\n</tr>\n<tr>\n<td><code>enable_thinking</code></td>\n<td><code>True</code></td>\n<td>Cascade 2's <code>&lt;think&gt;…&lt;/think&gt;</code> reasoning</td>\n</tr>\n<tr>\n<td><code>context_tokens</code></td>\n<td>262144</td>\n<td>vLLM <code>--max-model-len</code></td>\n</tr>\n<tr>\n<td><code>max_completion_tokens</code></td>\n<td>131072</td>\n<td>Per-turn completion cap</td>\n</tr>\n<tr>\n<td><code>notebook_limit</code></td>\n<td>17400 s (4.83 h)</td>\n<td>Kaggle inference ceiling</td>\n</tr>\n<tr>\n<td><code>high_problem_timeout</code></td>\n<td>900 s</td>\n<td>Default per-problem budget</td>\n</tr>\n<tr>\n<td><code>base_problem_timeout</code></td>\n<td>300 s</td>\n<td>Reserved per remaining problem; also the per-problem floor</td>\n</tr>\n<tr>\n<td><code>python_timeout</code></td>\n<td>30 s</td>\n<td>Per Python tool call</td>\n</tr>\n<tr>\n<td><code>max_output_characters</code></td>\n<td>1000</td>\n<td>Sandbox stdout truncation</td>\n</tr>\n<tr>\n<td><code>seed</code></td>\n<td>42</td>\n<td>Unused (vLLM picks fresh seeds per request — we <em>want</em> the cross-worker diversity)</td>\n</tr>\n</tbody>\n</table>\n<p><strong>Adaptive budget formula.</strong> For each problem we compute\n<code>budget = min(high_problem_timeout, max(base_problem_timeout, time_left − base_problem_timeout × problems_left_others))</code>, so early problems get the full 900 s; as the wall clock runs down we reserve 300 s per remaining problem and squeeze the current one toward the 300 s floor.</p>\n<p><strong>System prompt.</strong></p>\n<pre><code>You are a helpful assistant and a mathematical problem solver.\nSolve the given problem step by step using rigorous reasoning.\n</code></pre>\n<p><strong>User template</strong> (<code>{problem}</code> substituted per problem):</p>\n<pre><code>{problem}\n\nThe answer is a non-negative integer between 0 and 99999, inclusive.\nPut your final answer in \\boxed{}.\n</code></pre>\n<p><strong>Tool definition</strong> — the model is given exactly one function, a stateful IPython code executor:</p>\n<pre><code>{\n  \"type\": \"function\",\n  \"function\": {\n    \"name\": \"stateful_python_code_exec\",\n    \"description\": \"Call this function to execute Python code in a stateful Jupyter notebook environment. Python will respond with the output of the execution or time out after 120.0 seconds. The environment has math, numpy, sympy, itertools, collections, and mpmath pre-imported.\",\n    \"parameters\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"code\": {\"type\": \"string\", \"description\": \"Code to execute\"}\n      },\n      \"required\": [\"code\"]\n    }\n  }\n}\n</code></pre>\n<p>Per-call timeout is actually 30 s (from <code>CFG.python_timeout</code>), not 120 s — the description is the one the Cascade 2 model was trained to expect, so we leave it unchanged.</p>\n<p><strong>Sandbox preimports.</strong> Every new session starts with:</p>\n<pre><code>import math\nimport numpy\nimport sympy\nimport itertools\nimport collections\nimport mpmath\nmpmath.mp.dps = 64\n</code></pre>\n<p><strong>Answer extraction.</strong> A small regex cascade applied to each rollout's final non-tool-call message, in order: (i) <code>\\boxed{DIGITS}</code> — pick the last one; (ii) <code>\\boxed{… = DIGITS}</code> — pick the last integer inside the last <code>\\boxed{}</code>; (iii) <code>final answer is DIGITS</code>. Accepts integers in <code>[0, 99999]</code> only. Before scanning, <code>&lt;think&gt;…&lt;/think&gt;</code> reasoning blocks are stripped.</p>\n<p><strong>Voting.</strong> Each rollout's weight is <code>1 / max(mean_token_entropy, 1e-9)</code>, where the per-token entropy is computed from the top-5 logprobs of the generated tokens (not tool-call bytes). The picked answer is the one with the highest sum of weights across the rollouts that produced it.</p>\n<p><strong>Hyperparameters.</strong> Grader runs at temperature 0, top_p 0.95, same Python sandbox as the solver. Tool-call budget per grader session: 15 calls; past that we force-respond to any pending tool calls with a stub and instruct the model to output its ratings immediately. Maximum verifications per problem: 16. Maximum concurrent rollouts (solution + verification combined) in the thread pool: 8.</p>\n<h2>Appendix G: tool-call error breakdown (gpt-oss-120b vs Cascade 2)</h2>\n<p>Why does gpt-oss-120b's tool-call overhead come out roughly 2× Cascade 2's? The whole question is really about <strong>time</strong>: a typical gpt-oss-120b rollout burns <strong>37 s of wall-clock on errored or timed-out tool calls — 14.0% of its 266 s average rollout — vs 17 s for Cascade 2 (6.3% of its 272 s rollout)</strong>. ~2.2× the absolute, ~2.2× the rollout fraction. All numbers below are from the same LB-simulation runs that feed the chart in §Per-problem breakdown (aimo50a/b/c, 3 shuffles × 50 problems × up to 8 rollouts per problem, 1× H100 SXM under the 5 h Kaggle budget).</p>\n<p>Per-rollout averages — every column below is a mean across rollouts (912 for gpt-oss-120b, 850 for Cascade 2; &lt; 50×3×8 = 1200 because the adaptive scheduler cancels in-flight rollouts once 4 agree on an answer). In the table, \"error\" means <em>any</em> failed tool call — Python exceptions and the 30 s sandbox timeouts together.</p>\n<table>\n<thead>\n<tr>\n<th>Model</th>\n<th>rollout sec</th>\n<th>tool calls</th>\n<th>tool sec</th>\n<th><strong>error sec</strong></th>\n<th><strong>% rollout errored</strong></th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>gpt-oss-120b</td>\n<td>266</td>\n<td>18.4</td>\n<td>66.3</td>\n<td><strong>37.3</strong></td>\n<td><strong>14.0%</strong></td>\n</tr>\n<tr>\n<td>Cascade 2</td>\n<td>272</td>\n<td>14.3</td>\n<td>32.4</td>\n<td><strong>17.1</strong></td>\n<td><strong>6.3%</strong></td>\n</tr>\n</tbody>\n</table>\n<p>Within each model, where the error+timeout time goes (<code>% errs</code> and <code>% err-time</code> are within-model fractions of the <em>error+timeout</em> totals):</p>\n<p>All low-frequency classes (&lt; 0.1 occurrences per rollout in both models) are collapsed into \"other\" — this includes RuntimeError, ValueError, KeyError, IndexError, ImportError, and uncategorized Python tracebacks.</p>\n<table>\n<thead>\n<tr>\n<th>Class</th>\n<th>gpt-oss-120b<br>count/rollout</th>\n<th>sec/rollout</th>\n<th>% errs</th>\n<th>% err-time</th>\n<th>Cascade 2<br>count/rollout</th>\n<th>sec/rollout</th>\n<th>% errs</th>\n<th>% err-time</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>[TIMEOUT]</strong> (30 s cap)</td>\n<td><strong>1.01</strong></td>\n<td><strong>33.1</strong></td>\n<td>36.4%</td>\n<td><strong>88.6%</strong></td>\n<td>0.51</td>\n<td>16.0</td>\n<td>33.1%</td>\n<td><strong>94.0%</strong></td>\n</tr>\n<tr>\n<td><strong>SyntaxError</strong></td>\n<td><strong>0.72</strong></td>\n<td>0.3</td>\n<td>26.1%</td>\n<td>0.9%</td>\n<td>0.11</td>\n<td>0.0</td>\n<td>7.3%</td>\n<td>0.1%</td>\n</tr>\n<tr>\n<td><strong>NameError</strong></td>\n<td>0.49</td>\n<td>2.3</td>\n<td>17.8%</td>\n<td>6.0%</td>\n<td>0.15</td>\n<td>0.1</td>\n<td>9.7%</td>\n<td>0.7%</td>\n</tr>\n<tr>\n<td>TypeError</td>\n<td>0.17</td>\n<td>0.1</td>\n<td>6.1%</td>\n<td>0.3%</td>\n<td>0.23</td>\n<td>0.0</td>\n<td>15.1%</td>\n<td>0.2%</td>\n</tr>\n<tr>\n<td>AttributeError</td>\n<td>0.10</td>\n<td>0.0</td>\n<td>3.6%</td>\n<td>0.1%</td>\n<td>0.10</td>\n<td>0.1</td>\n<td>6.5%</td>\n<td>0.4%</td>\n</tr>\n<tr>\n<td>other</td>\n<td>0.28</td>\n<td>1.5</td>\n<td>10.0%</td>\n<td>4.1%</td>\n<td>0.41</td>\n<td>0.7</td>\n<td>28.3%</td>\n<td>4.6%</td>\n</tr>\n<tr>\n<td><strong>All errored + timed-out</strong></td>\n<td><strong>2.77</strong></td>\n<td><strong>37.3</strong></td>\n<td>100%</td>\n<td>100%</td>\n<td><strong>1.53</strong></td>\n<td><strong>17.1</strong></td>\n<td>100%</td>\n<td>100%</td>\n</tr>\n</tbody>\n</table>\n<p><strong>Timeouts are ~90% of the wasted wall-clock for both solvers</strong> (88.6% / 94.0%) — every Python error class is a rounding error by time.</p>\n<p><strong>NameError carries the strongest cross-model signal</strong> (0.49/rollout for gpt-oss-120b vs 0.15 for Cascade 2). gpt-oss-120b's 451 extractable NameErrors split into two causes:</p>\n<ul>\n<li>~80 cases — <strong>forgot to import.</strong> gpt-oss-120b writes Jupyter-cell-style snippets that assume <code>sp</code> (16), <code>random</code> (15), <code>deque</code> (9), <code>math</code> (5), and other stdlib aliases are already live, but the pre-import block (<code>math, numpy, sympy, itertools, collections, mpmath</code>) doesn't expose them and the model rarely writes the <code>import</code> line itself.</li>\n<li>The remaining ~370 — <strong>sandbox rollback.</strong> Standard Jupyter/IPython preserves partial assignments after an exception; ours snapshots state <em>before</em> each cell and restores it on error, so variables defined earlier in a failed cell are wiped. Models trained on REPL semantics write follow-up code referencing those now-gone names. Cascade 2's 126 NameErrors are mostly the same rollback pattern with user-defined helpers — just hit less often.</li>\n</ul>",
      "rawMarkdown": "For both [@yeoyunsianggeremie](https://www.kaggle.com/yeoyunsianggeremie) and me, this is our third time participating in AIMO. Last year, we went all out and put everything into SFT + RLHF (Kaggle post: [\"So… We cooked a model 🤷\"](https://www.kaggle.com/competitions/ai-mathematical-olympiad-progress-prize-2/writeups/so-we-cooked-a-model), detailed blog post: [\"Improving DeepSeek R1 in Math\"](https://hav4ik.github.io/improving-deepseek-r1/)). This year, with Geremie grinding 75+ hours per week at his startup and me in the middle of a release, we had almost no time to compete like we did last year. Luckily, we had companions helping us: Geremie's [bogoconic1/Qgentic-AI](https://github.com/bogoconic1/Qgentic-AI) and my Claude Code Max subscription!\n\nDespite joining the competition only in the last couple of weeks, we got delightfully stuck in a few side quests, and ended up creating a few useful artifacts that we think are worth sharing.\n\nWe'd like to express our gratitude to AIMO3 organizers and XTX market for hosting such a fun competition and providing H100 GPUs. Special thanks to Simon Frieder ([@friederrr](https://www.kaggle.com/friederrr)) for actively engaging with the community, making it one of the most pleasant competitions I've ever participated in.\n\n\n---\n\n# Summary\n\n## Open-Source Contributions\n\nOur little fun time with AIMO3 resulted in following open-source side quests, useful beyond this competition:\n\n- **A collection of quantized Nemotron Cascade 2 models**, [chankhavu/Nemotron-Cascade-2-30B-A3B-FP8](https://huggingface.co/chankhavu/Nemotron-Cascade-2-30B-A3B-FP8) and [chankhavu/Nemotron-Cascade-2-30B-A3B-NVFP4](https://huggingface.co/chankhavu/Nemotron-Cascade-2-30B-A3B-NVFP4), totaling 60K downloads on Huggingface 🤗. Hybrid Mamba-MoE models are tricky to quantize because the Mamba state is sensitive to numerical precision; these FP8 and NVFP4 variants were first ones of such precisions published for Cascade 2 and show no degradation in quality.\n- **Enabled Hybrid-Mamba models to use Prefix Caching (PC) with Speculative Decoding (SD) in vLLM.** PC (reuse KV tensors across the common prefix of sibling generations) and SD (draft K tokens with a small model, verify K at once with the large one) are standard inference-time tricks, but running them together on Hybrid-Mamba models causes *mamba cache poisoning* — rejected speculative tokens leak into the shared Mamba state — and has been an open problem in both vLLM and SGLang. [Our custom vLLM fork](https://github.com/hav4ik/vllm/tree/pc-spec-v2-nonpc-slots) solves it with per-request \"ghost\" Mamba states. See *Upstream PR status* in the vLLM section below for upstreaming status.\n- **An EAGLE-3 speculator for Nemotron Cascade 2:** [chankhavu/Nemotron-Cascade2-30B-A3B-Eagle3-Long-Context](https://huggingface.co/chankhavu/Nemotron-Cascade2-30B-A3B-Eagle3-Long-Context) — the small draft-head model SD uses to propose tokens — reaching **3.55** mean accepted tokens at K=7. For long-context speculator training we [modified SpecForge](https://github.com/hav4ik/SpecForge-Nemotron3/tree/nemotron-cascade-2-experiments) to use chunked fused soft-target CE, chunked MLP, sliding attention, and correct handling of Mamba SSM states. We'll make a PR to SpecForge upstream as well.\n\nWe also uncovered cheating attempts by Cascade 2 on Math problems (searching the web for answers) and reported to model authors here: [Cascade-2 cheating attempts on Math is... cute](https://huggingface.co/nvidia/Nemotron-Cascade-2-30B-A3B/discussions/25#69e67838889c53e909054431).\n\n## Our final submission\n\n> Kaggle notebook: [chankhavu/c2-distill-vllm018-watchdog](https://www.kaggle.com/code/chankhavu/c2-distill-vllm018-watchdog)\n> Model: [chankhavu/c2-legaldistill-30b-a3b-fp8](https://www.kaggle.com/models/chankhavu/c2-legaldistill-30b-a3b-fp8/transformers/fp8/1)\n\nOur solution uses [Nemotron-Cascade-2-30B-A3B](https://huggingface.co/nvidia/Nemotron-Cascade-2-30B-A3B), which we soft-cloned via layer-wise linear distillation to make it legal for this competition (Cascade 2 was released after the March 15 cutoff — see the Introduction below), then selectively quantized to FP8 with no quality degradation.\n\nThe final harness is intentionally simple: **8 parallel rollouts per problem with Python tool use, entropy-weighted majority voting**. We also tried a more sophisticated variant that filled the GPU idle bubbles between early- and late-finishing rollouts with pairwise LLM-as-judge self-verification, but didn't have time to tune it properly to beat plain majority voting. We also put a great deal of work to make speculative decoding work with prefix caching for hybrid-mamba models, but it didn't get into our final submission due to lack of submission budget and time to test things out properly. Summary of our main candidates on local validation and the Kaggle leaderboard:\n\n| **Submission**<br>(model, harness) | **Local validation**<br>(3 shuffles) | **Local validation**<br>(mean) | **Public LB attempts** | **Private LB** |\n| --- | --- | --- | --- | --- |\n| gpt-oss-120b, 44/50 public notebook | 33, 30, 36 | 33.0 | 38, 38, 37 | — |\n| Cascade 2, 8 rollouts, self-verification (untuned) | 33, 31 | 32.0 | 34 | — |\n| **Cascade 2, 8 rollouts, voting**<br>**(our final submission)** | **36, 34, 34** | **34.6** | **36, 39, 38** | **41** |\n\nOur local validation imitates the Kaggle leaderboard setting — 50 hard IMO-level problems drawn from [IMO-AnswerBench](https://imobench.github.io/) (an open benchmark of IMO-style problems with short-integer answers), 5 hours on 1× H100 SXM. We shuffle the 50 problems 3 times with fixed seeds and run each solver through each shuffle, which gives us a controlled way to compare candidates without burning LB submissions.\n\n\n---\n\n\n# Introduction\n\n**The competition.** [AIMO3](https://www.kaggle.com/competitions/ai-mathematical-olympiad-progress-prize-3) (the AI Mathematical Olympiad Prize 3, hosted by Kaggle and XTX Markets) evaluates math-problem solvers on 50 held-out IMO-level problems with short-integer answers. Each submission is a notebook that runs on a single NVIDIA H100 SXM (80 GB) with a 5-hour wall-clock budget. Models published after March 15, 2026 are not allowed to be used directly at runtime, which is the rule that shapes most of the model choices below. The public leaderboard is scored on a visible subset; the final standings use a disjoint private set revealed at the end.\n\n**The main model candidate** everyone converged on this year is [gpt-oss-120b](https://huggingface.co/openai/gpt-oss-120b) — OpenAI's 120 B-parameter open-weights reasoning model with native mxfp4 weights and a Harmony chat format. It's the model behind the strongest public-notebook baseline (the \"44/50\" notebook on the AIMO3 discussion board), and our main point of comparison throughout.\n\nIn March 19, [@chankhavu](https://www.kaggle.com/chankhavu)'s birthday, [Nemotron-Cascade-2-30B-A3B](https://huggingface.co/nvidia/Nemotron-Cascade-2-30B-A3B) (shortened to *Cascade 2* in this post) was released — NVIDIA's 30 B hybrid Mamba-MoE reasoning model. Published just after the March 15 cutoff, so not directly usable as-is, but the architecture is identical to the pre-cutoff [Nemotron-3-Nano-30B-A3B](https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16). This is what made the \"soft-cloning\" approach legal, and it's the model inside our final submission. The hybrid-Mamba architecture is what forced us down a bunch of the side quests below — quantizing it, getting prefix caching to work with speculative decoding, training an Eagle3 speculator, and so on.\n\n**Other participant's findings.** With only a few weeks to compete, we relied heavily on the open-source ecosystem and the community's collective findings. Some highlights:\n\n* [AIMO3 journey of 60 experiments](https://www.kaggle.com/code/pawanmali/chasing-47-50-aimo3-journey-of-60-experiments) by [@pawanmali](https://www.kaggle.com/pawanmali) - this incredible repository of experimentation results guided us towards simpler prompts in our solutions.\n* [44/50 public notebook](https://www.kaggle.com/code/nihilisticneuralnet/44-50-let-me-over-cook) by [@nihilisticneuralnet](https://www.kaggle.com/nihilisticneuralnet) - the strongest public notebook baseline, using gpt-oss-120b with a custom chat format and a lot of engineering to get the most out of it.\n* [Inference-Time Optimization Lessons from AIMO 3](https://arxiv.org/abs/2603.27844) by Natapong Nitarach - similar ablation findings to [@pawanmali](https://www.kaggle.com/pawanmali).\n\n# Delightful side quests\n\n## Soft-cloning Cascade 2\n\nThe competition rules prohibits using models published after March 15 directly during runtime. However, there's no limit on distillation or how distillation should be made. We rely on the following [FAQ clarification from the competition organizers](https://www.kaggle.com/competitions/ai-mathematical-olympiad-progress-prize-3/discussion/680682):\n\n> **Question:** Say I initialize an entirely new model with same architecture as the post - cutoff teacher model, but then train it by layerwise MSE to the teacher on the residual stream; in the limit this could approximate the teacher model weights, I just didn't explicitly say that I used the weights themselves.\n>\n> **Answer:** Indeed, while conceptually, these two cases can blur, practically they don't and that is why the AMLT rules makes sense: In practice, with this approach you'd recreate in a way the model that was off limits.\n\nWe further clarified with the organizers that this kind of \"soft cloning\" is legal for the competition ([here](https://www.kaggle.com/competitions/ai-mathematical-olympiad-progress-prize-3/discussion/680682#3425516) and [here](https://www.kaggle.com/competitions/ai-mathematical-olympiad-progress-prize-3/discussion/680682#3440271)).\n\n\n### Approach\n\nAfter confirming the legality, we performed the following layer-wise MSE distillation from [Nemotron-Cascade-2](https://huggingface.co/nvidia/Nemotron-Cascade-2-30B-A3B). We initialized a randomly initialized student model with the same architecture and performed the following procedure:\n\n*   We build a collection of linear models $$F^\\text{Teacher}_i(X) = W^{Teacher}_i \\times X$$ for each layer `i` of the teacher [Nemotron-Cascade-2](https://huggingface.co/nvidia/Nemotron-Cascade-2-30B-A3B) model, where `X` is the input to that layer and `W_i` the weight tensor.\n*   We do the same for the student model, building $$F^\\text{Student}_i(X) = W^{Student}_i \\times X$$ for each layer `i`. The student model happens to have the same architecture, so we can directly map each `F^Teacher}_i` to a student `F^Student_i`.\n*   We optimize each student layer `i` to minimize the following MSE loss: $$L_i = \\mathbb{E}_{X \\sim D} \\left[ \\| F^\\text{Teacher}_i(X) - F^\\text{Student}_i(X) \\|^2_2 \\right]$$ where `D` is a dataset of randomly sampled inputs. We optimize either by gradient descent or by closed-form MSE optimization techniques. Each student treats the teacher as a black-box function that produces outputs for given inputs.\n\nWe **never** optimize towards the teacher weights directly; we only observe the teacher outputs and use them as MSE targets for the student. However, since the sub-models are linear, convergence is mathematically absolute. We published the implementation [here](https://gist.github.com/hav4ik/e4b3513bb4ece8b45fc414e024aeae72). The BF16 model can be found here: [chankhavu/bogoconic1-c2-softcpy-v3](https://www.kaggle.com/models/chankhavu/bogoconic1-c2-softcpy-v3).\n\n\n\n## Quantizing the Cascade 2 model\n\n**Important clarification:** for Kaggle submission, we quantized the cloned Cascade 2 model separately from the cloned BF16 model! We did NOT use our published open-source models.\n\n> Open-source artifacts (totaling 66K downloads on Huggingface as of April 21, 2026):\n> - 🤗 Huggingface model: [chankhavu/Nemotron-Cascade-2-30B-A3B-NVFP4](https://huggingface.co/chankhavu/Nemotron-Cascade-2-30B-A3B-NVFP4)\n> - 🤗 Huggingface model: [chankhavu/Nemotron-Cascade-2-30B-A3B-FP8](https://huggingface.co/chankhavu/Nemotron-Cascade-2-30B-A3B-FP8)\n> - 🤗 Huggingface model: [chankhavu/Nemotron-Cascade-2-30B-A3B-AWQ-INT4](https://huggingface.co/chankhavu/Nemotron-Cascade-2-30B-A3B-AWQ-INT4)\n>\n> Quantization code: [chankhavu/quantize-cascade-2](https://www.kaggle.com/code/chankhavu/quantize-cascade-2); calibration data: [chankhavu/cascade-2-calibration-data](https://www.kaggle.com/datasets/chankhavu/cascade-2-calibration-data).\n\n### Precision recipe\n\nWe followed the Nano-architecture selective quantization recipe from the [Nemotron 3 Nano Technical Report](https://research.nvidia.com/labs/nemotron/files/NVIDIA-Nemotron-3-Nano-Technical-Report.pdf) (Section 4). This is the same recipe as [NVIDIA's official FP8 checkpoint](https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-FP8) of Nemotron-3-Nano-30B-A3B. Sensitive components are kept in higher precision:\n\n- There are only 6 attention layers, so they are highly sensitive and should be preserved in BF16.\n- To match the input granularity of attention layers, Mamba layers adjacent to attention layers should be kept at BF16 as well.\n- The Mamba 1D conv is kept at BF16 across all 23 Mamba layers (quantizing it degrades the state update).\n- MoE router gates should be kept at FP32. Degradation in routing precision will affect reasoning.\n\n![Per-layer quantization layout for Nemotron Cascade 2: 52 transformer layers flanked by Embedding and lm_head. Attention layers and the Mamba layer immediately preceding each attention are kept BF16; the other Mamba layers and all MoE expert blocks are quantized to FP8 / NVFP4.](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2Fca39d8cf5ccf892e7080773c16103025%2Fcascade2_quant_layout.png?generation=1776888105108688&alt=media)\n\n### Implementation details\n\nFor calibration, we constructed a dataset of **4000 samples** from [Nemotron-Cascade-2-SFT-Data](https://huggingface.co/datasets/nvidia/Nemotron-Cascade-2-SFT-Data) with the following domain mix, specifically designed for long-context reasoning: math (1000), swe (900), terminal_agent (500), science (500), chat (400), conversational_agent (300), instruction_following (300), safety (100). We trained on sequence length of up to **12,288 tokens** (no padding, natural length per sample).\n\nWe validated the correctness of our quantized models using [NVIDIA-NeMo/Evaluator](https://github.com/NVIDIA-NeMo/Evaluator) with config from [Nemotron-3-Super-120B's eval config](https://github.com/NVIDIA-NeMo/Evaluator/blob/main/packages/nemo-evaluator-launcher/examples/nemotron/nemotron-3-super/local_nemotron-3-super-120b-a12b_tools.yaml). We see no degradation in reasoning quality of quantized models. More details in *Appendix B*.\n\n\n## Enable Hybrid-Mamba models to use PC and SD together in vLLM\n\n> Open source artifact: [hav4ik/vllm branch pc-spec-v2-nonpc-slots](https://github.com/hav4ik/vllm/tree/pc-spec-v2-nonpc-slots)\n\n### The problem\n\nOut of the box, neither vLLM nor SGLang can run Prefix Caching (PC) and Speculative Decoding (SD) together on Hybrid-Mamba models. Mamba's SSM layer keeps a fixed-size state vector updated *in place* — each token folds into it irreversibly. SD runs K candidate tokens through the model before the rejection sampler decides how many to keep; by then the SSM kernel has already overwritten the canonical state with the full K-token trajectory. Transformers escape this because their KV cache is append-only — rejected entries sit past the sequence end and touch nothing. Mamba has one state vector per layer per request and can't roll it back, so PC + SD needs an explicit mechanism to preserve the accepted state.\n\n![When speculation crosses a PC block boundary, Mamba's shared state has already been overwritten by later tokens — the commit writes the final state (s7) into Block 0's cache slot instead of the correct boundary state (s4)](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2F6f4d1ef1b007dbf7421e0ef6f972dae9%2Fpc_sd_problem.png?generation=1776888162924985&alt=media)\n\n### The correctness fix\n\nThe non-PC spec-decode path already does the right thing: the SSM and conv Triton kernels maintain K+1 per-request \"ghost\" slots with native `init_token_idx` rollback — those slots are just gated out whenever PC is active. We re-enable them and add a one-way **boundary commit**: only when a request's last accepted token lands exactly on a mamba-block boundary (`last_accepted_pos % block_size == block_size - 1`) do we write the ghost slot back to the PC pool. On every other step the pool is untouched, so the canonical cache state is never contaminated by rejected speculations. Ghost slots are indexed by batch position, so batch reordering moves them automatically — no extra bookkeeping.\n\nOne memorable debug moment: the first cut of `init_spec_slots` allocated each spec slot at the pool's page-padded stride (~50 MB/slot). At 32 seqs × K=5 × 23 layers that comes out to **52 GB** — instant OOM. The fix was to allocate contiguously (~1.3 MB/slot); the kernels read `state.stride(0)` off the tensor itself, so ghost slots and pool slots can have completely different strides without breaking anything.\n\nWe used AIME-25 as a unit test throughout, with a pass threshold of **≥95% per-session accuracy** — ironic, because AIME-25 is a notoriously hard competition that few high schoolers solve reliably, but at this point it's well within what a modern reasoning LLM with tool use can do consistently. A reliable gold standard for \"nothing silently corrupted\" is useful regardless.\n\n### Performance optimization\n\nOnce the correctness fix was in, Eagle3 was still a net loss at single-sequence concurrency: a full step took ~18 ms vs ~3.4 ms for no-Eagle, even though the target forward itself is only 4.2 ms. Profiling (H100, K=4, p=1) split the 18 ms budget roughly like this:\n\n| Stage | Time (ms) | Notes |\n| --- | --- | --- |\n| Target forward (K+1 tokens) | 4.2 | actual model compute |\n| Drafter, K=4 iterations | 11.2 | **serial**: 4 × (~2 ms Python metadata + ~0.8 ms GPU forward) |\n| `_build_attention_metadata` | 5.7 | 7× more than no-Eagle — all 52 layers × K+1 |\n| `_prepare_inputs` | 1.7 | K+1 tokens per request |\n| `commit_boundary_states` (23 mamba layers) | 3.9 | `.nonzero()` D2H sync every step + `.synchronize()` wait |\n| Rejection sampling + misc | 1.1 | |\n\nTwo things stood out. First, the drafter loop is serially CPU-bound: each iteration needs the previously accepted token before it can build the next metadata object, so Python rebuilds can't overlap the 0.8 ms GPU forwards, and the GPU sits idle ~70% of the drafter phase. Second, `commit_boundary_states` — Mamba-specific infrastructure — was paying a full host-sync tax on every step, not just the rare steps where a commit actually fires.\n\n![Eagle3 step timeline showing CPU/GPU overlap and the serial drafter ping-pong](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2Ff9ef240767d03da1ecfd5b621f84b027%2Feagle3_step_timeline.png?generation=1776888210353385&alt=media)\n\nThe drafter issue needs a proper architectural fix (fuse all K iterations into one captured graph with persistent metadata buffers) that we left for upstream. What we did land is a sweep of surgical cuts targeting the other stages:\n\n- **Masked Triton commit kernel.** `commit_boundary_states` used to call `.nonzero()` on a CUDA bool mask to find requests that had crossed a mamba-block boundary. `.nonzero()` forces a device → host sync every step. The new kernel grid is `(num_layers, num_requests)`: each block checks its mask bit on-device and returns early if unset. No host ever sees the mask. **−1.5 ms/step.**\n- **Drop the deferred-commit `synchronize()`.** The deferred-commit path was calling `sample_complete_event.synchronize()` — a blocking CPU wait — to guarantee `num_accepted_tokens` was visible before launching the commit kernel. Since both run on the same CUDA stream as the sampler, stream ordering already guarantees visibility. Removed. **−1.0 ms/step.**\n- **Re-enable `update_block_table` + batched commit indices.** Two related cleanups on the commit path that were paying per-layer overhead on pure-decode steps. **−4.5 ms/step combined.**\n- **Fast decode `attn_meta`.** On pure-decode steps, only `state_indices_tensor_d` differs between the 23 mamba attention builders, but `_prepare_inputs` was rebuilding the full metadata struct each time. Cache the first build; for the other 22, `copy_` the per-layer pool mapping into the persistent buffer and `dataclasses.replace` just that field. **−0.3 ms/step.**\n\nEnd state at single-sequence (K=4, T=1.0): step time 18 → 8.9 ms, throughput 171 → 298 tok/s vs 303 tok/s for PC-only — essentially at parity.\n\n![Eagle3 step timeline after all optimizations — step ends at 8.9 ms, no more sync stall, drafter phase compact](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2Fb3245aec078557c3cb820f13d9fc5eae%2Feagle3_step_timeline_after.png?generation=1776888256293073&alt=media)\n\nAt higher concurrency, where the serial drafter overhead is amortized across the batch, Eagle3 becomes a real win:\n\n| Concurrency | PC-only | Eagle3 K=4, T=0.7 | vs PC-only |\n| --- | --- | --- | --- |\n| p=1 | 303 tok/s | 349 tok/s | +15% |\n| p=8 | 1186 tok/s | 1552 tok/s | +31% |\n| p=12 | 1383 tok/s | 1843 tok/s | +33% |\n\n### What we didn't land\n\nOn an experimental branch we captured the K-iteration drafter decode loop as a single FULL cudagraph, replayed in 0.16 ms. Draft-phase time dropped from 3.16 ms to 1.39 ms (−56%) with acceptance rates preserved. But overall step time at p=8 was unchanged (13.7 ms) — the CPU savings got absorbed by GPU pipeline stalls in the prep phase, since the drafter's GPU work that previously overlapped the PIECEWISE Python loop now blocked the next iteration. The theoretical payoff is at p=12+ where CPU dispatch becomes the real bottleneck, but we didn't validate that before the deadline.\n\n### Upstream PR status\n\nWe had planned to upstream the correctness fix and follow with the perf path, but two things have changed since the competition closed and the PR is currently blocked:\n\n- **Hopper-only optimizations don't compile on Blackwell.** Our perf work assumed Hopper SM 9.0 capabilities (cudagraph construction, scheduler shape, the specific PIECEWISE → FULL upgrade we pushed for at p=12+). On Blackwell SM 10.0 the same code hits graph-compilation issues that we haven't resolved yet. vLLM upstream maintains GPU-portable code paths, so we can't merge as-is without a Blackwell-clean rewrite of the kernel layer.\n- **A separate community PR now covers the correctness fix.** One week after the competition deadline, [vllm-project/vllm#41233](https://github.com/vllm-project/vllm/pull/41233) was opened against the same mamba-cache-poisoning bug. It's still open as of writing, but it tackles correctness from a different angle and is likely the path upstream takes. Our perf optimizations (the FlashInfer attention switch and the surgical \"What we did land\" cuts) are orthogonal to that PR and would still apply on top of it — that PR doesn't include any speed work — but we're holding off on a perf-only PR until the correctness side lands and our Blackwell story is fixed.\n\nThe fork itself remains useful for anyone running Hybrid-Mamba SD inference on Hopper; we just can't claim the upstream PR as a clean deliverable any more.\n\n\n## Training an Eagle3 speculator for Cascade 2\n\n> Open-source artifacts:\n> - 🤗 Huggingface model: [chankhavu/Nemotron-Cascade2-30B-A3B-Eagle3-Long-Context](https://huggingface.co/chankhavu/Nemotron-Cascade2-30B-A3B-Eagle3-Long-Context)\n> - 🤗 Huggingface dataset: [chankhavu/c2_eagle3_train](https://huggingface.co/datasets/chankhavu/c2_eagle3_train) (2 stage train data)\n> - 🤗 Huggingface dataset: [chankhavu/c2-traces-10197](https://huggingface.co/datasets/chankhavu/c2-traces-10197) (collected reasoning traces on 10'197 hard math problems)\n>\n> Code: [SpecForge-Nemotron3](https://github.com/hav4ik/SpecForge-Nemotron3/tree/nemotron-cascade-2-experiments).\n\nTo our knowledge this is the first Eagle3 draft head trained against a hybrid Mamba-Transformer verifier that's verified end-to-end with vLLM. We forked [SpecForge](https://github.com/sgl-project/SpecForge) as [SpecForge-Nemotron3](https://github.com/hav4ik/SpecForge-Nemotron3/tree/nemotron-cascade-2-experiments) and added the engineering needed to fit long-context training (`L=65536, ttt=6`) on 2× RTX 6000 Pro Blackwell (96 GB each).\n\n### Data and recipe\n\nTwo stages: a broad SFT bootstrap on ~20K conversations (378M untruncated tokens, from Cascade 2's supervised-finetune distribution) followed by on-policy fine-tuning on ~9.7K Cascade 2 reasoning traces (207M tokens) — **~292M loss-masked tokens total**. We prune the 131K target vocab to a 32K draft vocab via top-K token frequency on the loss-masked positions, which keeps 99.77% coverage with ~4× smaller embedding / lm_head. Stage 1 uses LR 1e-4 with length-bucketed sampling (eliminates straggler-rank waste at `batch_size=1`); stage 2 drops to LR 5e-5 and disables bucketing for gradient diversity.\n\n### Engineering\n\nTwo classes of fixes were required to get this training loop working at long context.\n\n**Correctness — FP32 SSM scan state.** Upstream `mamba_ssm` 2.3.1 silently downcasts the SSM scan boundary state to BF16 inside its Triton kernel. We monkey-patch it to force FP32, bit-equivalent to vLLM 0.19's NemotronH default. Without this, training converges to a worse minimum that costs ~10% absolute on AIME-class math.\n\n**Memory footprint — fitting L=65k on 96 GB.** Naïve training peaks north of 120 GiB (OOM on 96 GB). Four independent activation-memory cuts on the draft forward + TTT unroll (chunked fused linear + soft-target CE for the lm_head, freeing the verifier's 131k-vocab logits before TTT unrolling, Liger-style chunked MLP, sliding-window attention on the draft) bring peak to **~88–91 GiB**, with the single biggest win being the verifier-logit release. Step time at L=65k ttt=6 is ~4 s/step (high variance across sequence lengths). Separately, a 100× speedup in the loss-mask parser that was otherwise re-tokenizing each conversation `num_turns` times. Full details in *Appendix C*.\n\n### Results\n\nTemperature sweep, mean accepted tokens per step (on AIME-25):\n\n| K | T=0.0 | T=0.7 | T=0.8 | T=0.9 | T=1.0 |\n| --- | --- | --- | --- | --- | --- |\n| K=4 (out of K+1=5) | **3.33** | 3.00 | 3.05 | 2.88 | 2.91 |\n| K=7 (out of K+1=8) | **3.55** | 3.26 | 3.14 | 3.03 | 2.90 |\n\nLower temperature → draft head is more confident and its proposals track the target's argmax more closely, so acceptance climbs. You can find more information in *Appendix C*.\n\n\n## Cascade 2 searches the web when it's desperate\n\nThe sandbox we gave Cascade 2 for tool use was a `stateful_python_code_exec` Jupyter kernel with network egress not blocked — `urllib`, `requests`, and `pip install` all worked. On 10,197 rollouts against IMO-class problems, Cascade 2 issued real HTTP calls in **246 traces** (~2.4%).\n\nTop destinations (HTTP URL literals in tool-call code, per-trace dedup), by category:\n\n- **Reference sites** — `artofproblemsolving.com` (64), `en.wikipedia.org` (58), `oeis.org` (28), `mathworld.wolfram.com` (6)\n- **Web search** — `google.com` (40), `duckduckgo.com` (39), `bing.com` (3)\n- **Q&A archives** — `math.stackexchange.com` (13), `api.stackexchange.com` (12), `stackoverflow.com` (3)\n- **Competition archives** — `imo-official.org` (11), `purplecomet.org` (5)\n- **Misc** — `raw.githubusercontent.com` (8), `arxiv.org` (3), `api.github.com` (3)\n\nThe dominant pattern is AoPS wiki lookups by year/contest/number and Wikipedia for theorem statements. OEIS (28 traces) is usually queried with a computed-prefix sequence. DuckDuckGo Instant Answer API and `google.com/search` GETs with the prompt pasted verbatim show up often enough to be a recognizable tactic.\n\nA few representative (amusing) traces:\n\n- **IMO 2021 Shortlist PDF** — `urllib.request.urlopen` against `imo-official.org`, written to disk (byte-exact match to the real PDF), `PyPDF2.PdfReader` over every page, substring-grep for prompt keywords. Extraction worked; the problem wasn't in the document. Answer happened to be 0 → coincidental match.\n- **Purple Comet 2023 HS Final** — `requests.get(...)`, saved response, tried `PyPDF2` / `pdftotext` / `fitz` / `pdfminer` — all four failed with `EOF marker not found` / `No /Root object`. First 100 bytes of the \"PDF\" were `<!DOCTYPE html>`; the server returned an HTML error page with a `.pdf` extension. Wrong answer.\n- **IMO shortlist in-memory grep** — fetched `IMO2021SL.pdf` and `IMO2022SL.pdf` into memory via `response.read()`, never persisted, `grep_bytes(b'Mario')`. Problem was IMO 2022 SL C2 (\"Mario and Bowser\"). Wrong answer.\n- **purplecomets.org** — `!wget` with a typo (trailing `s`), DNS fails.\n\nNetwork-using traces end up at **34.6%** correct — these are the IMO-level problems the model couldn't crack with direct reasoning in the first place, and no venue in the breakdown meaningfully changes the picture (wikipedia 36.6%, AoPS 27.9%, OEIS 27.0%, Google search 42.9% at the top). That said, the model **recognizing a problem as a benchmark item** and reaching for the right contest archive is a contamination signal on its own, independent of whether the lookup paid off.\n\nWe reported this to the Nemotron team ([HF discussion](https://huggingface.co/nvidia/Nemotron-Cascade-2-30B-A3B/discussions/25#69e67838889c53e909054431)) and released the verified traces: 🤗 [chankhavu/nemotron-cascade2-cheating-attempts](https://huggingface.co/datasets/chankhavu/nemotron-cascade2-cheating-attempts) (173 rows). For our own runs since, we block egress at the sandbox namespace level.\n\n\n---\n\n# Back to Kaggle AIMO3\n\nWith the open-source artifacts above in hand — the soft-cloned FP8 Cascade 2, the vLLM PC+SD fix, the Eagle3 speculator — we now turn to the competition itself: how these pieces line up against gpt-oss-120b on the benchmarks, what we actually submitted, and which harnesses we tried but didn't ship.\n\n## Evaluation set\n\n> We publish the dataset construction notebook here: [https://www.kaggle.com/code/chankhavu/local-validation-benchmark](https://www.kaggle.com/code/chankhavu/local-validation-benchmark).\n>\n> All throughput analysis in this section are based on the model alone, without speculative decoding.\n\nWe construct a candidate pool of 63 problems by combining the 10 official AIMO3 reference problems with 53 hard problems from [IMO-AnswerBench](https://imobench.github.io/) — specifically those where gpt-oss-120b scored 6/8 or less on avg@8, restricted to answers in the Kaggle 0–99999 integer range. The pool draws from IMO Shortlists, USAMO, HMMT, Sharygin, RMM, and national TSTs (Vietnam, China, Korea, Japan). We then take a 50-problem subset to match the Kaggle Leaderboard setting used for local validation above.\n\nFor the gpt-oss-120b tool-use accuracy stats, we use the [answerbench-gpt-oss](https://www.kaggle.com/datasets/yiyangzheng/answerbench-gpt-oss) dataset by [@yiyangzheng](https://www.kaggle.com/yiyangzheng) — they were among the top contenders on the LB, so their inference pipeline is trustworthy.\n\n> **Note on contamination.** IMO-AnswerBench is itself drawn from previously published IMO and IMO-Shortlist problems (and other national olympiads), rewritten so that the answer is an integer in a fixed range. Both gpt-oss-120b and Cascade 2 were trained on data that almost certainly includes some of these problems in their original form, so contamination is a real concern. Cascade 2 in particular was released *after* IMO-AnswerBench was, which expands the contamination window. We did not test for contamination on our side; we chose to trust the data-curation procedure described in the Nemotron-Cascade 2 paper, which uses n-gram-based deduplication against known evaluation benchmarks. We flag this so the reader can weight the absolute accuracy numbers accordingly; the relative comparisons between solvers are less affected because both candidates draw from the same potentially-contaminated training pool.\n\n\n## Offline benchmark\n\n> We published all reasoning traces collected for offline benchmark: [chankhavu/benchmark-traces](https://www.kaggle.com/datasets/chankhavu/benchmark-traces)\n\nAll three candidates run through the same harness on the 63-problem evaluation set (8 rollouts per problem, tool use enabled, same prompt template): gpt-oss-120b, our FP8-quantized Cascade 2, and the [Nemotron-3-Nano-30B-A3B](https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16).\n\n### Accuracy and tool use\n\nMetric definitions used throughout this section: **Accuracy** is avg@1 (probability a single rollout is correct, averaged over all 504 rollouts). **Majority vote** is maj@8 (the most-common of 8 parallel rollouts matches the expected answer). **Pass@8** is the probability that *at least one* of 8 rollouts is correct (the upper bound an oracle verifier could reach).\n\n| Model | Accuracy | Majority vote | Pass@8 | No-answer | Avg gen tok | Avg tools/sess |\n| --- | --- | --- | --- | --- | --- | --- |\n| gpt-oss-120b | 44.0% (222/504) | 52.4% (33/63) | **77.8%** (49/63) | 8.9% (45/504) | **26k** | 20.7 (7% err) |\n| nano30b | 41.5% (209/504) | 52.4% (33/63) | 63.5% (40/63) | **1.6%** (8/504) | 47k | 14.2 (6% err) |\n| cascade2 | **47.0%** (237/504) | **55.6%** (35/63) | 74.6% (47/63) | 2.2% (11/504) | 34k | **11.5** (7% err) |\n\nCascade 2 leads on both accuracy and voting with half the tool calls — more sample-efficient when it does reach for Python. gpt-oss-120b's high no-answer rate comes from hard problems where it runs out of context mid-reasoning.\n\n### Generation speed on 1× H100 SXM\n\n|  | 1 sequence | 4 sequences | 8 sequences | 12 sequences |\n| --- | --- | --- | --- | --- |\n| gpt-oss-120b, mxfp4, kv8 | 175.4 tok/s | 506.4 tok/s | 810.9 tok/s | 842.8 tok/s |\n| Cascade 2, fp8, kv8 | 301.6 tok/s | 859.0 tok/s | 1333.5 tok/s | 1487.8 tok/s |\n\nWe use the FP8 Cascade 2 variant because H100 supports FP8 natively. Cascade 2 is faster at every batch size, and the gap widens at higher concurrency: gpt-oss-120b's throughput plateaus by 8 sequences, while Cascade 2 keeps scaling.\n\n### Test-time scaling\n\nTwo views of how each model scales with more compute per problem. **pass@k** = prob. ≥1 of k rollouts is correct; **maj@k** = probability that the most-common answer across k rollouts is correct. Methodology (x-axis conversion, truncation rule, tokenizer choice) in *Appendix D*.\n\n**Time budget.** Each dot is k ∈ {1, …, 8}; the x-axis converts k into wall-clock seconds per problem on 1× H100 SXM at 8 parallel sequences. Each model's dots are spaced differently because their per-rollout cost differs: Cascade 2 is the cheapest per rollout, so its curve packs the most to the left.\n\n![Test-time scaling vs wall-clock seconds per problem at 8 parallel sequences. Each dot is k ∈ {1, …, 8}.](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2F0f085dcf830df3ae82dd1dcdd2c87eeb%2Ftest_time_scaling.png?generation=1779061888745661&alt=media)\n\n**Within the tested budget, Cascade 2 wins both pass@k and maj@k** — it's the more consistent choice for the actual competition setting. It leads on maj@k across the whole range, *and* it wins pass@k at every plotted budget because it's cheaper per rollout and gets to fit more rollouts in for any given wall-clock. gpt-oss-120b's pass@k curve, however, has a *higher upper ceiling* — its rollouts are more diverse (helps pass@k, hurts plurality voting), so given more attempts / a larger budget than this chart covers, an oracle verifier picking the right rollout would eventually prefer gpt-oss-120b. That higher pass@k ceiling is what makes gpt-oss-120b interesting *with* an external verifier (see *Self-verification experiments* below); within the time budget of this competition, Cascade 2 dominates on both axes.\n\nA practical note on that pass@k advantage: realizing it would require an external verifier picking from gpt-oss-120b's rollouts, and Kaggle's resource constraints (5 h on 1× H100 SXM) make that infeasible in either obvious form — we cannot fit a *separate* verifier model alongside gpt-oss-120b's ~120 GB MXFP4 weights on a single H100, and using gpt-oss-120b *as its own* verifier doubles the per-problem token bill on a budget that already only fits ~8 solution rollouts per problem. Cascade 2's smaller footprint and faster throughput leave headroom for the self-verification experiments below; gpt-oss-120b does not.\n\n**Reasoning-time budget.** Left panel plots avg@1, right panel maj@8, both as a function of how long a single rollout is allowed to reason (wall-clock seconds on 1× H100 SXM at 8 parallel sequences). Each dot is the result of capping the stored rollouts at a different token length, retaining the last `\\boxed{}` within the cap as that rollout's answer. This is the \"one longer chain of thought\" axis, complementing the \"more parallel rollouts\" time-budget chart above.\n\n![Test-time scaling vs average effective reasoning time per rollout at 8 parallel sequences. Each dot is a truncation L ∈ {16k, 32k, 48k, 64k, 80k, 96k}.](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2Fc32a3b368450b06dd6948c37448d9330%2Ftest_time_scaling_length.png?generation=1779061923631429&alt=media)\n\nCascade 2 dominates on both panels. It reaches its ceiling (47.0% avg@1 / 57.1% maj@8) at ~207 s per rollout, while gpt-oss-120b tops out at ~313 s with a lower ceiling (44.0% / 52.9%) — gpt-oss reasons compactly in tokens, but its slower throughput costs it ~50% more wall-clock time. nano30b traces Cascade 2's shape shifted right by ~70 s: same throughput, longer traces — exactly the gap soft-cloning was meant to close.\n\n*How the x-axis is built.* For each truncation L ∈ {16k, 32k, 48k, 64k, 80k, 96k} we cut each rollout at L tokens, measure the *effective reasoning length* (the `\\boxed{}` position, or L if the model didn't stop), and convert tokens → seconds using the model's per-sequence throughput at 8 parallel sequences on 1× H100 SXM (101 tok/s/seq for gpt-oss-120b; 167 tok/s/seq for Cascade 2 and nano30b, which share a tokenizer and architecture).\n\n\n### Our baseline (and final LB) submission\n\n> Kaggle notebook: [chankhavu/c2-distill-vllm018-watchdog](https://www.kaggle.com/code/chankhavu/c2-distill-vllm018-watchdog)\n> Model: [chankhavu/c2-legaldistill-30b-a3b-fp8](https://www.kaggle.com/models/chankhavu/c2-legaldistill-30b-a3b-fp8/transformers/fp8/1)\n\nThe solver we actually submitted is compact: no self-verification, no external tools beyond Python, no learned verifier. Five mechanisms wrapped around the FP8 soft-clone:\n\n1. **Adaptive per-problem budget.** For each problem we set `budget = min(900, max(300, time_left − 300 × problems_left_others))`. Early problems get the full 900 s; as the wall clock runs down we reserve 300 s per remaining problem and squeeze the current one to the 300 s floor.\n2. **8 parallel rollouts.** Each problem is attacked by 8 independent rollouts on a 16-worker thread pool (over-provisioned so that early-stop cancellations don't stall new launches). All 8 share the same prompt at temperature 1.0, top_p 0.95 — diversity comes from sampling alone.\n3. **Python tool loop.** Inside each rollout, up to 128 turns of: stream a chat completion with native function-calling, execute any `stateful_python_code_exec` calls in a stateful IPython sandbox (30 s per call, 1000-char output cap, `math/numpy/sympy/itertools/collections/mpmath` preimported with `mpmath.mp.dps = 64`), append results as `tool` messages, continue. The rollout returns the integer inside the last `\\boxed{…}` when the model stops calling tools.\n4. **Early stop on 4-vote consensus.** The moment any integer has been emitted by 4 rollouts, we set a stop event, cancel the remaining futures, and move on.\n5. **Entropy-weighted majority vote.** Each rollout's weight is `1 / mean_token_entropy`, where the per-token entropy is computed from top-5 logprobs over the generation. The winning answer is the one with the largest sum of rollout weights.\n\nUnfortunately, we only finished our Eagle3 work on the last 2 days of the competition, so we never had a clean submission with only Eagle3, without other harnesses that degrades the results (like self-verification or increased number of parallel rollouts).\n\n\n## Online leaderboard simulation\n\n> The analysis in this section is based on collected traces from simulated online LB runs (50 problems per run, 5h per run, 1x H100 SXM). The data is published here: [chankhavu/aimo50-evals](https://www.kaggle.com/datasets/chankhavu/aimo50-evals/data).\n\nTo sanity-check how the offline numbers translate into the actual Kaggle setup, we replayed our final Cascade 2 voting submission and the gpt-oss-120b 44/50 public-notebook baseline on three independent shuffles of the same 50 problems, each under the real Kaggle budget: 5 hours wall-clock on 1× H100 SXM with an adaptive per-problem budget that starts at 900 s and can be squeezed down to 300 s as the wall clock runs low.\n\nThese 50 problems are a subset of the 63 problems in our offline benchmark section. We removed problems with `pass@8 = 0` for all 3 models: gpt-oss-120b, Nemotron Cascade 2, and Nemotron 3 Nano (so, basically impossible removed problems that were never solved).\n\n![Left: cumulative correct answers (out of 50) over wall-clock time, averaged across 3 shuffles (thin lines are the individual shuffles). Right: the per-problem budget the adaptive scheduler allocated to each problem as the run progressed — starts at 900 s and drops toward the 300 s floor when wall-clock time gets tight.](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2F0cc893180cba5bf84e4cf4ff7916fdbf%2Fkaggle_time_budget.png?generation=1776888430311498&alt=media)\n\nTwo readings. **(i)** Cascade 2 (mean **34.7/50**) is ahead of gpt-oss-120b (mean **33.0/50**) on the LB scoreboard from the ~2h mark onward, and reaches gpt-oss-120b's *final* score about 20 min earlier in the average run. **(ii)** gpt-oss-120b's per-problem budget gets visibly squeezed toward the end: on aimo50c the last 5 problems ran at 300 s (the minimum), because the notebook was within minutes of the 4.83h ceiling. Cascade 2 held 900 s per problem for most of the run across all three shuffles, with plenty of headroom at the finish.\n\n### Per-problem breakdown\n\nThree per-problem views of the 150 (problem × shuffle) pairs:\n\n![Per-problem analysis. Left: correctness bucketed by the number of distinct answers the 8 rollouts produced. Middle: 2×2 agreement matrix between the two notebooks on matched problem-shuffle pairs. Right: total wall-clock time broken down into generation, successful tool call, Python-error tool call, and timed-out tool call (per-call sandbox timeout = 30 s in both solvers).](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2F70f9e23ae071319d893dd81e92f91db5%2Fkaggle_per_problem_stats.png?generation=1779061973301798&alt=media)\n\n**Left — vote convergence predicts correctness.** When all 8 rollouts agree (bin 1), Cascade 2 is correct 82.8% of the time vs gpt-oss-120b's 74.6% — the online equivalent of the offline \"consistency → correctness\" story. Cascade 2 never produces a zero-answer case; gpt-oss-120b has 9 of 150 (all wrong). At 4+ distinct answers (both models stumped), Cascade 2 is slightly worse but has more such cases (30 vs 14), so the bin averages understate its overall advantage.\n\n**Middle — which model actually matters on 29% of pairs.** 53% both right, 18% both wrong, 16% Cascade-2-only right, 13% gpt-oss-only right. Net +5 pair-wise advantage to Cascade 2. Roughly half the LB score is \"easy for everyone\" and one-fifth is \"hard for everyone\"; the remaining third is where model choice actually moves the needle.\n\n**Right — where the wall-clock time actually goes.** Decomposing total wall time across all 3 shuffles into four buckets — generation, successful tool call, Python-error tool call, timed-out tool call — Cascade 2 spends **88% in generation** vs gpt-oss-120b's **74%**, and **6.3% of Cascade 2's time vs 14.4% of gpt-oss-120b's is wasted on tool calls that produced no useful output** (~2.2× more for gpt-oss-120b). For both solvers, **the wasted time is almost entirely pure 30 s timeouts** (12.5% / 5.9% of wall-clock) rather than Python errors (1.9% / 0.4%) — gpt-oss-120b's tool calls just trip the 30 s sandbox cap roughly 2× more often per rollout than Cascade 2's. *Appendix G* dives into the per-error-class breakdown and the most common root causes (forgotten imports, sandbox state rollback after a cell error).\n\n### Head-to-head: the two finalists\n\nCascade 2 voting vs the gpt-oss-120b baseline, log-derived:\n\n| **Submission**<br>(model, harness) | **Local validation**<br>(3 shuffles) | **Local val**<br>(mean) | **Tool err**<br>(per-call) | **Public LB attempts** | **Private LB** |\n| --- | --- | --- | --- | --- | --- |\n| gpt-oss-120b, 44/50 public notebook | 33, 30, 36 | 33.0 | 15.0% | 38, 38, 37 | — |\n| **Cascade 2, 8 rollouts, voting<br>(our final submission)** | **36, 34, 34** | **34.7** | **10.6%** | **36, 39, 38** | **41** |\n\nPer-call tool error rates count any call that produced no useful output — Python errors and 30 s sandbox timeouts together.\n\nThe fact that the results are in similar ballpark as Public LB (30 - 36) is pure coincidence - we did not cherry-pick the problems for our benchmark in any way. It means that the Public LB are really on the same difficulty level as the IMO-AnswerBench, which is a testament of how far we've come since AIMO1!\n\nCascade 2 wins local validation by 1.7 points and matches gpt-oss-120b on the LB mean (both 37.7), peaking higher (39 vs 38). Both finalists score meaningfully higher on the LB than on our local mean (gpt-oss +4.7, Cascade 2 voting +3.0), which suggests IMO-AnswerBench (our local set) is slightly harder than the hidden LB set. The self-verification harnesses we tried — folded into the full comparison table at the end of the `Self-verification experiments` section below — both underperformed plain voting.\n\n\n## Self-verification experiments\n\n> - Notebook we submitted to Public LB: https://www.kaggle.com/code/chankhavu/c2-distill-verifier?scriptVersionId=311859342\n> - **Caveat up front.** We built and evaluated the entire self-verification harness within the last 24 hours before the competition deadline — no time to tune any of the hyperparameters (pair-selection policy, grader prompt, max-verifications cap, temperature, rating scale, μ-score weighting, fallback thresholds). Treat the numbers below as *first-attempt* measurements, not a carefully-optimised upper bound on what self-verification can do.\n\nThe voting harness leaves an obvious source of slack: on most problems the 8 solution rollouts finish at wildly different times. The first rollout drops its `\\boxed{}` at 100–200 s, the slowest can take 500+ s, and in between we're sitting on GPU capacity we've already paid for. The self-verification variants from the table above use that capacity to run pairwise *grader* sessions that rate competing answers against each other.\n\n![Left: Gantt of one problem (correctly answered 13). 8 solution rollouts finish between 75 s and 334 s at a 3-3-2 answer split — `1` and `12` tied at three votes each, with the correct answer `13` as a 2-vote minority. Rows sorted fastest-first. Right: the pairwise tournament the grader ran (three matches, ratings out of 10), the resulting standings, and — for reference — what entropy-weighted voting alone would have picked. The matchup graph leaves `13` as the only undefeated bucket; voting would have confidently picked `12` (score 5.13 > 4.71 > 3.55), which is wrong.](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2Fd71a07030d85e1049dbf373179fd355d%2Fkaggle_self_verification_gantt.png?generation=1776888538729281&alt=media)\n\nWe borrow the core idea — pairwise LLM-as-judge comparisons aggregated into an uncertainty-weighted score — from [V1: Toward Verifiable Reasoning in LLMs](https://harmandotpy.github.io/v1-verification/) by Harman et al. Our pair-selection policy differs: V1 picks the next pair adaptively to maximise information gain, while we use a simpler greedy rule (newest answer vs current undefeated bucket). Our winner-selection scoring stays close to theirs.\n\n### Algorithm\n\nOn top of the voting harness (same model, same sampling, same adaptive budget, same early-stop at four agreeing rollouts), the self-verification variants add three conceptual pieces:\n\n1. **Pairwise grader.** Given the problem and two candidate solutions A and B, the model (at temperature 0) is asked to produce an integer correctness rating in 1–10 for each side. Higher rating wins; equal ratings are a tie. (The full prompt is in Appendix E.)\n2. **Matchup tracker.** Rollout answers are grouped into *buckets* by value (duplicate answers share a bucket). The tracker maintains a directed win/loss graph over buckets, not individual rollouts.\n3. **Winner = undefeated bucket.** After all solutions and verifications complete, we pick the bucket that no other bucket beat, with tie-breaking described below.\n\n### Scheduling policy: filling GPU bubbles\n\nEach time a solution rollout completes, the harness attempts to launch up to two verifications in whichever GPU slots are free, subject to a cap on concurrent rollouts. The tracker picks the next comparison in priority order:\n\n1. The newest answer against a currently undefeated bucket it hasn't yet faced (challenger vs champion) — this is the greedy rule we use in place of V1's information-gain-maximising pick.\n2. Two undefeated buckets against each other, to break ties at the top of the graph.\n3. Any pair of buckets that haven't yet been compared, as a fallback.\n\nCompleted verifications also re-trigger a new comparison if there's still wall-clock budget and solution rollouts are still arriving. Verifications are capped at a fixed maximum per problem so they can't swallow the solver's budget on pathological cases.\n\n### Scoring and winner selection\n\nLet *undefeated* be the set of buckets with zero losses in the win/loss graph. If exactly one bucket is undefeated, we pick it — done. If multiple remain undefeated (or none, in the cycle case), we rank them by an uncertainty-weighted score\n\n$$\\mu_i = \\frac{\\sum_j w_{ij} \\cdot v_{ij}}{\\sum_j w_{ij}}\\,,$$\n\nwhere $v_{ij} \\in \\{1, \\tfrac{1}{2}, 0\\}$ is the outcome (win / tie / loss) of bucket *i* against bucket *j*, and $w_{ij} = \\max(|r_i - r_j| / 9,\\, 0.1)$ scales each comparison by its rating margin: a 10–1 verdict carries an order of magnitude more weight than a 5–4 one, with a floor of 0.1 so ties still count. Further ties break by bucket size (number of rollouts that produced that answer), then by comparison count. If fewer than half the distinct answer buckets have been graded at all, we treat the grading as too sparse to trust and fall back to the entropy-weighted majority vote from the baseline voting solver.\n\nOn the problem in the chart, voting has nothing useful to say: three rollouts say **1**, three say **12**, only two say **13** — the correct answer. Voting breaks the tie by entropy-weighted score, picks `12` (score 5.13 vs `1`'s 4.71 and `13`'s 3.55), and is wrong. The grader only needs three pairwise comparisons to settle it (13 beats 1 with 10–1, 13 beats 12 with 9–2, and 12 beats 1 with 3–1), leaving `13` as the only undefeated bucket. This is the regime self-verification is *supposed* to win: when a single well-reasoned rollout can recognise a majority of sloppy rollouts for what they are.\n\n### Results: why it didn't win\n\nDespite the elegance, the table tells the disappointing story: self-verification underperformed voting on both local validation (32.0 / 33.0 vs 34.7) and public LB (34 vs 36/39/38). We tried two configurations — 8 solution rollouts with verification layered on top, and 6 solution rollouts with more verification budget per problem — and neither beat plain voting.\n\n**When does the grader even do something?** Across all 297 problem-shuffles we ran under self-verification (three variants × two shuffles × ~50 problems), the grader agrees with entropy-weighted voting's top answer **84.5 % of the time (251 / 297)** — meaning on 5 out of every 6 problems the verification layer adds literally nothing. On the remaining 15.5 % (46 cases) the grader flips the decision, and its hit rate on those flips is what really matters:\n\n| Harness | Flips | Grader right<br>(voting wrong) | Grader wrong<br>(voting right) | Both wrong | Net |\n| --- | ---: | ---: | ---: | ---: | ---: |\n| Cascade 2, 8 rollouts, self-verification | 11 | 6 | 3 | 2 | **+3** |\n| Cascade 2, 6 rollouts, self-verification | 11 | 6 | 2 | 3 | **+4** |\n| Cascade 2 + Eagle3, 6 rollouts | 24 | 10 | 10 | 4 | **0** |\n| **All self-verif (aggregate)** | **46** | **22** | **15** | **9** | **+7 / 297** |\n\nSo on the two non-Eagle3 runs the grader is net positive but only barely — +7 correct answers across 297 problem-shuffles, roughly 2.4 %. The Eagle3 run is the real tell: its 33 % throughput headroom lets the scheduler fire *more than twice as many* flip-candidate comparisons (24 vs 11), and the extra firings split right down the middle — ten wins cancelled by ten losses. More grading, no net gain. At the margins where voting and the grader disagree, the grader is essentially a coin flip.\n\n![Two views of the same 50-problem runs under the 5 h Kaggle-like setting, comparing voting (our final) against two self-verification harnesses — the 8-rollout variant and a 6-rollout variant run with Eagle3 (its numbers have been normalised onto the same 300–900 s / 4.83 h axes as everyone else). Left: per-problem wall-clock time — dots are means across shuffles, bars are ±1 SD. Right: the adaptive budget the scheduler handed each problem, capped between 900 s (default) and 300 s (floor); bold line is the mean across shuffles, faint lines are individual shuffles. Voting stays near the 900 s ceiling for most of the run. Both self-verification variants drop off the 900 s line within the first ten problems and spend most of the run in the 600–700 s band.](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2Fc2f032a216884a70d93e74af479db525%2Fkaggle_self_verification_time_budget.png?generation=1776888592784811&alt=media)\n\nOur read: the grader is too lenient to break ties confidently when both answers are backed by plausible-looking reasoning, and — as the chart above makes clear — at 1× H100 SXM there simply isn't enough wall-clock budget to run the 15–20 comparisons per problem the tracker would need to converge on harder problems. The per-problem means are close across all three runs (329 s voting vs 342 s self-verification vs 339 s with Eagle3), but the tails tell a very different story: voting's 90th-percentile problem consumes 472 s, self-verification's reaches 589 s, and with Eagle3 it stretches to 796 s — the longer the grader can keep running comparisons, the longer individual problems drag on, and the scheduler simply squeezes the remaining budget harder. Eagle3 buys roughly a 33 % throughput improvement (our motivation for training it in the first place), but even on that budget the extra complexity of the verification layer didn't translate into more correct answers — the Eagle3 run landed at **33.5 / 50** (mean of 36 and 31), which is no better than the plain self-verification variant and still behind voting's 34.7. The ingredients — fast model, honest grader — were right; the budget wasn't.\n\n| **Submission**<br>(model, harness) | **Local validation**<br>(3 shuffles) | **Local val**<br>(mean) | **Tool err**<br>(per-call) | **Public LB attempts** |\n| --- | --- | --- | --- | --- |\n| gpt-oss-120b, 44/50 public notebook | 33, 30, 36 | 33.0 | 15.0% | 38, 38, 37 |\n| Cascade 2, 8 rollouts, self-verification | 33, 31 | 32.0 | 5.1% | 34 |\n| Cascade 2, 6 rollouts, self-verification | 35, 31 | 33.0 | 5.2% | never submitted |\n| Cascade 2 + Eagle3, 6 rollouts, self-verification | 36, 31 | 33.5 | 5.2% | never submitted |\n| **Cascade 2, 8 rollouts, voting<br>(our final submission)** | **36, 34, 34** | **34.7** | **6.0%** | **36, 39, 38** |\n\n\n---\n\n\n## Conclusion\n\n**The boring harness wins.** Our final submission is the simplest thing that works: 8 rollouts of soft-cloned FP8 Cascade 2 with Python tool use, an adaptive 300–900 s per-problem budget, early-stop at 4 agreeing answers, and an entropy-weighted majority vote. On the 3-shuffle local validation (IMO-AnswersBench, 5 h, 1× H100 SXM) it scores **34.7 / 50**, ahead of the public gpt-oss-120b 44/50 notebook's **33.0 / 50**, and it ties gpt-oss-120b on the public LB (mean 37.7, peak **39 / 50** vs 38). Every more-sophisticated Cascade 2 harness we tried — pairwise-comparison self-verification, the same plus Eagle3-accelerated inference — underperformed the baseline. We believe self-verification could beat voting with more tuning, but not in the 24 hours we had before the deadline.\n\n**What the side quests produced.** Three open-source artifacts that outlast the competition: a family of quantized Cascade 2 checkpoints (FP8, NVFP4, AWQ-INT4 — 66K downloads and counting), a vLLM fork that correctly runs prefix caching alongside speculative decoding on hybrid-Mamba models (long-standing open problem), and an EAGLE-3 speculator for Cascade 2 reaching 3.55 mean accepted tokens at K=7 along with the SpecForge modifications needed to train it at 64 k context. We also filed a note on Cascade 2's tendency to quietly search the web for answers through its Python tool.\n\n**Why this is mostly a first-try harness.** We joined the competition late and the Kaggle 1-submission-per-day cap meant essentially every choice we shipped was a first-try call, with no headroom to A/B-test on the hidden LB. That's also why our scheduling, early-stop, and budget policy are deliberately simple: we picked Cascade 2 specifically because it offered the best **maj@k under a time budget**, and our priors from AIMO 2 were that winning solutions in this class of competition optimized maj@k rather than chasing pass@k at infinity — pass@∞ tends to be similar across competitive models, but maj@k under a real time budget is where the consistency premium pays off. A more elaborate per-problem scheduling system was on the wishlist (several contestants reported good gains from one), but the engineering dependency we did spend weeks on — PC + SD for hybrid-Mamba models in vLLM — was already over our time budget, and we judged that getting the simple harness right was a safer use of the remaining days than tuning a scheduler we couldn't validate without using up the submission slots.\n\n**What we'd do differently.** Finalize PC + SD support in vLLM earlier (this was the engineering dependency that would have unlocked a genuinely competitive self-verification harness), start the self-verification work on day one instead of the last 24 hours, spend some time tuning the grader (prompt, rating scale, pair-selection policy), and develop a smarter per-problem scheduling policy that exploits the random hard/easy ordering. The ingredients were right; we just ran out of room to tune them. Even so, we're glad we got side-tracked — the open-source artifacts are the part of this project we're most proud of.\n\n\n---\n\n\n## References\n\n* Zhuolin et al. [\"Nemotron-Cascade 2: Post-Training LLMs with Cascade RL and Multi-Domain On-Policy Distillation\"](https://arxiv.org/abs/2603.19220) (2026).\n* OpenAI. [\"gpt-oss-120b & gpt-oss-20b Model Card\"](https://arxiv.org/abs/2508.10925) (2025).\n* Natapong Nitarach. [\"Model Capability Dominates: Inference-Time Optimization Lessons from AIMO 3\"](https://arxiv.org/abs/2603.27844) (2026).\n* Harman et al. [\"V1: Unifying Generation and Self-Verification for Parallel Reasoners\"](https://arxiv.org/abs/2603.04304v1) (2026).\n* Woosuk et al. [\"Efficient Memory Management for Large Language Model Serving with PagedAttention\"](https://arxiv.org/abs/2309.06180), in *SOSP '23: Proceedings of the 29th Symposium on Operating Systems Principles* (2023).\n* Yuhui et al. [\"EAGLE-3: Scaling up Inference Acceleration of Large Language Models via Training-Time Test\"](https://arxiv.org/abs/2503.01840), in *Annual Conference on Neural Information Processing Systems* (2025).\n\n---\n\n# Appendix\n\n## Appendix A: soft-cloning the Cascade 2 model\n\n### Question: Why is this legal? Can this be even considered distillation?\n\n* Semantically and mathematically this is a distillation: the \"students\" weights NEVER optimize directly towards \"teachers\" weights, only through observation of the inputs/outputs $X$ and $Y$ of functions $F_\\text{teacher}(X)$, where $F_\\text{teacher}$ happens to be a linear mapping.\n* This is different from \"non-legal\" [approach with GPT-OSS-Puzzle-88B in this discussion](https://www.kaggle.com/competitions/ai-mathematical-olympiad-progress-prize-3/discussion/680682#3437318), because that approach is basically an interpolation $W_\\text{final} = a*W_\\text{gptoss88b} + b*W_\\text{finetuned}$, which still directly involves the weights of non-legal model. Here, none of the weights of teacher are used in runtime student model.\n\n### Implementation details\n\nThe recovery loop runs per tensor, one tensor type at a time. For each teacher tensor we (1) generate random probe inputs $X \\sim \\mathcal{N}(0, 1)$, (2) observe the teacher's output $Y = f_\\text{teacher}(X)$ — this is the only step that touches teacher values — and (3) solve for student parameters that minimise the mean-squared error $\\lVert f_\\text{student}(X) - Y \\rVert^2$. Linear layers $y = W x$ are recovered in closed form via the normal equations (it's already an obvious MSE minimiser and the loop would add ~100× runtime for no benefit). The other three tensor types — depthwise Conv1d kernels, 1D scalar vectors ($y = w \\odot x$; norms, SSM $A_{\\log}$, $d_t$, $D$), and additive biases ($y = x + b$) — all run an explicit SGD loop with a learning rate that cosine-decays to 0 over 500 steps. Max learning rates are exposed as CLI flags; the choices below come from stability arguments on the per-parameter Hessian.\n\nEach recovery loss is a convex quadratic, and SGD is stable exactly when $\\mathrm{lr} < 2/\\lambda_{\\max}(H)$ with $H$ the per-parameter Hessian. For the $y = w \\odot X$ and $y = X + b$ cases the loss is separable across features, so with the normalisation we use (mean over samples, sum over features) every parameter sees the same scalar Hessian $H = 2\\,\\mathbb{E}[X^2] = 2$ for $X \\sim \\mathcal{N}(0, 1)$ — hence $\\mathrm{lr} = 0.5 \\approx 1/H$ lands each element at its optimum in $O(1)$ effective steps, independent of $N$ and of the magnitude of the target.\n\nThe unfolded-window conv1d Hessian is not a scaled identity. Its $(k_1, k_2)$ entry is the sample autocorrelation of standard noise at lag $|k_1 - k_2|$: diagonal $\\approx T$, off-diagonals have $\\mathcal{O}(\\sqrt{T})$ noise fluctuations (plus small deterministic shifts near the zero-padded boundary of the window). Divided by $T$ in the loss, the spectrum concentrates around $2$ but spreads to roughly $[1.3, 3.5]$ at typical Mamba sizes ($T = K + 32 = 36$, $K = 4$). $\\mathrm{lr} = 0.5$ is right on the stability edge for $C \\lesssim 10^3$ and clearly past it by $C \\gtrsim 4 \\cdot 10^3$ (it diverges with extreme numeric blow-up). $\\mathrm{lr} = 0.3$ sits inside $2 / 3.5 \\approx 0.57$ with comfortable margin across every Cascade 2 size we ran.\n\n## Appendix B: quantization details\n\n### Precision recipe — full layer breakdown\n\nThe per-component precisions behind the layout chart in the main section. Each row maps an architectural component to the precision it's kept at in the FP8 and NVFP4 checkpoints, following the NVIDIA Nano sensitivity recipe.\n\n| Component | Precision (FP8) | Precision (NVFP4) | Comment |\n| --- | --- | --- | --- |\n| MoE expert GEMMs (routed + shared) | **FP8** | **NVFP4** | All 23 MoE layers, 128 routed + 2 shared experts each |\n| Mamba GEMMs (non-adjacent) | **FP8** | **NVFP4** | 17 of 23 Mamba layers |\n| Attention layers (all 6) | **BF16** | **BF16** | Most sensitive — kept BF16 per NVIDIA sensitivity analysis |\n| Mamba layers adjacent to attention (6) | **BF16** | **BF16** | Layers {4, 11, 18, 25, 32, 41} — found sensitive in ablations |\n| Mamba 1D conv | **BF16** | **BF16** | All layers |\n| Router gates | **FP32** | **FP32** | Routing precision must not degrade |\n| Embeddings & lm_head | **BF16** | **BF16** | Not quantized |\n| KV cache | **FP8** | **FP8** | All 6 attention layers |\n\n### Evaluation setup\n\nAll benchmark runs use [NVIDIA-NeMo/Evaluator](https://github.com/NVIDIA-NeMo/Evaluator) with the [Nemotron-3-Super-120B tool-use config](https://github.com/NVIDIA-NeMo/Evaluator/blob/main/packages/nemo-evaluator-launcher/examples/nemotron/nemotron-3-super/local_nemotron-3-super-120b-a12b_tools.yaml): 8 rollouts per problem, temperature 1.0, tool use enabled (sandbox code execution), `enable_thinking=true`. Same config across the BF16 reference and our FP8 / NVFP4 checkpoints so numbers are directly comparable.\n\n> **Critical serving flag.** NemotronH's SSM state must stay in FP32 at inference. vLLM 0.19 reads this from the model's `config.json` by default (Cascade 2 ships the override), but anyone serving a variant whose config doesn't include it should pass `--mamba_ssm_cache_dtype float32` explicitly. Leaving the SSM state at BF16 costs **~10% absolute** on AIME-class benchmarks — silent precision regression, no crash. The same applies to SGLang (`--mamba-ssm-dtype float32`), which defaults to BF16.\n\n### Verification results\n\n| Benchmark | [Nemotron-Cascade-2-30B-A3B](https://huggingface.co/nvidia/Nemotron-Cascade-2-30B-A3B)<br>(reproduced results) | Our FP8 model | Our NVFP4 model |\n| --- | --- | --- | --- |\n| AIME 2025 (avg@8) | 98.8 | **96.7** | **97.9** |\n| AIME 2026 (avg@8) | 94.2 | **95.0** | **92.1** |\n| HMMT Feb 2025 (avg@8) | 92.9 | **93.8** | **90.1** |\n\n*With the low sample count (8 rollouts per problem), a deviation of ±2% across runs is expected. Safe to say BF16, FP8, and NVFP4 variants are equivalent in reasoning performance.*\n\n## Appendix C: Eagle3 training details\n\n### Per-position acceptance\n\nAt the sweet spot (K=4, T=0.7 on AIME-25):\n\n| Position | 1 | 2 | 3 | 4 | 5 |\n| --- | --- | --- | --- | --- | --- |\n| Accept rate | 73.3% | 49.8% | 34.5% | 24.7% | 17.3% |\n\nRates decay geometrically with a ratio of ~0.7 per position — each additional draft token needs all previous drafts to verify first, and the marginal drafter confidence drops with depth.\n\n### Memory optimizations\n\nNaïve training at L=65k, ttt=6 peaks at 120+ GiB — OOM on a 96 GB GPU. Four independent activation-memory cuts on the draft forward + TTT unroll bring peak down to ~88–91 GiB, approximately `63 GiB verifier weights + 4.2 GiB target_p_padded + 12 GiB attention activations + 4.2 GiB MLP saves + ~5–8 GiB overhead`:\n\n- **Free verifier full-vocab logits before TTT unrolling** (biggest single win, ~30 GiB saved across the unroll). The HF target backend returned the verifier's `[seq, 131072]` logits as `target` (~16 GiB at L=65k), held through three independent references across the draft forward + backward. Precomputing a small draft-vocab `target_p_padded` (~4 GiB) and dropping the rest before `eagle3_model.forward()` releases the full-vocab tensor before the expensive phase. Dropping the `.float()` upcast on the softmax saves another ~8 GiB.\n- **Chunked fused linear + soft-target CE** for the draft lm_head (~24 GiB saved across ttt=6 unrolls). The `[B, T, V]` logits tensor is never materialized — the existing `LogSoftmaxLoss` Triton kernel runs inside a chunked + grad-checkpointed wrapper. Numerically equivalent to the unchunked path on both `hidden_states` and `lm_head_weight` grads, verified at realistic shapes.\n- **Chunked MLP forward** (Liger-style, sequence-dim chunking; ~3.6 GiB per-step transient reduction). MLP intermediates shrink from ~4.4 GiB to ~0.8 GiB at L=65k, intermediate=8064. Composes cleanly with grad-checkpointing.\n- **Sliding-window attention on the draft** (modest direct memory win, but unblocks ttt=6 at L=65k by bounding attention from O(L²) to O(L × window)), plumbed through `LlamaForCausalLMEagle3`, the flex_attention mask generator, and the causal-mask builder. The TTT suffix block is intentionally not windowed.\n\nThe four savings don't sum linearly — some overlap (e.g., freeing target logits shrinks the memory available for future cuts to operate on). Aggregate per-step wall time is ~4 s (1.2 s on short samples up to 8.6 s on near-max-length conversations at L=65k); grad-checkpointed MLP costs ~8–12% per-step recompute overhead, which the memory savings more than pay for.\n\n### Loss-mask parser (100× speedup)\n\n`GeneralParser.parse()` was running per-turn re-tokenization in O(num_turns × seq_len) — for long reasoning traces with many assistant turns this took several seconds per conversation. We replaced it with a single `return_offsets_mapping` + bisect pass to map character spans to token spans in O(num_turns × log seq_len). Per-conversation preprocessing dropped from several seconds to ~30 ms on long reasoning traces.\n\n\n## Appendix D: test-time scaling methodology\n\n**Attempt-count view.** What the time-budget chart in the body would look like if we drop the per-rollout-cost scaling and plot against raw `k`. Same data, x-axis = k instead of k × seconds-per-rollout.\n\n![pass@k and maj@k vs number of attempts k ∈ {1, …, 8} on the 63-problem offline benchmark.](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F2270821%2F02418e1ddd660c78b71374af498f4ef3%2Ftest_time_scaling_attempts.png?generation=1779062011546385&alt=media)\n\n**Time-budget chart.** For each model m and k ∈ {1, …, 8}, we plot the (pass@k, maj@k) point at `x = k × avg_tokens_per_rollout(m) / tok-per-sec(m)` seconds, using the 8-parallel-sequence throughput from the generation-speed table. pass@k and maj@k are computed exactly (no Monte-Carlo) by enumerating all `C(8, k)` subsets of the 8 available sessions per problem and averaging across the 63 problems. maj@k uses the most-common answer rule: for each subset, the modal answer wins, ties split uniformly across tied answers.\n\n**Reasoning-time budget chart.** For each truncation length L ∈ {16k, 32k, 48k, 64k, 80k, 96k}:\n\n- Truncate each session's model-generated text at L tokens.\n- Find the last `\\boxed{…}` inside the truncated portion. If found, that token position is the session's *effective length* and its original predicted answer counts. If not found, the session is treated as \"no answer\" at that L and its effective length is L.\n- Mean effective length across the 504 sessions gives the raw token x-coordinate, which we convert to seconds by dividing by the per-sequence throughput at 8 parallel sequences (101.4 tok/s/seq for gpt-oss-120b from 810.9/8; 166.7 tok/s/seq for Cascade 2 and nano30b from 1333.5/8 — nano30b uses Cascade 2's throughput since the architectures are identical). y is avg@1 (fraction of 504 sessions correct after truncation) or maj@8 (most-common-answer rule over the 8 truncated answers per problem, averaged across 63 problems).\n\n**Tokenization.** Per-model, exact:\n\n- gpt-oss-120b: `num_completion_tokens` from each trace record (the model's own Harmony token count for generated output, exclusive of prompt and tool results). Because gpt-oss emits `\\boxed{…}` as its final generation, we approximate its `\\boxed{}`-end token position as the total generated-token count.\n- Cascade 2 / nano30b: we tokenize the `generation` text with the Nemotron-Cascade-2 tokenizer (`nvidia/Nemotron-Cascade-2-30B-A3B`) and compute both the total and the `\\boxed{}`-end positions exactly by running the tokenizer on the full text and on the prefix up to the closing brace of the last `\\boxed{…}`.\n\n## Appendix E: self-verification grader prompt and hyperparameters\n\nEach pairwise verification is its own chat session with Python tool use enabled. System message:\n\n```\nYou are a helpful assistant and an expert math contest grader.\n```\n\nUser message (fields `{problem}`, `{sol_A}`, `{sol_B}` substituted from the current problem and two competing solution texts):\n\n```\nCompare two submissions and rate correctness based solely on the final answer.\n\n**Evaluation Guidelines:**\n- Extract each submission's final answer. Use any provided reasoning only\n  to help you assess whether the stated final answer is trustworthy. Do\n  not award credit for method quality or rigor.\n- Carefully analyze the problem statement and the submissions to assess\n  whether each final answer is correct. Grade only the final answer.\n**Problem**\n{problem}\n**Solution A**\n{sol_A}\n**Solution B**\n{sol_B}\n**Output Format:**\nFirst, provide your reasoning (what checks you performed). Then, on\nseparate new lines, give ratings using the EXACT tags below. Add no\nother text after the tags.\n<rating_A>INTEGER_1_TO_10</rating_A>\n<rating_B>INTEGER_1_TO_10</rating_B>\n**Rating Rules:**\n- Rate correctness on a 1-10 scale (10 = certainly correct, 8 = very\n  likely correct, 5 = uncertain/borderline, 3 = likely incorrect, 1 =\n  certainly incorrect).\n- Higher rating wins. Equal ratings imply a tie.\nPlease provide your analysis now.\n```\n\nWe parse the rating pair out of the model's final message with an XML-tag regex first, then fall back to `\\boxed{N}` per side if the model used its trained `\\boxed{}` format instead of the requested tags.\n\n## Appendix F: final LB submission — configuration\n\nAll the knobs we actually shipped for [chankhavu/c2-distill-vllm018-watchdog](https://www.kaggle.com/code/chankhavu/c2-distill-vllm018-watchdog).\n\n**Model.** `chankhavu/c2-legaldistill-30b-a3b-fp8` — the FP8-quantized soft-clone of Nemotron-Cascade-2, re-quantized specifically for the Kaggle submission (not our published open-source FP8 checkpoint; see the \"Quantizing the Cascade 2 model\" section). Runtime: vLLM 0.18.0 on 1× H100 SXM under the Kaggle inference server sandbox.\n\n**vLLM serve command.** Single GPU, FP8 KV cache, large context window, chunked prefill, prefix caching, async scheduling, native function-calling enabled with the `qwen3_coder` tool-call parser:\n\n```bash\nvllm serve /path/to/c2-legaldistill-30b-a3b-fp8 \\\n    --served-model-name chankhavu/c2-legaldistill-30b-a3b-fp8 \\\n    --tensor-parallel-size 1 \\\n    --max-num-seqs 64 \\\n    --gpu-memory-utilization 0.9 \\\n    --dtype auto \\\n    --max-model-len 262144 \\\n    --max-num-batched-tokens 8192 \\\n    --kv-cache-dtype fp8 \\\n    --mamba-ssm-cache-dtype float32 \\\n    --mamba-cache-mode all \\\n    --mamba-block-size 512 \\\n    --enable-chunked-prefill \\\n    --enable-prefix-caching \\\n    --async-scheduling \\\n    --enable-auto-tool-choice \\\n    --tool-call-parser qwen3_coder \\\n    --trust-remote-code \\\n    --host 0.0.0.0 --port 8000\n```\n\nWe also run a lightweight watchdog subprocess that polls `/proc` every 30 s and relaunches vLLM if the APIServer / EngineCore processes disappear (scrapes the `VLLM::APIServer` / `VLLM::EngineCor...` `comm` names — the literal `vllm.entrypoints` string stops appearing in `cmdline` after vLLM calls `setproctitle` during startup). The relaunch itself takes ~100 s, which is why the watchdog restart trigger is \"no live vllm process found\" rather than HTTP-based.\n\n**Solver configuration** (from `CFG`):\n\n| Setting | Value | Notes |\n| --- | --- | --- |\n| `attempts` | 8 | Solution rollouts per problem |\n| `workers` | 16 | Thread pool size (2× `attempts`, so cancellations don't stall new launches) |\n| `early_stop` | 4 | Stop once 4 rollouts agree on an integer answer |\n| `turns` | 128 | Max conversation turns per rollout |\n| `temperature` | 1.0 | Sampling diversity — all rollouts share the prompt |\n| `top_p` | 0.95 | |\n| `top_logprobs` | 5 | Top-5 logprobs for entropy computation |\n| `enable_thinking` | `True` | Cascade 2's `<think>…</think>` reasoning |\n| `context_tokens` | 262144 | vLLM `--max-model-len` |\n| `max_completion_tokens` | 131072 | Per-turn completion cap |\n| `notebook_limit` | 17400 s (4.83 h) | Kaggle inference ceiling |\n| `high_problem_timeout` | 900 s | Default per-problem budget |\n| `base_problem_timeout` | 300 s | Reserved per remaining problem; also the per-problem floor |\n| `python_timeout` | 30 s | Per Python tool call |\n| `max_output_characters` | 1000 | Sandbox stdout truncation |\n| `seed` | 42 | Unused (vLLM picks fresh seeds per request — we *want* the cross-worker diversity) |\n\n**Adaptive budget formula.** For each problem we compute\n`budget = min(high_problem_timeout, max(base_problem_timeout, time_left − base_problem_timeout × problems_left_others))`, so early problems get the full 900 s; as the wall clock runs down we reserve 300 s per remaining problem and squeeze the current one toward the 300 s floor.\n\n**System prompt.**\n\n```\nYou are a helpful assistant and a mathematical problem solver.\nSolve the given problem step by step using rigorous reasoning.\n```\n\n**User template** (`{problem}` substituted per problem):\n\n```\n{problem}\n\nThe answer is a non-negative integer between 0 and 99999, inclusive.\nPut your final answer in \\boxed{}.\n```\n\n**Tool definition** — the model is given exactly one function, a stateful IPython code executor:\n\n```json\n{\n  \"type\": \"function\",\n  \"function\": {\n    \"name\": \"stateful_python_code_exec\",\n    \"description\": \"Call this function to execute Python code in a stateful Jupyter notebook environment. Python will respond with the output of the execution or time out after 120.0 seconds. The environment has math, numpy, sympy, itertools, collections, and mpmath pre-imported.\",\n    \"parameters\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"code\": {\"type\": \"string\", \"description\": \"Code to execute\"}\n      },\n      \"required\": [\"code\"]\n    }\n  }\n}\n```\n\nPer-call timeout is actually 30 s (from `CFG.python_timeout`), not 120 s — the description is the one the Cascade 2 model was trained to expect, so we leave it unchanged.\n\n**Sandbox preimports.** Every new session starts with:\n\n```python\nimport math\nimport numpy\nimport sympy\nimport itertools\nimport collections\nimport mpmath\nmpmath.mp.dps = 64\n```\n\n**Answer extraction.** A small regex cascade applied to each rollout's final non-tool-call message, in order: (i) `\\boxed{DIGITS}` — pick the last one; (ii) `\\boxed{… = DIGITS}` — pick the last integer inside the last `\\boxed{}`; (iii) `final answer is DIGITS`. Accepts integers in `[0, 99999]` only. Before scanning, `<think>…</think>` reasoning blocks are stripped.\n\n**Voting.** Each rollout's weight is `1 / max(mean_token_entropy, 1e-9)`, where the per-token entropy is computed from the top-5 logprobs of the generated tokens (not tool-call bytes). The picked answer is the one with the highest sum of weights across the rollouts that produced it.\n\n**Hyperparameters.** Grader runs at temperature 0, top_p 0.95, same Python sandbox as the solver. Tool-call budget per grader session: 15 calls; past that we force-respond to any pending tool calls with a stub and instruct the model to output its ratings immediately. Maximum verifications per problem: 16. Maximum concurrent rollouts (solution + verification combined) in the thread pool: 8.\n\n## Appendix G: tool-call error breakdown (gpt-oss-120b vs Cascade 2)\n\nWhy does gpt-oss-120b's tool-call overhead come out roughly 2× Cascade 2's? The whole question is really about **time**: a typical gpt-oss-120b rollout burns **37 s of wall-clock on errored or timed-out tool calls — 14.0% of its 266 s average rollout — vs 17 s for Cascade 2 (6.3% of its 272 s rollout)**. ~2.2× the absolute, ~2.2× the rollout fraction. All numbers below are from the same LB-simulation runs that feed the chart in §Per-problem breakdown (aimo50a/b/c, 3 shuffles × 50 problems × up to 8 rollouts per problem, 1× H100 SXM under the 5 h Kaggle budget).\n\nPer-rollout averages — every column below is a mean across rollouts (912 for gpt-oss-120b, 850 for Cascade 2; < 50×3×8 = 1200 because the adaptive scheduler cancels in-flight rollouts once 4 agree on an answer). In the table, \"error\" means *any* failed tool call — Python exceptions and the 30 s sandbox timeouts together.\n\n| Model | rollout sec | tool calls | tool sec | **error sec** | **% rollout errored** |\n| --- | ---: | ---: | ---: | ---: | ---: |\n| gpt-oss-120b | 266 | 18.4 | 66.3 | **37.3** | **14.0%** |\n| Cascade 2 | 272 | 14.3 | 32.4 | **17.1** | **6.3%** |\n\nWithin each model, where the error+timeout time goes (`% errs` and `% err-time` are within-model fractions of the *error+timeout* totals):\n\nAll low-frequency classes (< 0.1 occurrences per rollout in both models) are collapsed into \"other\" — this includes RuntimeError, ValueError, KeyError, IndexError, ImportError, and uncategorized Python tracebacks.\n\n| Class | gpt-oss-120b<br>count/rollout | sec/rollout | % errs | % err-time | Cascade 2<br>count/rollout | sec/rollout | % errs | % err-time |\n| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |\n| **[TIMEOUT]** (30 s cap) | **1.01** | **33.1** | 36.4% | **88.6%** | 0.51 | 16.0 | 33.1% | **94.0%** |\n| **SyntaxError** | **0.72** | 0.3 | 26.1% | 0.9% | 0.11 | 0.0 | 7.3% | 0.1% |\n| **NameError** | 0.49 | 2.3 | 17.8% | 6.0% | 0.15 | 0.1 | 9.7% | 0.7% |\n| TypeError | 0.17 | 0.1 | 6.1% | 0.3% | 0.23 | 0.0 | 15.1% | 0.2% |\n| AttributeError | 0.10 | 0.0 | 3.6% | 0.1% | 0.10 | 0.1 | 6.5% | 0.4% |\n| other | 0.28 | 1.5 | 10.0% | 4.1% | 0.41 | 0.7 | 28.3% | 4.6% |\n| **All errored + timed-out** | **2.77** | **37.3** | 100% | 100% | **1.53** | **17.1** | 100% | 100% |\n\n**Timeouts are ~90% of the wasted wall-clock for both solvers** (88.6% / 94.0%) — every Python error class is a rounding error by time.\n\n**NameError carries the strongest cross-model signal** (0.49/rollout for gpt-oss-120b vs 0.15 for Cascade 2). gpt-oss-120b's 451 extractable NameErrors split into two causes:\n\n- ~80 cases — **forgot to import.** gpt-oss-120b writes Jupyter-cell-style snippets that assume `sp` (16), `random` (15), `deque` (9), `math` (5), and other stdlib aliases are already live, but the pre-import block (`math, numpy, sympy, itertools, collections, mpmath`) doesn't expose them and the model rarely writes the `import` line itself.\n- The remaining ~370 — **sandbox rollback.** Standard Jupyter/IPython preserves partial assignments after an exception; ours snapshots state *before* each cell and restores it on error, so variables defined earlier in a failed cell are wiped. Models trained on REPL semantics write follow-up code referencing those now-gone names. Cascade 2's 126 NameErrors are mostly the same rollback pattern with user-defined helpers — just hit less often.",
      "votes": 17
    },
    {
      "id": 3458341,
      "postDate": "2026-05-15T16:43:35.753Z",
      "content": "<p>Following up on the questions from <a href=\"https://www.kaggle.com/philipvonderlind\" target=\"_blank\">@philipvonderlind</a>, here are some additional questions to help us assess your submission:</p>\n<ol>\n<li><strong>AnswerBench contamination concern:</strong> Cascade 2 was released in March 2026, a few months after IMO-AnswerBench was published, whereas gpt-oss-120b was released earlier. Are you worried that Cascade 2's higher score on your local validation set (drawn from AnswerBench) might partly reflect AnswerBench problems having been in Cascade 2's training data, rather than a fair reasoning comparison against gpt-oss-120b?</li>\n<li><strong>Why Cascade 2 over a gpt-oss-120b verifier path:</strong> Your offline numbers show gpt-oss-120b has the higher pass@8 (77.8% vs Cascade 2's 74.6%), and you note an oracle verifier would pick gpt-oss-120b. You then invested heavily in the soft-clone + quantization + speculator pipeline for Cascade 2. With hindsight, what made you commit to the Cascade 2 route rather than building (e.g. a learned) verifier for gpt-oss-120b that could close the maj@8 → pass@8 gap?</li>\n</ol>\n<p>Look forward to reading your responses on these.</p>",
      "rawMarkdown": "Following up on the questions from @philipvonderlind, here are some additional questions to help us assess your submission:\n\n1. **AnswerBench contamination concern:** Cascade 2 was released in March 2026, a few months after IMO-AnswerBench was published, whereas gpt-oss-120b was released earlier. Are you worried that Cascade 2's higher score on your local validation set (drawn from AnswerBench) might partly reflect AnswerBench problems having been in Cascade 2's training data, rather than a fair reasoning comparison against gpt-oss-120b?\n2. **Why Cascade 2 over a gpt-oss-120b verifier path:** Your offline numbers show gpt-oss-120b has the higher pass@8 (77.8% vs Cascade 2's 74.6%), and you note an oracle verifier would pick gpt-oss-120b. You then invested heavily in the soft-clone + quantization + speculator pipeline for Cascade 2. With hindsight, what made you commit to the Cascade 2 route rather than building (e.g. a learned) verifier for gpt-oss-120b that could close the maj@8 → pass@8 gap?\n\nLook forward to reading your responses on these.",
      "votes": 1,
      "replies": [
        {
          "id": 3459537,
          "postDate": "2026-05-17T23:58:58.033Z",
          "content": "<p>Thanks a lot, Sam Bealing (@sambealing) for thoughtful questions! We had similar concerns during AIMO3. In fact, we were skeptical about Cascade 2 at first and were not sure if it was benchmaxxed. Here is why we ultimately chose to trust it.</p>\n<h2>AnswerBench contamination</h2>\n<blockquote>\n  <p>and also AnswerBench contamination concern: Cascade 2 was released in March 2026, a few months after IMO-AnswerBench was published, whereas gpt-oss-120b was released earlier. Are you worried that Cascade 2's higher score on your local validation set (drawn from AnswerBench) might partly reflect AnswerBench problems having been in Cascade 2's training data, rather than a fair reasoning comparison against gpt-oss-120b?</p>\n</blockquote>\n<p>This is a legitimate concern and we've added a note to <em>§Evaluation set</em> acknowledging it explicitly. Short version:</p>\n<ul>\n<li><strong>Both models are at some risk.</strong> IMO-AnswerBench is a rewriting of previously-published IMO problems, Shortlists, and national olympiads. Both gpt-oss-120b and Cascade 2 were trained on data that almost certainly includes some of these problems in their original form. So contamination is <em>the baseline</em>, not a one-sided concern.</li>\n<li><strong>Cascade 2 is more exposed than gpt-oss-120b.</strong> Cascade 2 was released after AnswerBench was published, so the contamination window for Cascade 2 is wider. The reasoning advantage we measure should be discounted accordingly when comparing absolute accuracy.</li>\n<li><strong>We did not run our own contamination checks.</strong> We chose to trust the data-curation procedure described in the Nemotron-Cascade-2 paper, which uses n-gram deduplication against known evaluation benchmarks. If that procedure works as documented, the contamination on AnswerBench specifically should be limited — but we can't verify this independently.</li>\n<li><strong>What this doesn't undermine:</strong> the <em>relative</em> observations that hold up regardless of contamination. The per-rollout cost difference, the maj@k stability difference, the tool-call error breakdown, the time-budget scaling shape — none of these depend on absolute reasoning ability. If Cascade 2's true reasoning capability is lower than its AnswerBench number suggests, our conclusions about \"Cascade 2 wins maj@k under time budget <em>because</em> it's cheaper per rollout and more consistent across rollouts\" still hold; we'd just be over-reporting the absolute scoreboard.</li>\n</ul>\n<p>The contamination caveat is now in the writeup body so the reader can weight the absolute numbers themselves.</p>\n<h2>Why Cascade 2 over a gpt-oss-120b verifier path</h2>\n<blockquote>\n  <p><strong>Why Cascade 2 over a gpt-oss-120b verifier path:</strong> Your offline numbers show gpt-oss-120b has the higher pass@8 (77.8% vs Cascade 2's 74.6%), and you note an oracle verifier would pick gpt-oss-120b. You then invested heavily in the soft-clone + quantization + speculator pipeline for Cascade 2. With hindsight, what made you commit to the Cascade 2 route rather than building (e.g. a learned) verifier for gpt-oss-120b that could close the maj@8 → pass@8 gap?</p>\n</blockquote>\n<p>A few interacting reasons, in roughly the order they mattered:</p>\n<ol>\n<li><strong>The verifier route doesn't fit on the H100.</strong> gpt-oss-120b's MXFP4 weights are ~120 GB; that leaves ~60 GB of HBM for KV cache, activations, and the 8 parallel solution rollouts we already need to fit. There is no headroom on a single H100 SXM for a <em>separate</em> verifier model running alongside it. Using gpt-oss-120b as its own verifier doubles the per-problem token bill on a budget that, at our throughput, already only fits ~8 solution rollouts. Both forms of \"gpt-oss-120b + verifier\" run out of either memory or wall-clock before they can be meaningfully tuned.</li>\n<li><strong>Cascade 2 leaves room for verification later.</strong> Cascade 2's smaller footprint and ~70% higher throughput meant we could <em>start</em> with plain voting and then layer self-verification in the GPU bubbles between fast and slow solution rollouts. The self-verification harness we shipped (untuned, last 24 h) underperformed voting, but the option existed at all because of Cascade 2's overhead. With gpt-oss-120b we wouldn't have had that fallback path even in principle.</li>\n<li><strong>Training a learned verifier is its own project.</strong> \"Build a learned verifier for gpt-oss-120b that closes maj@8 → pass@8\" is a meaningfully larger undertaking than the soft-clone we shipped: it needs verifier-training data (problem + multiple solutions + correctness labels), a training run, an eval harness for the verifier itself, and integration into the inference path.</li>\n<li><strong>AIMO 2 priors.</strong> The historical evidence from AIMO 2 was that winning solutions optimized maj@k under a real time budget, not pass@∞. Cascade 2 <em>measurably</em> wins maj@k under our time budget; gpt-oss-120b would have needed a working verifier to compete on the same axis. Investing in the model that already wins the relevant metric was the lower-variance bet.</li>\n</ol>\n<p>The resource-constraint version of this argument is now in the writeup as a follow-up paragraph after the oracle-verifier note in §Test-time scaling. The training-time / risk-budget version we hadn't articulated; thank you for prompting it.</p>",
          "rawMarkdown": "Thanks a lot, Sam Bealing (@sambealing) for thoughtful questions! We had similar concerns during AIMO3. In fact, we were skeptical about Cascade 2 at first and were not sure if it was benchmaxxed. Here is why we ultimately chose to trust it.\n\n## AnswerBench contamination\n\n> and also AnswerBench contamination concern: Cascade 2 was released in March 2026, a few months after IMO-AnswerBench was published, whereas gpt-oss-120b was released earlier. Are you worried that Cascade 2's higher score on your local validation set (drawn from AnswerBench) might partly reflect AnswerBench problems having been in Cascade 2's training data, rather than a fair reasoning comparison against gpt-oss-120b?\n\nThis is a legitimate concern and we've added a note to *§Evaluation set* acknowledging it explicitly. Short version:\n\n- **Both models are at some risk.** IMO-AnswerBench is a rewriting of previously-published IMO problems, Shortlists, and national olympiads. Both gpt-oss-120b and Cascade 2 were trained on data that almost certainly includes some of these problems in their original form. So contamination is *the baseline*, not a one-sided concern.\n- **Cascade 2 is more exposed than gpt-oss-120b.** Cascade 2 was released after AnswerBench was published, so the contamination window for Cascade 2 is wider. The reasoning advantage we measure should be discounted accordingly when comparing absolute accuracy.\n- **We did not run our own contamination checks.** We chose to trust the data-curation procedure described in the Nemotron-Cascade-2 paper, which uses n-gram deduplication against known evaluation benchmarks. If that procedure works as documented, the contamination on AnswerBench specifically should be limited — but we can't verify this independently.\n- **What this doesn't undermine:** the *relative* observations that hold up regardless of contamination. The per-rollout cost difference, the maj@k stability difference, the tool-call error breakdown, the time-budget scaling shape — none of these depend on absolute reasoning ability. If Cascade 2's true reasoning capability is lower than its AnswerBench number suggests, our conclusions about \"Cascade 2 wins maj@k under time budget *because* it's cheaper per rollout and more consistent across rollouts\" still hold; we'd just be over-reporting the absolute scoreboard.\n\nThe contamination caveat is now in the writeup body so the reader can weight the absolute numbers themselves.\n\n\n## Why Cascade 2 over a gpt-oss-120b verifier path\n\n> **Why Cascade 2 over a gpt-oss-120b verifier path:** Your offline numbers show gpt-oss-120b has the higher pass@8 (77.8% vs Cascade 2's 74.6%), and you note an oracle verifier would pick gpt-oss-120b. You then invested heavily in the soft-clone + quantization + speculator pipeline for Cascade 2. With hindsight, what made you commit to the Cascade 2 route rather than building (e.g. a learned) verifier for gpt-oss-120b that could close the maj@8 → pass@8 gap?\n\nA few interacting reasons, in roughly the order they mattered:\n\n1. **The verifier route doesn't fit on the H100.** gpt-oss-120b's MXFP4 weights are ~120 GB; that leaves ~60 GB of HBM for KV cache, activations, and the 8 parallel solution rollouts we already need to fit. There is no headroom on a single H100 SXM for a *separate* verifier model running alongside it. Using gpt-oss-120b as its own verifier doubles the per-problem token bill on a budget that, at our throughput, already only fits ~8 solution rollouts. Both forms of \"gpt-oss-120b + verifier\" run out of either memory or wall-clock before they can be meaningfully tuned.\n2. **Cascade 2 leaves room for verification later.** Cascade 2's smaller footprint and ~70% higher throughput meant we could *start* with plain voting and then layer self-verification in the GPU bubbles between fast and slow solution rollouts. The self-verification harness we shipped (untuned, last 24 h) underperformed voting, but the option existed at all because of Cascade 2's overhead. With gpt-oss-120b we wouldn't have had that fallback path even in principle.\n3. **Training a learned verifier is its own project.** \"Build a learned verifier for gpt-oss-120b that closes maj@8 → pass@8\" is a meaningfully larger undertaking than the soft-clone we shipped: it needs verifier-training data (problem + multiple solutions + correctness labels), a training run, an eval harness for the verifier itself, and integration into the inference path.\n4. **AIMO 2 priors.** The historical evidence from AIMO 2 was that winning solutions optimized maj@k under a real time budget, not pass@∞. Cascade 2 *measurably* wins maj@k under our time budget; gpt-oss-120b would have needed a working verifier to compete on the same axis. Investing in the model that already wins the relevant metric was the lower-variance bet.\n\nThe resource-constraint version of this argument is now in the writeup as a follow-up paragraph after the oracle-verifier note in §Test-time scaling. The training-time / risk-budget version we hadn't articulated; thank you for prompting it.\n"
        }
      ]
    },
    {
      "id": 3457297,
      "postDate": "2026-05-13T13:49:55.270Z",
      "content": "<p>Hi, thank you for your submission to the Writeup Prize. <a href=\"https://www.kaggle.com/sambealing\" target=\"_blank\">@sambealing</a> and I will post some questions below that help us in assessing your submission more closely, as described in the <a href=\"https://www.kaggle.com/competitions/ai-mathematical-olympiad-progress-prize-3/discussion/689703\" target=\"_blank\">updated discussion post</a>.</p>\n<p>Your work shows impressive contributions, including implementing a missing feature in vLLM. We especially appreciate the in-depth engineering insights you provided, which even helped resolve issues that other participants encountered during the competition. I only have a few more surface-level questions:</p>\n<ol>\n<li><p><strong>vLLM</strong>: Was your PR eventually accepted into vLLM? I have read several other writeups from participants who used EAGLE3 and encountered the same issue you describe above. Please keep us posted if there are any updates.</p></li>\n<li><p><strong>Pass@k Plot</strong>: Did you mean to write that <code>gpt-oss-120b</code> wins pass@k at every time budget? Based on the plot, it seems that Cascade outperforms <code>gpt-oss-120b</code> across all tested budgets.</p></li>\n<li><p><strong>Time Management</strong>: Since EAGLE3 primarily improves inference speed and therefore saves time, why did you not spend more effort on developing a more advanced time-management system as well to further increase the gains? Many other contestants found that a good scheduling strategy could help, especially because hard and easy problems appeared in random order.</p></li>\n<li><p><strong>Tool Calls</strong>: Did you investigate the common causes of the significantly higher tool-call error rate in <code>gpt-oss-120b</code>, such as missing packages, outdated APIs, syntax issues, or similar failure modes? It would be interesting to understand why the Nemotron model appears to produce much cleaner tool calls.</p></li>\n</ol>\n<p>Looking forward to your response :)</p>",
      "rawMarkdown": "Hi, thank you for your submission to the Writeup Prize. @sambealing and I will post some questions below that help us in assessing your submission more closely, as described in the [updated discussion post](https://www.kaggle.com/competitions/ai-mathematical-olympiad-progress-prize-3/discussion/689703).\n\nYour work shows impressive contributions, including implementing a missing feature in vLLM. We especially appreciate the in-depth engineering insights you provided, which even helped resolve issues that other participants encountered during the competition. I only have a few more surface-level questions:\n\n1. **vLLM**: Was your PR eventually accepted into vLLM? I have read several other writeups from participants who used EAGLE3 and encountered the same issue you describe above. Please keep us posted if there are any updates.\n\n2. **Pass@k Plot**: Did you mean to write that `gpt-oss-120b` wins pass@k at every time budget? Based on the plot, it seems that Cascade outperforms `gpt-oss-120b` across all tested budgets.\n\n3. **Time Management**: Since EAGLE3 primarily improves inference speed and therefore saves time, why did you not spend more effort on developing a more advanced time-management system as well to further increase the gains? Many other contestants found that a good scheduling strategy could help, especially because hard and easy problems appeared in random order.\n\n4. **Tool Calls**: Did you investigate the common causes of the significantly higher tool-call error rate in `gpt-oss-120b`, such as missing packages, outdated APIs, syntax issues, or similar failure modes? It would be interesting to understand why the Nemotron model appears to produce much cleaner tool calls.\n\nLooking forward to your response :)\n",
      "votes": 1,
      "replies": [
        {
          "id": 3459539,
          "postDate": "2026-05-17T23:59:55.407Z",
          "content": "<p>Hi Philip Vonderlind (@philipvonderlind), thank you for thoughtful questions. Your 2nd question raised a real concern that we were not clear about our wording enough. The \"gpt-oss-120b always wins pass@k\" is false, and it should be attributed to \"wins given attempt budget\", not \"time budget\".</p>\n<p>Your 4th question called out a real mistake in our numbers and analysis. We carefully re-visited that analysis and re-generated the chart (basically it was a parsing issue on our end due to prompt differences between gpt-oss and cascade 2).</p>\n<h2>1. vLLM PR status</h2>\n<blockquote>\n  <p><strong>vLLM:</strong> Was your PR eventually accepted into vLLM? I have read several other writeups from participants who used EAGLE3 and encountered the same issue you describe above. Please keep us posted if there are any updates.</p>\n</blockquote>\n<p>Not merged yet — we're still working through two issues before it's ready to upstream:</p>\n<ol>\n<li><strong>Hopper-only optimizations.</strong> The performance work in our fork (cudagraph capture of the K-iteration drafter loop, the PIECEWISE → FULL upgrade we planned for p=12+, the FlashInfer attention switch) assumed Hopper SM 9.0 behaviour (because AIMO3 competition uses H100). On Blackwell SM 10.0 the same code hits graph-compilation issues we're still resolving. vLLM upstream needs GPU-portable code paths, so the Blackwell-clean rewrite of the kernel layer needs to land before we can merge.</li>\n<li><strong>A community PR now covers the correctness fix.</strong> One week after the competition deadline, <a href=\"https://github.com/vllm-project/vllm/pull/41233\" target=\"_blank\"><code>vllm-project/vllm#41233</code></a> was opened against the same mamba-cache-poisoning bug. It's still open at the time of writing, but it's likely the path upstream takes. Our perf optimizations are <strong>orthogonal</strong> to that PR (it has no speed/kernels work) and would still apply on top of it; we plan to submit them as a follow-up once the correctness side lands and our Blackwell story is fixed.</li>\n</ol>\n<p>The <a href=\"https://github.com/hav4ik/vllm/tree/pc-spec-v2-nonpc-slots\" target=\"_blank\">fork</a> remains useful for anyone running Hybrid-Mamba SD on Hopper today. We added a new <em>§Upstream PR status</em> subsection inside the writeup's vLLM section to track this honestly, and we'll keep you posted as the situation upstream evolves.</p>\n<h2>2. Pass@k plot</h2>\n<blockquote>\n  <p><strong>Pass@k Plot:</strong> Did you mean to write that gpt-oss-120b wins pass@k at every time budget? Based on the plot, it seems that Cascade outperforms gpt-oss-120b across all tested budgets.</p>\n</blockquote>\n<p>You're right — the original sentence was wrong. Within the plotted time budget, <strong>Cascade 2 wins both pass@k and maj@k</strong>: maj@k by a comfortable margin across the whole range, and pass@k by a smaller margin because Cascade 2 is cheaper per rollout and gets to fit more rollouts in any fixed wall-clock budget.</p>\n<p>What we <em>should</em> have written, and now do: gpt-oss-120b has a higher pass@k <em>ceiling</em> that only becomes visible <strong>beyond</strong> the plotted budget — its rollouts are more diverse (helps pass@k, hurts plurality voting), so given enough attempts a hypothetical oracle verifier would eventually prefer gpt-oss-120b. Inside the actual competition's time budget, Cascade 2 dominates on both axes.</p>\n<p>The paragraph next to the chart has been rewritten to reflect this. We've also added a note explaining why we couldn't realize that pass@k ceiling within Kaggle's resource budget anyway: a separate verifier won't fit alongside gpt-oss-120b's ~120 GB MXFP4 weights on a single H100, and using gpt-oss-120b as its own verifier doubles the per-problem token bill on a budget that already only fits ~8 solution rollouts.</p>\n<h2>3. Time management</h2>\n<blockquote>\n  <p><strong>Time Management:</strong> Since EAGLE3 primarily improves inference speed and therefore saves time, why did you not spend more effort on developing a more advanced time-management system as well to further increase the gains? Many other contestants found that a good scheduling strategy could help, especially because hard and easy problems appeared in random order.</p>\n</blockquote>\n<p>Honest answer: we joined the competition late, and the Kaggle 1-submission-per-day cap meant essentially every scheduling choice we shipped was a <strong>first-try call</strong>, with no headroom to A/B-test on the hidden LB. A more elaborate per-problem scheduler was on the wishlist, but the engineering dependency we <em>did</em> spend weeks on — PC + SD support for hybrid-Mamba models in vLLM — was already over our time budget, and we judged that getting the simple harness right was a safer use of the remaining days than tuning a scheduler we couldn't validate without burning submission slots.</p>\n<p>We also weighted this decision by what optimizing for in the first place. Our priors from AIMO 2 were that winning solutions in this class of competition optimized for <strong>maj@k under a time budget</strong>, not pass@∞ — pass@∞ tends to be similar across competitive models, but maj@k under a real budget is where the consistency premium pays off. That's why we picked Cascade 2, which (per the test-time-scaling chart) gives the best maj@k under a wall-clock budget. Cascade 2's lower per-rollout cost also means an adaptive scheduler has less to squeeze: the simple <code>min(900, max(300, time_left − 300 × problems_left_others))</code> formula already kept us at the 900 s ceiling for most of each shuffle.</p>\n<p>In hindsight, a smarter scheduler would have been a high-leverage thing to try, and we've added it to the <em>What we'd do differently</em> list in the Conclusion. The Conclusion now also has a <em>\"Why this is mostly a first-try harness\"</em> paragraph that articulates this trade-off explicitly.</p>\n<h2>4. Tool calls</h2>\n<blockquote>\n  <p><strong>Tool Calls:</strong> Did you investigate the common causes of the significantly higher tool-call error rate in gpt-oss-120b, such as missing packages, outdated APIs, syntax issues, or similar failure modes? It would be interesting to understand why the Nemotron model appears to produce much cleaner tool calls.</p>\n</blockquote>\n<p>Yes, in depth. We added <strong>Appendix G</strong> (tool-call error breakdown) which re-pairs every tool response in the LB-sim with the code that triggered it (504 rollouts per model). Headline findings:</p>\n<ol>\n<li><strong>Time, not count, is what matters.</strong> A typical gpt-oss-120b rollout burns ~37 s of wall-clock on failed tool calls (14% of its rollout), Cascade 2 ~17 s (6%). Per-call error <em>rates</em> are within a few points of each other; the per-rollout <em>time</em> difference is ~2.2×.</li>\n<li><strong>Timeouts dominate the wasted time.</strong> Pure 30 s sandbox timeouts (no output produced) account for <strong>88.6%</strong> of gpt-oss-120b's wasted tool wall-clock and <strong>94.0%</strong> of Cascade 2's. Every Python error class is a rounding error by time compared to timeouts. This was the most surprising finding for us — the wasted wall-clock is overwhelmingly about <em>how often the model writes code that doesn't finish in 30 s</em>, not about syntactic or library failures.</li>\n<li><strong>NameError is the strongest cross-model signal on Python errors.</strong> 0.49/rollout for gpt-oss-120b vs 0.15 for Cascade 2 (3.3×). Two root causes split the gpt-oss-120b cases roughly into a third and two-thirds:<ul>\n<li><strong>Forgotten imports (~1/3)</strong> — <code>sp</code> (sympy alias), <code>random</code>, <code>deque</code>, <code>math</code>, etc. The pre-import block (<code>math, numpy, sympy, itertools, collections, mpmath</code>) doesn't expose <code>sympy as sp</code> or <code>random</code>, and gpt-oss-120b writes Jupyter-cell-style snippets that assume these names are already live in the session rather than <code>import</code>-ing them.</li>\n<li><strong>Sandbox rollback (~2/3)</strong> — standard Jupyter / IPython preserves partial assignments when a cell errors; our sandbox snapshots state <em>before</em> each cell and restores that snapshot whenever the cell raises. Models trained on REPL semantics write follow-up code referencing variables from cells that errored, and hit <code>NameError</code> on what they assumed were live names. Cascade 2 hits the same rollback mechanism, just less often (it writes more self-contained cells with explicit imports at the top of each block).</li></ul></li>\n<li><strong>gpt-oss-120b also pays a SyntaxError toll</strong> (0.72 / rollout, 26% of its non-timeout errors), almost all incomplete-bracket / truncation patterns. We suspect this is a public-notebook artefact (smaller code chunks) rather than a model-trait, but didn't run the experiment to confirm.</li>\n</ol>\n<p>We also re-rendered the per-problem-breakdown chart on the same LB-sim data after discovering that the original chart silently bucketed Cascade 2's bare-text timeout responses as \"successful tool call\" (the two solver recorders formatted timeout strings differently).</p>",
          "rawMarkdown": "Hi Philip Vonderlind (@philipvonderlind), thank you for thoughtful questions. Your 2nd question raised a real concern that we were not clear about our wording enough. The \"gpt-oss-120b always wins pass@k\" is false, and it should be attributed to \"wins given attempt budget\", not \"time budget\".\n\nYour 4th question called out a real mistake in our numbers and analysis. We carefully re-visited that analysis and re-generated the chart (basically it was a parsing issue on our end due to prompt differences between gpt-oss and cascade 2).\n\n\n## 1. vLLM PR status\n\n> **vLLM:** Was your PR eventually accepted into vLLM? I have read several other writeups from participants who used EAGLE3 and encountered the same issue you describe above. Please keep us posted if there are any updates.\n\nNot merged yet — we're still working through two issues before it's ready to upstream:\n\n1. **Hopper-only optimizations.** The performance work in our fork (cudagraph capture of the K-iteration drafter loop, the PIECEWISE → FULL upgrade we planned for p=12+, the FlashInfer attention switch) assumed Hopper SM 9.0 behaviour (because AIMO3 competition uses H100). On Blackwell SM 10.0 the same code hits graph-compilation issues we're still resolving. vLLM upstream needs GPU-portable code paths, so the Blackwell-clean rewrite of the kernel layer needs to land before we can merge.\n2. **A community PR now covers the correctness fix.** One week after the competition deadline, [`vllm-project/vllm#41233`](https://github.com/vllm-project/vllm/pull/41233) was opened against the same mamba-cache-poisoning bug. It's still open at the time of writing, but it's likely the path upstream takes. Our perf optimizations are **orthogonal** to that PR (it has no speed/kernels work) and would still apply on top of it; we plan to submit them as a follow-up once the correctness side lands and our Blackwell story is fixed.\n\nThe [fork](https://github.com/hav4ik/vllm/tree/pc-spec-v2-nonpc-slots) remains useful for anyone running Hybrid-Mamba SD on Hopper today. We added a new *§Upstream PR status* subsection inside the writeup's vLLM section to track this honestly, and we'll keep you posted as the situation upstream evolves.\n\n## 2. Pass@k plot\n\n> **Pass@k Plot:** Did you mean to write that gpt-oss-120b wins pass@k at every time budget? Based on the plot, it seems that Cascade outperforms gpt-oss-120b across all tested budgets.\n\nYou're right — the original sentence was wrong. Within the plotted time budget, **Cascade 2 wins both pass@k and maj@k**: maj@k by a comfortable margin across the whole range, and pass@k by a smaller margin because Cascade 2 is cheaper per rollout and gets to fit more rollouts in any fixed wall-clock budget.\n\nWhat we *should* have written, and now do: gpt-oss-120b has a higher pass@k *ceiling* that only becomes visible **beyond** the plotted budget — its rollouts are more diverse (helps pass@k, hurts plurality voting), so given enough attempts a hypothetical oracle verifier would eventually prefer gpt-oss-120b. Inside the actual competition's time budget, Cascade 2 dominates on both axes.\n\nThe paragraph next to the chart has been rewritten to reflect this. We've also added a note explaining why we couldn't realize that pass@k ceiling within Kaggle's resource budget anyway: a separate verifier won't fit alongside gpt-oss-120b's ~120 GB MXFP4 weights on a single H100, and using gpt-oss-120b as its own verifier doubles the per-problem token bill on a budget that already only fits ~8 solution rollouts.\n\n## 3. Time management\n\n> **Time Management:** Since EAGLE3 primarily improves inference speed and therefore saves time, why did you not spend more effort on developing a more advanced time-management system as well to further increase the gains? Many other contestants found that a good scheduling strategy could help, especially because hard and easy problems appeared in random order.\n\nHonest answer: we joined the competition late, and the Kaggle 1-submission-per-day cap meant essentially every scheduling choice we shipped was a **first-try call**, with no headroom to A/B-test on the hidden LB. A more elaborate per-problem scheduler was on the wishlist, but the engineering dependency we *did* spend weeks on — PC + SD support for hybrid-Mamba models in vLLM — was already over our time budget, and we judged that getting the simple harness right was a safer use of the remaining days than tuning a scheduler we couldn't validate without burning submission slots.\n\nWe also weighted this decision by what optimizing for in the first place. Our priors from AIMO 2 were that winning solutions in this class of competition optimized for **maj@k under a time budget**, not pass@∞ — pass@∞ tends to be similar across competitive models, but maj@k under a real budget is where the consistency premium pays off. That's why we picked Cascade 2, which (per the test-time-scaling chart) gives the best maj@k under a wall-clock budget. Cascade 2's lower per-rollout cost also means an adaptive scheduler has less to squeeze: the simple `min(900, max(300, time_left − 300 × problems_left_others))` formula already kept us at the 900 s ceiling for most of each shuffle.\n\nIn hindsight, a smarter scheduler would have been a high-leverage thing to try, and we've added it to the *What we'd do differently* list in the Conclusion. The Conclusion now also has a *\"Why this is mostly a first-try harness\"* paragraph that articulates this trade-off explicitly.\n\n## 4. Tool calls\n\n> **Tool Calls:** Did you investigate the common causes of the significantly higher tool-call error rate in gpt-oss-120b, such as missing packages, outdated APIs, syntax issues, or similar failure modes? It would be interesting to understand why the Nemotron model appears to produce much cleaner tool calls.\n\nYes, in depth. We added **Appendix G** (tool-call error breakdown) which re-pairs every tool response in the LB-sim with the code that triggered it (504 rollouts per model). Headline findings:\n\n1. **Time, not count, is what matters.** A typical gpt-oss-120b rollout burns ~37 s of wall-clock on failed tool calls (14% of its rollout), Cascade 2 ~17 s (6%). Per-call error *rates* are within a few points of each other; the per-rollout *time* difference is ~2.2×.\n2. **Timeouts dominate the wasted time.** Pure 30 s sandbox timeouts (no output produced) account for **88.6%** of gpt-oss-120b's wasted tool wall-clock and **94.0%** of Cascade 2's. Every Python error class is a rounding error by time compared to timeouts. This was the most surprising finding for us — the wasted wall-clock is overwhelmingly about *how often the model writes code that doesn't finish in 30 s*, not about syntactic or library failures.\n3. **NameError is the strongest cross-model signal on Python errors.** 0.49/rollout for gpt-oss-120b vs 0.15 for Cascade 2 (3.3×). Two root causes split the gpt-oss-120b cases roughly into a third and two-thirds:\n    - **Forgotten imports (~1/3)** — `sp` (sympy alias), `random`, `deque`, `math`, etc. The pre-import block (`math, numpy, sympy, itertools, collections, mpmath`) doesn't expose `sympy as sp` or `random`, and gpt-oss-120b writes Jupyter-cell-style snippets that assume these names are already live in the session rather than `import`-ing them.\n    - **Sandbox rollback (~2/3)** — standard Jupyter / IPython preserves partial assignments when a cell errors; our sandbox snapshots state *before* each cell and restores that snapshot whenever the cell raises. Models trained on REPL semantics write follow-up code referencing variables from cells that errored, and hit `NameError` on what they assumed were live names. Cascade 2 hits the same rollback mechanism, just less often (it writes more self-contained cells with explicit imports at the top of each block).\n4. **gpt-oss-120b also pays a SyntaxError toll** (0.72 / rollout, 26% of its non-timeout errors), almost all incomplete-bracket / truncation patterns. We suspect this is a public-notebook artefact (smaller code chunks) rather than a model-trait, but didn't run the experiment to confirm.\n\nWe also re-rendered the per-problem-breakdown chart on the same LB-sim data after discovering that the original chart silently bucketed Cascade 2's bare-text timeout responses as \"successful tool call\" (the two solver recorders formatted timeout strings differently)."
        }
      ]
    },
    {
      "id": 3447586,
      "postDate": "2026-04-23T13:12:42.480Z",
      "content": "<p>Fantastic engineering work! I have a question about speculative decoding. You show very good \"Accept rate\", but how it changes during long trajectories? You fine-tuned it with 65k context, but does it work well with long context at test time?</p>",
      "rawMarkdown": "Fantastic engineering work! I have a question about speculative decoding. You show very good \"Accept rate\", but how it changes during long trajectories? You fine-tuned it with 65k context, but does it work well with long context at test time?",
      "votes": 1,
      "replies": [
        {
          "id": 3447798,
          "postDate": "2026-04-24T01:00:39.007Z",
          "content": "<p>Thanks for your kind words 😁 You've got a good point!</p>\n<p>Although we did fine-tune with maximum of 65K context, in reality there's not many TIR tasks that have more than 40K generated tokens (we did our best to filter out the hardest ones). TIR reasoning is much shorter than COT (which we didn't sampled enough for training). Validation during training was done on a hard subset of IMO-AnswerBench (we filtered all validation benchmarks like AIME and IMO-benches out from training), but they don't have that much rollouts with &gt;65K tokens either.</p>\n<p>However, the <strong>3.55</strong> acceptance rate cited above was on AIME'26, which doesn't need more than 65K tokens. It was my primary \"unit test\" for Eagle3 work, because AIME is simple enough to run in ~10 minutes, but just hard enough to reveal any bugs in Mamba states handling (my criteria was 98% accuracy - if I see a drop of 10%, I know exactly that somewhere a Mamba state got corrupted), and acceptance rate numbers were collected together with accuracy unit testing. We didn't have time to properly test it on enough 65K+ samples. Will come back with some measurements and update the writeup (I'll continue working on the PC+SD anyways).</p>\n<p>One thing I want to emphasize: Speculative Decoding is not worth it for small models. I know that people smarter than me already knows this, but the economy of 3B active models is so brutal. 1 token step is just <strong>3.4</strong> ms: 2.8ms Cuda Graph + 0.6ms overhead from CPU/sampling. Naive Eagle3 implementation is 13ms - you need acceptance rate of 4 to break even. We brought that down to 8.9ms - but that still requires 2.6 acceptance rate to break even. Not to mention other overheads that makes it closer to 3.0 acceptance rate to break even. Models like Cascade2 are heartbreakingly ridiculously fast!</p>",
          "rawMarkdown": "Thanks for your kind words 😁 You've got a good point!\n\nAlthough we did fine-tune with maximum of 65K context, in reality there's not many TIR tasks that have more than 40K generated tokens (we did our best to filter out the hardest ones). TIR reasoning is much shorter than COT (which we didn't sampled enough for training). Validation during training was done on a hard subset of IMO-AnswerBench (we filtered all validation benchmarks like AIME and IMO-benches out from training), but they don't have that much rollouts with >65K tokens either.\n\nHowever, the **3.55** acceptance rate cited above was on AIME'26, which doesn't need more than 65K tokens. It was my primary \"unit test\" for Eagle3 work, because AIME is simple enough to run in ~10 minutes, but just hard enough to reveal any bugs in Mamba states handling (my criteria was 98% accuracy - if I see a drop of 10%, I know exactly that somewhere a Mamba state got corrupted), and acceptance rate numbers were collected together with accuracy unit testing. We didn't have time to properly test it on enough 65K+ samples. Will come back with some measurements and update the writeup (I'll continue working on the PC+SD anyways).\n\nOne thing I want to emphasize: Speculative Decoding is not worth it for small models. I know that people smarter than me already knows this, but the economy of 3B active models is so brutal. 1 token step is just **3.4** ms: 2.8ms Cuda Graph + 0.6ms overhead from CPU/sampling. Naive Eagle3 implementation is 13ms - you need acceptance rate of 4 to break even. We brought that down to 8.9ms - but that still requires 2.6 acceptance rate to break even. Not to mention other overheads that makes it closer to 3.0 acceptance rate to break even. Models like Cascade2 are heartbreakingly ridiculously fast!",
          "votes": 2
        }
      ]
    },
    {
      "id": 3447327,
      "postDate": "2026-04-23T00:08:32.747Z",
      "content": "<p>This is <a href=\"https://www.kaggle.com/yeoyunsianggeremie\" target=\"_blank\">@yeoyunsianggeremie</a> watching me getting stuck at some random side-quests \n<img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F2270821%2F7f5e4044400f6c2e3207611fc92cbad5%2Ffrieren-beyond-journey-s-end-15-frieren-fern-mimic.avif?generation=1776902886558630&amp;alt=media\" alt=\"\"></p>",
      "rawMarkdown": "This is @yeoyunsianggeremie watching me getting stuck at some random side-quests \n![](https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F2270821%2F7f5e4044400f6c2e3207611fc92cbad5%2Ffrieren-beyond-journey-s-end-15-frieren-fern-mimic.avif?generation=1776902886558630&alt=media)",
      "votes": 2
    },
    {
      "id": 3459536,
      "postDate": "2026-05-17T23:58:07.073Z",
      "content": "<p>Changes to address rebuttal questions:</p>\n<h1>Writeup changelog</h1>\n<p>Short list of edits made to <code>writeup.md</code> (and supporting artifacts) in this rebuttal pass.</p>\n<h2>Bug fixes</h2>\n<ul>\n<li><strong>Per-problem-breakdown chart (Online LB simulation section).</strong> Re-rendered <code>kaggle_per_problem_stats.png</code> from scratch. The original right panel used an asymmetric <code>is_error</code> rule: the gpt-oss-120b recorder prefixed its 30 s timeouts with <code>[ERROR]</code>, our Cascade 2 recorder left them bare, and the same rule caught one and missed the other. Cascade 2's <code>tool_err</code> went from <strong>0.2% → 6.3%</strong> of wall-clock; gpt-oss-120b's stayed at 14.4%. The corrected chart also splits timeouts from Python errors into two visually distinct buckets (purple/red), and the panel-3 subtitle now states the 30 s sandbox timeout cap that applies to both solvers. Full reconciliation in <code>rebuttal_n4.md</code>.</li>\n<li><strong>Head-to-head table — Tool err (per-call).</strong> Cascade 2 line updated from <strong>6.0% → 10.6%</strong> for the same reason (broader symmetric definition that includes timeouts).</li>\n<li><strong>Test-time scaling claim.</strong> Removed the incorrect statement that \"gpt-oss-120b wins pass@k at every time budget\" — within the plotted range, Cascade 2 wins both pass@k and maj@k. Rewrote the surrounding paragraph: gpt-oss-120b has the higher pass@k <em>ceiling</em> beyond the plotted budget, but Cascade 2 dominates inside it.</li>\n</ul>\n<h2>New content</h2>\n<ul>\n<li><strong>Appendix G — Tool-call error breakdown.</strong> New appendix answering \"why does gpt-oss-120b's tool-call overhead come out roughly 2× Cascade 2's?\". Per-rollout summary table + per-class breakdown (timeouts, SyntaxError, NameError, …) + prose on the two NameError root causes (forgotten stdlib imports; sandbox state-rollback semantics differing from standard Jupyter/IPython).</li>\n<li><strong>Resource-constraint note on the pass@k ceiling.</strong> Added a paragraph in <em>Test-time scaling</em> explaining why we can't realize gpt-oss-120b's higher pass@k ceiling on Kaggle: a separate verifier won't fit alongside ~120 GB MXFP4 weights, and gpt-oss-120b as its own verifier doubles the per-problem token bill.</li>\n<li><strong>First-try-harness explanation in Conclusion.</strong> New paragraph explaining the late entry, the 1-submission-per-day cap, why we picked Cascade 2 (AIMO-2 priors on maj@k vs pass@∞), and why we didn't develop a more advanced scheduler. Also added \"smarter per-problem scheduling policy\" to the existing <em>What we'd do differently</em> list.</li>\n<li><strong>Contamination note in §Evaluation set.</strong> Acknowledged that IMO-AnswerBench is built from prior IMO/Shortlist problems and that both candidate models likely have some exposure, with Cascade 2 more so (it was released <em>after</em> AnswerBench). We rely on the Nemotron-Cascade-2 paper's n-gram deduplication procedure; relative comparisons are less affected than absolute numbers.</li>\n<li><strong>Appendix D — attempt-count chart.</strong> Added <code>test_time_scaling_attempts.png</code> as a visual companion that drops the per-rollout cost scaling. One sentence of framing, no separate findings.</li>\n<li><strong>Private LB column.</strong> Added to the top summary table and the head-to-head table; our final submission scored <strong>41 / 50</strong> on private LB.</li>\n<li><strong>Upstream PR status subsection.</strong> Inside the vLLM section, explaining why the PC + SD PR is on hold: Hopper-only kernel optimizations that won't compile on Blackwell, and community PR <a href=\"https://github.com/vllm-project/vllm/pull/41233\" target=\"_blank\">vllm-project/vllm#41233</a> (opened 1 week after the competition deadline) now covers the correctness fix from a different angle. The open-source-contributions bullet at the top no longer claims the PR as a deliverable.</li>\n<li><strong><code>Summary of our main candidates</code></strong> lead-in tightened from \"local validation and public LB\" → \"local validation and the Kaggle leaderboard\" (now covers Public + Private columns).</li>\n</ul>",
      "rawMarkdown": "Changes to address rebuttal questions:\n\n# Writeup changelog\n\nShort list of edits made to `writeup.md` (and supporting artifacts) in this rebuttal pass.\n\n## Bug fixes\n\n- **Per-problem-breakdown chart (Online LB simulation section).** Re-rendered `kaggle_per_problem_stats.png` from scratch. The original right panel used an asymmetric `is_error` rule: the gpt-oss-120b recorder prefixed its 30 s timeouts with `[ERROR]`, our Cascade 2 recorder left them bare, and the same rule caught one and missed the other. Cascade 2's `tool_err` went from **0.2% → 6.3%** of wall-clock; gpt-oss-120b's stayed at 14.4%. The corrected chart also splits timeouts from Python errors into two visually distinct buckets (purple/red), and the panel-3 subtitle now states the 30 s sandbox timeout cap that applies to both solvers. Full reconciliation in `rebuttal_n4.md`.\n- **Head-to-head table — Tool err (per-call).** Cascade 2 line updated from **6.0% → 10.6%** for the same reason (broader symmetric definition that includes timeouts).\n- **Test-time scaling claim.** Removed the incorrect statement that \"gpt-oss-120b wins pass@k at every time budget\" — within the plotted range, Cascade 2 wins both pass@k and maj@k. Rewrote the surrounding paragraph: gpt-oss-120b has the higher pass@k *ceiling* beyond the plotted budget, but Cascade 2 dominates inside it.\n\n## New content\n\n- **Appendix G — Tool-call error breakdown.** New appendix answering \"why does gpt-oss-120b's tool-call overhead come out roughly 2× Cascade 2's?\". Per-rollout summary table + per-class breakdown (timeouts, SyntaxError, NameError, …) + prose on the two NameError root causes (forgotten stdlib imports; sandbox state-rollback semantics differing from standard Jupyter/IPython).\n- **Resource-constraint note on the pass@k ceiling.** Added a paragraph in *Test-time scaling* explaining why we can't realize gpt-oss-120b's higher pass@k ceiling on Kaggle: a separate verifier won't fit alongside ~120 GB MXFP4 weights, and gpt-oss-120b as its own verifier doubles the per-problem token bill.\n- **First-try-harness explanation in Conclusion.** New paragraph explaining the late entry, the 1-submission-per-day cap, why we picked Cascade 2 (AIMO-2 priors on maj@k vs pass@∞), and why we didn't develop a more advanced scheduler. Also added \"smarter per-problem scheduling policy\" to the existing *What we'd do differently* list.\n- **Contamination note in §Evaluation set.** Acknowledged that IMO-AnswerBench is built from prior IMO/Shortlist problems and that both candidate models likely have some exposure, with Cascade 2 more so (it was released *after* AnswerBench). We rely on the Nemotron-Cascade-2 paper's n-gram deduplication procedure; relative comparisons are less affected than absolute numbers.\n- **Appendix D — attempt-count chart.** Added `test_time_scaling_attempts.png` as a visual companion that drops the per-rollout cost scaling. One sentence of framing, no separate findings.\n- **Private LB column.** Added to the top summary table and the head-to-head table; our final submission scored **41 / 50** on private LB.\n- **Upstream PR status subsection.** Inside the vLLM section, explaining why the PC + SD PR is on hold: Hopper-only kernel optimizations that won't compile on Blackwell, and community PR [vllm-project/vllm#41233](https://github.com/vllm-project/vllm/pull/41233) (opened 1 week after the competition deadline) now covers the correctness fix from a different angle. The open-source-contributions bullet at the top no longer claims the PR as a deliverable.\n- **`Summary of our main candidates`** lead-in tightened from \"local validation and public LB\" → \"local validation and the Kaggle leaderboard\" (now covers Public + Private columns).\n"
    }
  ],
  "complete": true,
  "source": "live",
  "fetched_at": "2026-10-20T04:24:32Z",
  "competition_id": 118448,
  "competition_slug": "ai-mathematical-olympiad-progress-prize-3",
  "team_id": 14799269,
  "solution_url": "/competitions/ai-mathematical-olympiad-progress-prize-3/writeups/aimo3-a-collection-of-delightful-side-quests"
}