Skip to content

perf(eagle3): reuse the argmax the forward already computes - #1295

Merged
orestis-z merged 1 commit into
mainfrom
perf/argmax-reuse
Oct 27, 2026
Merged

orestis-z merged 1 commit into
mainfrom
perf/argmax-reuse

Conversation

@orestis-z

@orestis-z orestis-z commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Purpose

Each TTT step took three argmax passes over a [total_seq_len, draft_vocab_size] tensor for two distinct results: one over the draft logits for the accuracy metrics, one over the targets for the same, and one over the draft logits again to pick the next step's input ids. At a ~32k draft vocab that tensor does not fit in cache, so each pass is a full stream of it. This takes the draft argmax once and hands it to compute_metrics, and moves the target argmax out of the TTT loop entirely since the targets do not change across steps.

compute_metrics takes the ids unaligned and applies the same slicing align_for_step applies to the tensors they came from, so the reported per-step accuracies are unchanged.

Measured

Single H100, eagle3, synthetic batches, full 151936-token vocab, total_seq_len 2048, 10 warmup + 50 measured steps, 3 reps, median of the per-run means, log_freq=1, scripts/benchmark.py. This PR against #1294:

Qwen3-0.6B Qwen3-1.7B Qwen3-8B Qwen3-32B
1.076x 1.026x 1.013x 1.007x

The argmax it removes is over the draft vocabulary, which does not grow with the verifier, so the relative win shrinks as the rest of the step gets heavier — but it is positive at every size.

Tests

pytest tests/unit/models/test_eagle3_metrics.py

New cases cover that the accuracy counts match the argmax of the tensors align_for_step produces, for several ttt_steps with and without a loss mask, plus a deterministic case that only passes if pred_ids and target_ids get the opposite slices. Passes.

Checklist

I have filled in:

  • The purpose of the PR, such as "Fix some issue (link existing issues this PR will resolve)".
  • The test plan/results, such as providing test command and pasting the results.
  • (Optional) The necessary documentation update.
  • I (a human) have written or reviewed the code in this pr to the best of my ability.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review.

ℹ️ Recent review info
⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between e845fed and 57cb865.

📒 Files selected for processing: 3
  • src/speculators/models/eagle3/core.py
  • src/speculators/models/eagle3/metrics.py
  • tests/unit/models/test_eagle3_metrics.py

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review.

Walkthrough

compute_metrics now accepts optional precomputed argmax IDs. Eagle3DraftModel.forward passes those IDs to the metric function. Unit tests compare loss and metrics with and without precomputed IDs across TTT steps and masking cases.

Changes: Eagle3 metric argmax reuse

Layer / File(s) Summary
Add optional precomputed IDs to metrics
src/speculators/models/eagle3/metrics.py, tests/unit/models/test_eagle3_metrics.py
compute_metrics accepts optional prediction and target IDs, aligns supplied IDs for accuracy calculation, and computes missing IDs internally. Tests compare loss and metrics across TTT steps and mask settings, including the case where only target IDs are supplied.
Pass precomputed IDs from the forward path
src/speculators/models/eagle3/core.py
Eagle3DraftModel.forward computes verifier target IDs before the TTT loop, computes draft prediction IDs before metric calculation, and passes both to compute_metrics.

Priority: Low

Merge Risk: Minimal · previous review 57cb8

The argmax reuse has no identified correctness issue and is ready to merge after normal checks.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main performance change: reusing the draft argmax computed during Eagle3 forward processing.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The description directly matches the changes. It explains the argmax reuse, target ID computation outside the TTT loop, metric alignment, performance measurements, and unit tests.
✨ Finishing Touches: 💡 2
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@orestis-z
orestis-z added this pull request to stack #1302 October 21, 2026 17:19
@mergify

mergify Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Merge Protections

🟢 Merge protection satisfied — ready to merge.

Show 1 satisfied protection

🟢 Require approval from approved reviewers list

All pull requests must have at least one approving review from a member of the approved reviewers list before merging.

  • any of:
    • approved-reviews-by = fynnsu
    • approved-reviews-by = dsikka
    • approved-reviews-by = orestis-z
    • approved-reviews-by = rahul-tuli
    • approved-reviews-by = shanjiaz

@orestis-z
orestis-z marked this pull request as ready for review October 22, 2026 13:25
@orestis-z orestis-z self-assigned this Oct 22, 2026
@orestis-z
orestis-z force-pushed the perf/argmax-reuse branch 2 times, most recently from 57cb865 to 7614c0f Compare October 22, 2026 14:32
@orestis-z
orestis-z removed this pull request from stack #1302 October 22, 2026 14:32
@orestis-z
orestis-z added this pull request to stack #1308 October 22, 2026 14:33
@orestis-z
orestis-z force-pushed the perf/argmax-reuse branch 2 times, most recently from 53e343f to 2dc252f Compare October 22, 2026 14:34

@fynnsu fynnsu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally approve but could use some cleanup.

Comment thread src/speculators/models/eagle3/core.py Outdated
Comment thread src/speculators/models/eagle3/core.py
Comment thread src/speculators/models/eagle3/metrics.py Outdated
Comment thread src/speculators/models/eagle3/metrics.py Outdated
Comment thread src/speculators/models/eagle3/metrics.py Outdated
@mergify

mergify Bot commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @orestis-z.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Oct 26, 2026
Base automatically changed from perf/metrics-no-host-sync to main October 27, 2026 15:23
Each TTT step took three argmax passes over a
``[total_seq_len, draft_vocab_size]`` tensor: one over the draft logits
for the accuracy metrics, one over the targets for the same, and one
over the draft logits again to pick the next step's input ids. At a
~32k draft vocab that is three full streams of a tensor that does not
fit in cache, for two distinct results.

Take the draft argmax once, before the metrics, and hand it to
``compute_metrics``. The targets do not change across TTT steps, so
their argmax moves out of the loop entirely and is computed once per
forward.

``compute_metrics`` takes the ids unaligned and applies the same
slicing ``align_for_step`` applies to the tensors they came from, so
the reported per-step accuracies are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Orestis Zambounis <orestis.zambounis@gmail.com>
orestis-z added a commit that referenced this pull request Oct 27, 2026
<!-- markdownlint-disable -->

## Purpose

Split out of the measurements in #1259 / #1262 / #1263, one change per
PR.

Every training step built its metric scalars with `torch.tensor(<python
float>, device=cuda)`, which copies from pageable host memory and so
cannot be issued asynchronously — the host blocks until the work already
queued on the device drains. The same step ran `torch.masked_select` on
the accuracy mask, whose output size depends on the mask's values,
forcing another device-to-host sync before the count could be read.
Building those scalars with `torch.full`/`torch.zeros`/`torch.ones` and
counting masked positions with a reduction gives identical numbers
without either sync.

`full_total` and `cond_total` stay separate tensors so a caller reducing
the metrics dict in place cannot count one of them twice.

### Measured

Single H100, eagle3, synthetic batches, full 151936-token vocab,
`total_seq_len` 2048, 10 warmup + 50 measured steps, 3 reps, median of
the per-run means, `log_freq=1` (the default), `scripts/benchmark.py`.
This PR against main:

| Qwen3-0.6B | Qwen3-1.7B | Qwen3-8B | Qwen3-32B |
|---|---|---|---|
| **1.111x** | **1.110x** | **1.030x** | **1.013x** |

<details>
<summary>Where the gain comes from, change by change</summary>

Same harness and settings, each change measured against its parent
commit, so every cell is one PR's marginal contribution at that verifier
size. Run-to-run spread within a cell is under 1% everywhere except main
at 0.6B.

| PR | | Qwen3-0.6B | Qwen3-1.7B | Qwen3-8B | Qwen3-32B |
|---|---|---|---|---|---|
| #1294 | metrics off host | **1.111x** | **1.110x** | **1.030x** |
**1.013x** |
| #1295 | reuse the argmax | **1.076x** | **1.026x** | **1.013x** |
1.007x |
| #1296 | single-pass fused KL | **1.047x** | **1.017x** | **1.012x** |
1.003x |
| #1297 | logs off the thread | **1.029x** | **1.042x** | **1.015x** |
1.006x |
| #1298 | one metric readback | 0.980x | 1.004x | 0.997x | 0.999x |
| #1299 | H2D prefetch † | 1.024x | 1.013x | 1.029x | 1.021x |
| #1300 | skip dropped positions | 0.997x | 0.988x | 0.980x | 0.988x |
| #1301 | precast frozen heads | **1.055x** | **1.096x** | **1.059x** |
**1.036x** |
| | **cumulative** | **1.358x** | **1.329x** | **1.141x** | **1.077x** |

† #1299's row is not the prefetch. That commit also pins the synthetic
batch in `scripts/benchmark.py`, and the real `DataLoader` already sets
`pin_memory=True`, so it is a benchmark-fidelity fix rather than a
training speedup. Applying only the pinning hunk on top of #1298 gives
1.017x / 1.017x / 1.030x / 1.022x — the whole row. Discounting it, the
cumulative row is **1.335x / 1.307x / 1.108x / 1.054x**.

Reading it:

- **#1294 and #1301 carry most of the gain.** They are the two largest
at every size, and #1301 is the only one that also moves peak memory
(−1.5 GB at 0.6B to −7.4 GB at 32B).
- **#1295, #1296 and #1297 fade with verifier size** but stay positive —
1–2% at 8B, 0.3–0.7% at 32B.
- **#1298 is a tie at every size.** One sync per step instead of a few
dozen is the right shape, but it does not show up here.
- **#1299's prefetch is inside noise** in this regime once the harness
fix is separated out, though it does pay once the metric log emit is
deferred — see #1299.
- **#1300 is negative at `seq_len` 2048 at every size** and costs
0.3–1.5 GB of peak; it needs both long sequences and a small hidden size
to pay.

Only #1300 and #1301 change peak allocated memory at all; the other six
are flat to the megabyte.

Most of these changes remove fixed per-step host and metric overhead, so
the relative win shrinks as the verifier grows even though the absolute
saving does not. At 8B and 32B the step is dominated by the optimizer
(42% and 58%, all Muon Newton–Schulz), which nothing here touches — the
same point #1111's side note made.

</details>

## Tests

```bash
pytest tests/unit/models/test_metrics.py
```

New cases cover that the metric tensors are built without a
host-to-device copy and that the masked counts match `masked_select`.
Passes.

## Checklist

I have filled in:

- [x] The purpose of the PR, such as "Fix some issue (link existing
issues this PR will resolve)".
- [x] The test plan/results, such as providing test command and pasting
the results.
- [ ] (Optional) The necessary documentation update.
- [x] I (a human) have written or reviewed the code in this pr to the
best of my ability.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Orestis Zambounis <orestis.zambounis@gmail.com>
Signed-off-by: Orestis Zambounis <23146389+orestis-z@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@orestis-z orestis-z added the ready This PR is ready for review label Oct 27, 2026
@orestis-z
orestis-z enabled stacked auto-merge (squash) October 27, 2026 15:29
@orestis-z
orestis-z merged commit e33f2ba into main Oct 27, 2026
11 checks passed
@orestis-z
orestis-z deleted the perf/argmax-reuse branch October 27, 2026 15:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-rebase ready This PR is ready for review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants