Skip to content

perf(train): keep metric construction off the host - #1294

Merged
orestis-z merged 3 commits into
mainfrom
perf/metrics-no-host-sync
Oct 27, 2026
Merged

orestis-z merged 3 commits into
mainfrom
perf/metrics-no-host-sync

Conversation

@orestis-z

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

Copy link
Copy Markdown
Collaborator

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
Where the gain comes from, change by change

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:

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.

Tests

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:

  • 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 94db8b8 and e845fed.

📒 Files selected for processing: 6
  • src/speculators/losses/utils.py
  • src/speculators/models/eagle3/core.py
  • src/speculators/models/eagle3/metrics.py
  • src/speculators/models/metrics.py
  • src/speculators/train/trainer.py
  • tests/unit/models/test_metrics.py

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

Walkthrough

The changes update scalar tensor initialization in loss, metric, and training code. They also change masked accuracy counting and add tests for masked counts, empty masks, and total tensor independence.

Changes: Loss and metric tensor handling

Layer / File(s) Summary
Scalar tensor initialization
src/speculators/losses/utils.py, src/speculators/models/eagle3/core.py, src/speculators/models/eagle3/metrics.py, src/speculators/train/trainer.py
Scalar loss, metric, and error-record tensors now use torch.zeros, torch.ones, or torch.full instead of torch.tensor constructors. The changes retain the specified device and dtype.
Masked accuracy counts
src/speculators/models/metrics.py, tests/unit/models/test_metrics.py
compute_accuracy_single_step computes correctness and totals using the loss mask without selecting masked elements first. Tests cover masked counts with and without prev_correct, an all-false mask, and independent total tensors.

Possibly related issues

  • vllm-project/speculators#1251: This PR changes metric accounting for masked positions, but does not add agentic rollout ingestion.

Priority: Normal

Merge Risk: Minimal · previous review e845f

The changed metric construction and accuracy counts have no established regression in the supported training path. The PR is mergeable after normal checks.

Pre-merge checks: Passed 5
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: keeping training metric construction off the host.
Docstring Coverage Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 6 files.
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 device-side metric construction, the replacement of masked selection with reductions, the separation of metric tensors, benchmark results,…
Full details: Description check

Explanation

The description directly matches the changes. It explains the device-side metric construction, the replacement of masked selection with reductions, the separation of metric tensors, benchmark results, and tests.

✨ Finishing Touches: 💡 1
📝 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 changed the title perf/metrics no host sync perf(train): keep metric construction off the host Oct 21, 2026
@orestis-z
orestis-z marked this pull request as ready for review October 22, 2026 13:23
@orestis-z orestis-z self-assigned this Oct 22, 2026
@orestis-z
orestis-z force-pushed the perf/metrics-no-host-sync branch 2 times, most recently from e845fed to 31c78e1 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/metrics-no-host-sync branch from e845fed to 4a5effc Compare October 22, 2026 14:33
orestis-z added a commit that referenced this pull request Oct 22, 2026
<!-- markdownlint-disable -->

## Purpose

Four PR-description rules the guide is missing, all from writing the
#1294–#1301 stack: ask for the test command and whether it passed rather
than pasted output, keep the description proportional to the diff, do
not hard-wrap it, and do not narrate stack position. Plus a CODEOWNERS
entry for `AGENTS.md`, which had none.

## Tests

Docs only.

## 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>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@orestis-z
orestis-z force-pushed the perf/metrics-no-host-sync branch from 4a5effc to cc4cd12 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.

Looks good, but maybe cleanup comments.

Also as we discussed, I think there's an open question of whether we should also be applying the torch.full() switch to all uses of torch.tensor() with scalar values in the repo, or keep this pr scoped to Eagle3.

Comment thread src/speculators/models/metrics.py Outdated
Comment thread src/speculators/train/trainer.py Outdated
@orestis-z
orestis-z force-pushed the perf/metrics-no-host-sync branch from cc4cd12 to c564528 Compare October 26, 2026 21:53
orestis-z and others added 3 commits October 27, 2026 16:12
Every training step built a handful of metric scalars with
``torch.tensor(<python scalar>, device=cuda)``. That is a copy from
pageable host memory, so it cannot be issued asynchronously -- the
caller blocks until the work already queued on the device drains. The
same step also ran ``torch.masked_select`` on the accuracy mask, whose
output size depends on the mask's values, forcing another device->host
sync before the count could be read.

Build those scalars with ``torch.full``/``torch.zeros``/``torch.ones``,
which fill on the device, and count masked positions with a reduction
over the mask instead of selecting it. The numbers are identical; the
step no longer waits on the device to produce them.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Orestis Zambounis <orestis.zambounis@gmail.com>
Signed-off-by: Orestis Zambounis <23146389+orestis-z@users.noreply.github.com>
Signed-off-by: Orestis Zambounis <23146389+orestis-z@users.noreply.github.com>
@orestis-z
orestis-z force-pushed the perf/metrics-no-host-sync branch from 73c5749 to 2d9e141 Compare October 27, 2026 15:12
@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:13
@orestis-z
orestis-z merged commit facaac9 into main Oct 27, 2026
11 checks passed
@orestis-z
orestis-z deleted the perf/metrics-no-host-sync branch October 27, 2026 15:23
orestis-z added a commit that referenced this pull request Oct 27, 2026
<!-- markdownlint-disable -->

## 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

```bash
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:

- [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>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready This PR is ready for review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants