Skip to content

fix(eagle3): isolate draft grad clipping - #3323

Merged
yuki-97 merged 9 commits into
NVIDIA-NeMo:mainfrom
yuekaizhang:spec
Jul 30, 2026
Merged

fix(eagle3): isolate draft grad clipping#3323
yuki-97 merged 9 commits into
NVIDIA-NeMo:mainfrom
yuekaizhang:spec

Conversation

@yuekaizhang

@yuekaizhang yuekaizhang commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Two fixes for eagle3 draft co-training.

1. Spec decode crashes at the max_model_len boundary

The drafter looks num_speculative_tokens ahead, so when a request reaches
max_model_len it reads/writes KV slots past the engine buffers → CUDA illegal
memory access. Fix: clamp per-request max_tokens to leave
num_speculative_tokens + 1 headroom, so generation stops before the drafter
crosses the boundary. No-op when speculative decoding is off.

2. Draft gradients shrink the policy update

With draft co-training, the freshly-initialized draft head dominates the shared
gradient norm (2–5× the policy-only norm), so the global max_grad_norm clip
rescales the policy gradient much smaller than the no-draft baseline — making
the curves incomparable. Fix: clip the draft head in its own grad-norm group so
policy and draft are clipped independently. No-draft runs are unchanged;
train/grad_norm is now policy-only and train/draft_grad_norm is logged.

Green Curve: baseline without eagle3
Red Curve: ealge3 + RL with the PR's fix
Yello Curve: eagle3 + RL without the PR's fix
image

yuekaizhang and others added 5 commits July 22, 2026 02:13
vLLM 0.20 eagle3 speculative decoding hits a CUDA illegal memory access
when a request's total length reaches max_model_len: the drafter looks
ahead num_speculative_tokens past the current position and reads/writes
KV slots beyond the engine buffers. Crash dumps consistently showed
requests within num_spec_tokens+1 of the cap (e.g. 8188+1+3 == 8192).

Clamp per-request max_tokens to
max_model_len - prompt_len - (num_speculative_tokens + 1) so generation
length-stops before the drafter can cross the boundary. No-op when
speculative decoding is disabled. Give the recipe's vllm max_model_len
the same headroom (e.g. 4096 + 4) to keep generation totals identical
to the non-speculative baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Yuekai Zhang <zhangyuekai@foxmail.com>
With draft co-training (policy.draft.enabled), the draft head's loss is
added to the policy loss and shares one optimizer step. The freshly
initialized draft head dominates the global grad norm (measured 2-5x
the policy-only norm), so the shared clip at max_grad_norm rescaled the
policy gradient 2-5x smaller than an identical no-draft baseline,
making eagle3-vs-baseline training curves incomparable.

Tag draft params with Megatron's separate grad-norm group mechanism
(the 'mtp' precedent): the policy is clipped on its own norm, the draft
head on its own, each to max_grad_norm. Tagging happens only when a
draft model is built, so no-draft runs keep Megatron's stock clipping
byte-for-byte. train/grad_norm is now policy-only; a new
train/draft_grad_norm metric reports the draft group.

Also detach teacher logits in DraftCrossEntropyLossFn's non-TP fallback
to match DistributedCrossEntropy semantics (backward flows only through
student logits, never into the policy).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Yuekai Zhang <zhangyuekai@foxmail.com>
The Megatron policy worker computes the draft model's separate grad norm
and sets metrics["draft_grad_norm"] (megatron_policy_worker.py), but
Policy.train() only hand-copied a fixed set of top-level worker keys
(loss, grad_norm, moe_metrics, mtp_metrics, flops) into aggregated_results.
draft_grad_norm was silently dropped, so it never reached wandb/tensorboard
even though the draft grads are clipped in their own group.

Forward draft_grad_norm the same way mtp_metrics is forwarded, so
train/draft_grad_norm is logged for any run with policy.draft.enabled=true.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Yuekai Zhang <zhangyuekai@foxmail.com>
Add an optional decoupled learning rate for the Eagle draft submodule so a
from-scratch draft can train at a larger LR than the fine-tuned policy.

- policy.draft.lr / min_lr (DraftConfig): when lr is set, draft params get a
  dedicated optimizer param group at that peak LR; unset keeps the policy LR.
- setup.py: _DraftLROverrideProvider passed to setup_optimizer builds a
  ParamGroupOverride({"max_lr": draft_lr}) matching draft params by the
  grad_norm_group=="draft" attribute (robust to Float16Module/DDP name
  prefixes present at optimizer-build time). The scheduler drives each group
  by its own max_lr, same path as decoupled/embedding LR.
- megatron_policy_worker.py: log the draft group's scheduled LR as train/draft_lr
  alongside train/lr.
- grpo.py: add draft_lr to the per-microbatch mean-allowlist (otherwise
  unlisted keys are summed, inflating the logged value).
- add recipe grpo-qwen3-8b-base-1n8g-megatron-eagle3-scratch-draft-draftlr.yaml.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Yuekai Zhang <zhangyuekai@foxmail.com>
This reverts commit 4dd259f.

The decoupled draft learning-rate feature is not needed for now. It is kept in
git history (commit 4dd259f) for future reference and can be cherry-picked
back if we revisit per-submodule LRs for from-scratch draft training.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Yuekai Zhang <zhangyuekai@foxmail.com>
@yuekaizhang
yuekaizhang requested review from a team as code owners July 23, 2026 10:25
@copy-pr-bot

copy-pr-bot Bot commented Jul 23, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@yuekaizhang yuekaizhang changed the title fix(eagle3): isolate draft grad clipping and keep spec decode within max_model_len fix(eagle3): isolate draft grad clipping Jul 23, 2026
@yuekaizhang

Copy link
Copy Markdown
Contributor Author

@isomap @yuki-97 Could you help review the PR? Thanks!

Comment thread nemo_rl/algorithms/loss/loss_functions.py Outdated
yuekaizhang and others added 2 commits July 28, 2026 19:48
…yLossFn

teacher_logits is derived from logits.detach() in utils.py before
reaching this loss path, so the additional .detach() here is redundant.
Keep gradient-boundary ownership in one place.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Yuekai Zhang <zhangyuekai@foxmail.com>
@yuekaizhang yuekaizhang added the CI:Lfast Runs a fast test suite and re-use nightly `main` container (but sync dependencies to PRs version) label Jul 29, 2026
@yuekaizhang

Copy link
Copy Markdown
Contributor Author

/ok to test b9dfa51

Auto-formatted 3 files that ruff-format reformatted in CI.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Yuekai Zhang <zhangyuekai@foxmail.com>
@yuekaizhang

Copy link
Copy Markdown
Contributor Author

/ok to test a8317ca

@yuki-97 yuki-97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

thanks for the fix, LGTM and let's add some tests to guard.

Comment thread nemo_rl/models/megatron/draft/utils.py
Comment thread nemo_rl/models/generation/vllm/vllm_worker.py Outdated
Comment thread nemo_rl/models/generation/vllm/vllm_worker.py
…m group

- Extract _spec_decode_max_tokens as a static method on
  BaseVllmGenerationWorker so the +1 boundary reservation and max(1,…)
  floor are testable without a live vLLM engine.
- Add parametrized test covering clamp-inactive, clamp-active,
  at-boundary, past-boundary, and base-wins cases.
- Add mcore-marked test for register_draft_grad_norm_group verifying
  idempotency and that the pre-existing 'mtp' entry is preserved.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Yuekai Zhang <zhangyuekai@foxmail.com>
@yuekaizhang
yuekaizhang requested a review from a team as a code owner July 30, 2026 06:34
@yuekaizhang

Copy link
Copy Markdown
Contributor Author

/ok to test bbdb04c

@yuki-97
yuki-97 merged commit ba5bbe5 into NVIDIA-NeMo:main Jul 30, 2026
81 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI:Lfast Runs a fast test suite and re-use nightly `main` container (but sync dependencies to PRs version)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants