Repository navigation
perf(train): keep metric construction off the host - #1294
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the
|
| 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:
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: 
5
-
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.
Comment @coderabbitai help to get the list of available commands.
Merge Protections🟢 Merge protection satisfied — ready to merge. Show 1 satisfied protection🟢 Require approval from approved reviewers listAll pull requests must have at least one approving review from a member of the approved reviewers list before merging.
|
e845fed to
31c78e1
Compare
e845fed to
4a5effc
Compare
<!-- 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>
4a5effc to
cc4cd12
Compare
There was a problem hiding this comment.
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.
cc4cd12 to
c564528
Compare
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>
73c5749 to
2d9e141
Compare
<!-- 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>
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 rantorch.masked_selecton 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 withtorch.full/torch.zeros/torch.onesand counting masked positions with a reduction gives identical numbers without either sync.full_totalandcond_totalstay 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_len2048, 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: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.
† #1299's row is not the prefetch. That commit also pins the synthetic batch in
scripts/benchmark.py, and the realDataLoaderalready setspin_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:
seq_len2048 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.
Tests
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:
🤖 Generated with Claude Code