Skip to content

feat: anchored EAGLE-3 training - #186

Merged
Dogacel merged 2 commits into
lightseekorg:mainfrom
Dogacel:anchored-eagle3
Sep 1, 2026
Merged

feat: anchored EAGLE-3 training#186
Dogacel merged 2 commits into
lightseekorg:mainfrom
Dogacel:anchored-eagle3

Conversation

@Dogacel

@Dogacel Dogacel commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

Adds DFlash style training.eagle3_num_anchors: the TTT unroll is evaluated at N sampled anchor positions per sample instead of at every supervised position, so each depth costs O(N) rather than O(sequence length). Off by default; the dense Eagle3 path is untouched.

Why: Cheaper and uniform length-sequence training. Also a long sequence can dominate the loss heavily, therefore anchor sampling ensures each sample contributes roughly equally, removing the bias for model to lean towards longer samples.

How it works

Traditionally, EAGLE-3 trains with TTT rollout on every loss enabled token. The KV cache structure is shown below. This is expensive in terms of memory and runtime, especially for longer sequences.

image

DFlash has a different training method, it randomly samples "anchor tokens" and predicts N block continuations from each token. It achieves this by sorting each anchor token and create a bidirectional attention mask for each block prediction. This technique helps DFlash train stabily on longer sequences, since anchor token count doesn't change for longer sequences. Faster and potentially more stable training.

Screenshot 2026-08-30 at 3 20 06 PM

This PR combines both approaches, instead of doing TTT rollout on every token, we sample random anchor tokens to calculate loss from. Each token unsampled contributes through KV cache contributes anyway, so we don't need the full coverage to get similar results.

image

This means we calculate loss from significantly tokens, however results already show this doesn't hurt meaningfully. Below is an image of loss calculation coverage. The dark color means contributes more to the loss, since there is weight decay on same TTT (Same as DFlash)

image

Below is anchored, note that each token indirectly contributes via their KV cache anyway.

image

Since each token's weight is different, model can be trained for more epochs. In each epoch, data order will be random, so an un-seen token, or a seen but different TTT / weight token can contribute in the other epoch.

Runtime

Attention only (TTT=4, N=256, one GB300, ms / peak GB)

seq dense TTT anchored SDPA anchored BlockMask anchored score_mod
4096 1.78 / 0.24 1.12 / 0.08 0.97 / 0.08 0.97 / 0.08
6144 3.82 / 0.44 1.10 / 0.12 1.38 / 0.15 1.39 / 0.11
16384 20.77 / 1.11 2.30 / 0.30 3.44 / 0.34 3.45 / 0.31
32768 75.95 / 2.06 4.24 / 0.59 6.73 / 0.61 6.75 / 0.57
131072 1134.53 / 7.89 15.88 / 2.33 26.43 / 2.22 26.45 / 2.19

Dense time and memory grows quadratically, anchored is roughly flat. SDPA does not cost more memory, since the mask is built with H=1 and broadcast and SDPA tiles rather than materialising the score matrix.

End-to-end training (Qwen3-8B, 20K open-perfectblend, 5 epochs)

Open-perfectblend doesn't have very long rows, so the benefits are minimal, but still visible.

image

DFlash is less data efficient, because predicted blocks are not overlapped with each other, so a token at position X is always treated as a token at (X mod block_size) position. Because position of a token also changes prediction hardness + loss weight, model doesn't see all tokens truly during training.

This has been validated by running EAGLE with non-overlapped anchors, the result was significantly worse than regular EAGLE, but it was better than DFlash and DFlash 2.

Method acc len step/s samples/s opt steps accum state
DFlash 0.39 5.1 10.1 12370 2 completed
DFlash2 0.43 4.7 9.4 12370 2 completed
EAGLE-3 (dense) 0.78 17.4 17.4 24745 1 completed
Anchored, scattered 0.47 18.8 18.8 12163 1 died @ 12163
Anchored, adjacent 0.78 9.4 18.9 12370 2 completed

Also added block mask compilation for DFlash, which speeds up training by 10%,

block mask build is EAGER      →  9.26 samples/s
block mask build is COMPILED   → 10.24 samples/s   (1.106x)

Multi-layer sweep (N=1024, TTT=4, one GB300, ms / peak GB)

seq dense TTT anchored SDPA speedup
4096 L=1 1.79 / 0.24 1.24 / 0.17 1.44×
L=2 3.53 / 0.24 2.84 / 0.17 1.24×
L=5 8.78 / 0.24 6.99 / 0.17 1.26×
6144 L=1 3.82 / 0.44 1.44 / 0.23 2.65×
L=2 7.63 / 0.44 4.51 / 0.29 1.69×
L=5 18.99 / 0.44 11.15 / 0.29 1.70×
32768 L=1 75.89 / 2.06 5.37 / 1.02 14.1×
L=2 151.79 / 2.06 47.40 / 1.24 3.2×
L=5 379.35 / 2.06 118.42 / 1.24 3.2×

MLA

seq dense TTT anchored SDPA speedup
4096 2.89 / 0.92 1.21 / 0.43 2.4x
6144 5.99 / 1.69 1.19 / 0.63 5.0x
16384 34.27 / 4.20 2.17 / 1.60 15.8x
32768 127.48 / 7.91 4.02 / 3.17 31.7x

Real end to end run timees.

pblong10k, MLA draft, 16k seq compute_time data_time step_time samples/s
dense, 1 train / 3 infer 0.1540 0.0374 0.1625 12.31
anchored, 1 train / 3 infer 0.1244 0.0299 0.1547 12.93
data median supervised samples > N GPU split compute end-to-end
pblong10k 1401 100% 1 train / 3 infer 1.24x 1.05x

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7326fa6ed3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread torchspec/models/anchored_eagle3.py Outdated
Comment on lines +226 to +227
step_mask = _gather(loss_mask, positions) * keep_mask
step_target = _gather_target(target, positions)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve the pruned-vocabulary position mask

When draft_vocab_size < vocab_size, compute_target_p_padded() uses PrecomputedTarget.position_mask to exclude positions whose full-vocabulary argmax is unavailable to the draft, and the dense Eagle3 path honors that mask. This path instead samples and masks solely with loss_mask, while _gather_target() also drops position_mask, so impossible targets silently contribute a renormalized KL loss and bias training. Use the target position mask as the effective mask for anchor sampling and each depth.

Useful? React with 👍 / 👎.

Comment thread torchspec/models/anchored_eagle3.py Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3faeb79f4a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread torchspec/models/ops/anchors.py Outdated
Comment on lines +67 to +68

# An anchor is only usable if its own position and the position right

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Sample Eagle anchors without requiring adjacent supervision

When a dataset contains isolated supervised tokens or one-token assistant responses, this DFlash-specific adjacency predicate produces no Eagle anchors even though dense Eagle3 would train its depth-0 prediction; keep_mask is therefore entirely false and every returned loss is silently zero. More generally, it excludes the final supervised position of every response from depth 0 and biases the anchored objective. The Eagle path needs its own candidate predicate based on the positions valid for each TTT depth rather than reusing the DFlash requirement that both anchor and anchor + 1 be supervised.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a63726ce6b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread torchspec/models/anchored_eagle3.py
Comment on lines 143 to 144
lk_eta=getattr(self.args, "lk_eta", 3.0),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject USP when enabling anchored Eagle3

When eagle3_num_anchors is combined with attention_backend="usp", the USP data fetcher supplies a sequence shard per rank, but AnchoredEagle3Model.forward bypasses the draft's USP attention implementation, ignores the supplied global position_ids, and runs local SDPA over that shard only. Anchors therefore cannot attend to preceding context held by other sequence-parallel ranks, silently corrupting training; reject this combination or implement the required USP communication and global-position handling before selecting the anchored model.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dba6e35101

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread torchspec/models/ops/anchors.py
Comment on lines +208 to +209
hidden = current + attn.o_proj(attn_out)
hidden = hidden + layer.mlp(layer.post_attention_layernorm(hidden))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor gradient checkpointing in the anchored unroll

When training.gradient_checkpointing=true, the anchored path still executes the attention and MLP for every depth directly, whereas the dense Eagle path checkpoints its backbone invocation. The option therefore only checkpoints the loss helper here and silently retains all layer activations across the recurrent depths, which can negate the expected memory reduction or cause long-sequence runs to OOM. Wrap the per-depth layer computation in checkpointing when the flag is enabled.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Anchoring already saves significant memory, so not a P0.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a7196221b9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +157 to +159
return sample_anchor_positions(
seq_len, loss_mask, self.num_anchors, self.block_size, device
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve uniform DFlash anchor sampling

Delegating DFlash to this strided sampler silently changes its established random-anchor objective. For example, with 700 valid candidates and 512 anchors, valid_counts // num_anchors is 1, so each step selects a contiguous 512-token window; central candidates are selected nearly every time while edge candidates are rarely selected, instead of every candidate having probability 512/700 as in the previous implementation. This biases DFlash training on common sequences with between one and two anchor budgets of supervision; keep uniform random sampling for DFlash or make the new spacing behavior explicitly opt-in.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ab79f568a9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread torchspec/models/anchored_eagle3.py Outdated
Comment on lines +175 to +178
cos, sin = attn.rotary_emb(key_cache, seq_len=seq_len + self.length)
cos, sin = cos.to(device), sin.to(device)
context_positions = torch.arange(seq_len, device=device).expand(bsz, -1)
key_cache, _ = apply_rotary_pos_emb(key_cache, key_cache, cos, sin, context_positions)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject or implement multimodal RoPE drafts

When anchoring is enabled for a supported Llama-style draft whose rope_scaling uses mrope, this call fails immediately because LlamaMutiRotaryEmbedding.forward accepts position_ids, not the seq_len keyword. The normal attention implementations also use apply_multimodal_rotary_pos_emb with the configured MRoPE sections, whereas this path always applies standard rotary embedding and ignores the supplied multidimensional position_ids; either implement that branch or reject MRoPE drafts during initialization.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b0f7ba991b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread torchspec/models/anchored_eagle3.py Outdated
Add training.eagle3_num_anchors. When set, the TTT unroll is evaluated at N
sampled anchor positions per sample instead of at every supervised position,
so each depth costs O(N) rather than O(sequence length). Off by default; the
dense Eagle3 path is untouched.

The draft is a single layer, so its depth-0 keys and values are a pointwise
projection of the layer input and can be built for the whole sequence without
attention. Only the queries are gathered. KV is laid out as
[context | chain depth 1 | ...] with N slots per depth; an anchor sees the
context up to and including its own position plus its own slot in each chain
block, and anchors never see each other. test_context_rows_match_the_dense_
eagle3_mask asserts a row equals the dense Eagle3 row for that position.

Masking is a materialised [B, 1, N, KV] boolean passed to
scaled_dot_product_attention. Anchored attention is small enough that a
BlockMask costs more to build than the density it removes: at S=4096 with 4
depths the build alone is 2.1ms against 1.2ms for the whole SDPA path.

Anchor sampling and the per-depth mask use the target position mask when the
draft vocabulary is pruned, matching the dense path -- those positions have no
representable argmax, so training on them fits a renormalized target the draft
cannot emit.

MLA drafts work alongside the usual k_proj/v_proj shape. Their keys come from
the compressed kv_a/kv_b pair, which is still pointwise in the layer input, so the
context K/V is built the same way; they rotate only the rope-side dims and carry
their own softmax scale. The rotary table is built once over the whole sequence
rather than through the draft's own helper, which sizes it from the query length --
here the anchor count, so every position past it would index off the end.
configs/vllm_qwen3_8b_anchored.yaml uses an MLA draft by default.

test_per_depth_losses_match_dense covers both draft shapes, and a separate case
covers num_anchors far below seq_len -- the regime where that table sizing goes
wrong and an equal-sized test cannot see it.

MRoPE drafts are rejected: they rotate through apply_multimodal_rotary_pos_emb with
the configured mrope_section rather than the standard rotary this path applies.

Anchors are sampled with a spacing cap. training.eagle3_anchor_max_gap is how
many supervised positions may sit between neighbours, so 0 puts them side by
side. It matters because anchor a supervises token a+d at depth d: a token is
seen at depth d only if the anchor d positions behind it was also picked, so
anchors g apart leave each token ttt_length/(g+1) of its depths, and once g
reaches ttt_length every token is pinned to a single depth. None (the default)
means block_size, which spreads anchors as evenly as the budget allows. Gaps
count supervised positions rather than raw tokens, so no slot is spent on an
unsupervised region and anchors stay token-adjacent inside a span.

Qwen3-8B, 10k PerfectBlend, 5 epochs, N=512: max_gap=0 reaches the dense
simulated acceptance length of 0.78 at 1.09x the samples/s, where sampling
without a cap plateaus at 0.47 from a third of the way in. The gap is not data
volume -- both see the same 512 slots and the same tokens -- it is that
isolated anchors never show a token more than one depth.

The anchor sampler moves out of models/dflash.py to models/ops/anchors.py so
both paths share it. DFlash picks up the default cap, which replaces its
uniformly random draw with an evenly strided one at the same density: same
expected depth coverage, without the isolated anchors that random gaps
produce.

Attention cost against dense TTT (TTT=4, N=256, one GB300, ms):
  seq 4096   dense 1.78   anchored 1.12
  seq 6144   dense 3.82   anchored 1.10
  seq 32768  dense 75.95  anchored 4.24
  seq 131072 dense 1134.5 anchored 15.89

test_dflash's test_accuracy_improves trained for 30 steps, which left the accuracy
in the noise: it improved on only 9 of 12 seeds, so the assertion turned on which
anchors happened to be drawn rather than on whether the model learned. Both the
old and new samplers score 9/12 there, so this is not a regression, but changing
the sampler shifts the RNG stream and moves which seeds land where. At 100 steps
every seed improves by at least 0.18 under either sampler.

Claude-Session: https://claude.ai/code/session_01CqPZAxkfxK7vTCbBQniBni
@Dogacel
Dogacel merged commit 4f44765 into lightseekorg:main Sep 1, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants