feat: add P-EAGLE parallel-drafting drafter backend - #18
Conversation
Port P-EAGLE (parallel-drafting EAGLE) from NeMo AutoModel into the SpeCo overlay. Unlike EAGLE-3's sequential test-time-training recurrence, P-EAGLE predicts all num_depths tokens in a single parallel forward over a flat, COD-subsampled sequence and is supervised by a count-normalized KL(target || draft) over the draft vocab (no per-depth decay). New verl_speco/models/peagle/: COD sampling and the flex-attention COD block mask (verbatim ports), plus LlamaForCausalLMPeagle (fused layer 0 over [embed, hidden], vanilla deep layers, a learnable mask_hidden placeholder for masked depths, flex attention with the COD mask). PEagleTrainerBackend subclasses Eagle3TrainerBackend and reuses the aux + last-hidden collection, but reports model_type=peagle so base_trainer assembles the UNSHIFTED per-position batch P-EAGLE needs; the frozen target head turns last_hidden_states into the target logits. Wired through the worker dispatch and config keys. P-EAGLE targets vLLM's parallel-drafting runtime for serving; the vLLM config builder raises until that runtime is wired, so this PR is training-side. AI assistance was used for this change. Signed-off-by: khazic <khazzz1c@gmail.com>
…cess) AI assistance was used for this change. Signed-off-by: khazic <khazzz1c@gmail.com>
There was a problem hiding this comment.
Code Review
This pull request introduces the P-EAGLE (parallel-drafting) drafter training backend, including its configuration, modeling with Conditional-On-Distribution (COD) sampling, custom flex-attention masking, integration into the base trainer, and associated tests. The review feedback identifies a critical issue where independent sequences in a batch are treated as a single document, causing cross-document attention leakage; to resolve this, individual sequence lengths must be preserved and passed in the batch. Additionally, several minor improvements are suggested, such as removing a redundant deepcopy call, using torch.full for cleaner tensor initialization, and replacing squeeze(0) with flatten() for safer tensor reshaping.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| down_sample_ratio_min=down_sample_ratio_min, | ||
| ) | ||
| orig_positions = anchor_pos + depth | ||
| row_length = attention_mask[b].sum().clamp_min(1).reshape(1).to(device) |
There was a problem hiding this comment.
Because base_trainer.py concatenates all sequences in the batch into a single flat sequence of batch size 1, attention_mask[0] is a flat tensor of ones of length seq_len (the sum of all sequence lengths). As a result, row_length is computed as a single-element tensor [seq_len]. When passed to create_peagle_mask_mod, this causes document_ids to be a tensor of all 0s of length seq_len, meaning all independent sequences in the batch are treated as a single document. This allows tokens from independent prompts/documents to cross-attend to each other, leading to severe document leakage and training contamination. To fix this, retrieve the individual sequence lengths from the batch.
row_length = batch.get(\"seq_lengths\")
if row_length is None:
row_length = attention_mask[b].sum().clamp_min(1).reshape(1).to(device)| elif self.backend.model_type == "peagle": | ||
| batch["last_hidden_states"] = last_hidden_states |
There was a problem hiding this comment.
To support proper document isolation and prevent cross-document attention leakage in P-EAGLE training, we should preserve the individual sequence lengths of the concatenated chunks and pass them in the batch.
elif self.backend.model_type == \"peagle\":
batch[\"last_hidden_states\"] = last_hidden_states
batch[\"seq_lengths\"] = torch.tensor([chunk.size(0) for chunk in input_id_chunks], dtype=torch.long, device=dev)| return PeagleConfig.from_pretrained(spec_model_path) | ||
|
|
||
| training_cfg = self._training_cfg() | ||
| cfg_dict = deepcopy(target_hf_config).to_dict() |
There was a problem hiding this comment.
| document_ids = torch.cat( | ||
| [ | ||
| document_ids, | ||
| -1 * torch.ones(total_seq_len - document_ids.shape[0], device=lengths.device, dtype=torch.long), |
There was a problem hiding this comment.
Using torch.full is more direct and idiomatic for creating a tensor filled with a specific value than multiplying torch.ones by -1.
| -1 * torch.ones(total_seq_len - document_ids.shape[0], device=lengths.device, dtype=torch.long), | |
| torch.full((total_seq_len - document_ids.shape[0],), -1, device=lengths.device, dtype=torch.long), |
| Returns ``(anchor_pos, depth)`` flat tensors of shape ``[total_sampled]``. | ||
| The reference (target) position of each element is ``anchor_pos + depth``. | ||
| """ | ||
| loss_mask = loss_mask.squeeze(0) |
Address the review on verl-project#18. base_trainer concatenates every document into one flat batch-1 sequence with an all-ones attention_mask, so the P-EAGLE COD mask derived document boundaries from attention_mask.sum() and saw a single giant document, letting depth-0 positions cross-attend between independent documents (training contamination). Pass the per-document chunk lengths (seq_lengths) from base_trainer and build the COD document_ids from them; fall back to the single-document behaviour only when unavailable. Add a regression test asserting a cross-document depth-0 pair is masked while the merged-length variant leaks. Also apply the review nits: drop the redundant deepcopy before to_dict, use torch.full for the padding fill, and flatten() instead of squeeze(0). Signed-off-by: khazic <khazzz1c@gmail.com>
|
Thanks for the review. Verified and fixed the high-priority one against the code path: Cross-document leakage (real). Also applied the three nits: dropped the redundant Pushed in 0acc2a0. |
…drafter # Conflicts: # verl_speco/config/speco_trainer.yaml
The P-EAGLE trainer is a verbatim port of NeMo AutoModel's _peagle_position_loss, but the reference feeds that code SHIFTED tensors: its target wrapper left-shifts input_ids, target logits, and loss_mask by one (aux hidden states stay unshifted), so element p pairs aux[p] with the NEXT token x[p+1], predicts the distribution of x[p+2], and is gated by loss_mask[p+1]. The port kept the trainer verbatim but assembled fully unshifted batches, which trains a different function: (aux[p], x[p]) predicting x[p+1]. At serve time the parallel-drafting runtime feeds (aux[t], x[t+1]) pairs, since the hidden state of the newest sampled token does not exist yet, so a drafter trained on the unshifted pairing is misaligned with inference by one position. Fix the assembly to mirror the reference shift exactly: ids[1:1+T], aux[:T], last_hidden[1:1+T], loss_mask[1:1+T], with bounds reduced by one so the slices stay aligned. The backend stays a verbatim port; its docstring now documents the shifted contract. A contract test pins the assembled batch against the reference slices. Signed-off-by: khazic <khazzz1c@gmail.com>
_prepare_training_batch rebuilds the batch dict after the Ulysses pad/slice section, and the rebuild only re-attached the eagle3 target tensors. The P-EAGLE last_hidden_states and seq_lengths set on the pre-sanitize dict were silently dropped, so the real assembly path failed compute_loss's last_hidden_states check on the first step. The GPU smoke drives the backend directly and never exercised this path, which is why it passed; the new assembly contract test catches it. Signed-off-by: khazic <khazzz1c@gmail.com>
|
Pushed two fixes to this branch. 6691ac1 fix(peagle): apply the reference target-wrapper shift in batch assembly. The trainer here is a verbatim port of the reference 8095358 fix(peagle): keep last_hidden_states and seq_lengths in the final batch. Note: the training-side top1/loss from the earlier smoke were measured on the unshifted pairing, so they are not comparable to post-fix numbers; the shifted task is strictly harder (the draft no longer sees the hidden state of the position right before the predicted token). I will re-run the GPU smoke when cluster access is back. |
…drafter Resolves conflicts with the Domino backend (verl-project#17), EAGLE-1/2 backend (verl-project#15), and the DSpark L1 target-hidden plumbing: the peagle and domino worker dispatch branches, vllm guardrails, and speco_base.yaml config blocks are all additive and kept side by side. The merge also brings in main's supports_ulysses_sp gate, which force-disables Ulysses SP for the P-EAGLE backend (supports_ulysses_sp=False) and enforces the unsliced-batch invariant the peagle assembly relies on. Signed-off-by: khazic <khazzz1c@gmail.com>
What
Adds a P-EAGLE (parallel-drafting EAGLE) drafter training backend to the SpeCo overlay (part of #16). Select it with:
Design
Unlike EAGLE-3's sequential test-time-training recurrence, P-EAGLE predicts all
num_depthstokens in a single parallel forward over a flat, COD-subsampled sequence, supervised by a count-normalizedKL(target || draft)over the draft vocabulary (no per-depth decay). Logic follows the NeMo AutoModel reference (peagle_data.py/peagle_attention.py/peagle_draft.py/peagle_trainer.py, itself a port of speculators PR #480).New
verl_speco/models/peagle/:cod_sampling.pyandpeagle_mask.py: the COD geometric subsampling and the flex-attention COD block mask (verbatim ports). Depth 0 keeps every position; depthdkeeps adown_sample_ratio**dfraction. The mask lets each element attend to the causal depth-0 context of its document plus earlier-or-equal depths of its own rollout.modeling_peagle.py:LlamaForCausalLMPeaglewith a fused layer 0 ([embed, hidden]-> 2H attention), vanilla deep layers, a single learnablemask_hiddenplaceholder that substitutes for the target aux feature at masked (depth>=1) slots, and flex attention driven by the COD block mask.PEagleTrainerBackendsubclassesEagle3TrainerBackendand reuses the aux + last-hidden collection, but reportsmodel_type == "peagle"sobase_trainerassembles the UNSHIFTED per-position batch P-EAGLE needs (auxf[p], tokenx[p], target distribution forx[p+1]). The frozen target head turnslast_hidden_statesinto the full-vocab target logits, which are restricted to the draft vocab viat2d. Onlybuild_modelandcompute_lossdiffer; preprocess/optimizer/target-head are inherited.flex_attentionis run eagerly: the COD block mask captures per-element index tensors and the compiled (max-autotune, dynamic) backward is unstable on it (illegal memory access); an opt-inVERL_PEAGLE_COMPILE_FLEX=1keeps the compile path for later verification.P-EAGLE targets vLLM's parallel-drafting runtime for serving; the vLLM config builder raises until that runtime is wired here, so this PR is training-side.
Not a duplicate
No open PR touches P-EAGLE (
gh pr list); #13 and #10 are unrelated.Testing
A human reviewed every changed line and ran the tests below.
CPU contract tests (
tests/integration/test_peagle_backend_contract.py), 6 passed: COD sampling structure (depth-0 keeps all positions, deeper depths shrink geometrically), the flex-mask predicate (depth-0 causal context + own-rollout depth order), the draft-model modules (fc/mask_hidden/lm_head/selected_token_ids), the KL loss, the backend metadata, and the vLLM guardrail. The full suite is94 passed, 2 skipped, 2 failed; both failures pre-exist onmainand are unrelated.GPU hardware smoke (
ci/peagle_gpu_smoke.py) on a real target (Qwen3-4B, 1x A100): it collects the unshifted per-position data (3 aux layers + the final hidden + tokens + loss mask) from the frozen target, builds the P-EAGLE draft viaPEagleTrainerBackend.build_model, and runs 150 optimizer steps throughcompute_loss(COD sampling -> flat 8-depth flex-attention forward -> count-normalized KL). The draft is cold-started, so the signal is convergence: the KL loss falls and draft-vs-target top-1 agreement rises across the 8 parallel depths.P-EAGLE GPU training log (Qwen3-4B target, cold start, 8 depths, 150 steps)
Known follow-ups
mask_hiddenand the COD config for that runtime).VERL_PEAGLE_COMPILE_FLEX=1) pending a verified compiled backward.