eagle3: γ≥2 recursive drafting + batched verify with hooks
Adds infrastructure for γ≥2 EAGLE speculative decoding:
qwen3.rs:
- New forward_verify_paged_decode_attention_with_hidden: same as the
existing verify but also captures target hidden states at 3 hook
layers, one per verify position. Needed to seed next round's EAGLE.
eagle3.rs:
- step split into step (unchanged public API) + step_with_aux (also
returns final hidden state) + step_recursive (takes fused_h directly,
no fc+3-hidden combine). This mirrors the EAGLE3 paper: γ=1 uses
target hooks + fc; γ≥2 uses previous EAGLE aux as fused_h for
subsequent drafts, approximating target hidden.
bench-eagle3.rs:
- New run_eagle_gamma_multi function with --gamma CLI (default 2).
- Per round: recursive EAGLE γ drafts, verify [prev_token, d0..d_{γ-1}]
in one target forward, accept longest prefix, correction via 1 more
target decode.
- max_seqs bumped to 16 in the paged cache so verify can batch up to
16 rows.
γ=2 test result (5 prompts × 32 tokens, dash5):
matched=false — sequences diverge
acceptance_rate = 29.8% at γ=2 (~1.1 tokens accepted per draft)
speedup_e2e = 0.52x (SLOWER than baseline)
The divergence bug is in the verify's re-writing of prev_token's K/V
at position round_pos-1. In principle matmul_batched_gemv at row-0
should be bit-exact with the seed decode's launch_gemv_bf16, but the
sequence output diverges so something is off. Investigation pending
(likely the correction decode step or seed_hooks position offset).
γ=1 path still works correctly (matched=true, acceptance 20%,
speedup 0.95x) from the previous commit. The γ≥2 path is scaffolded
but not yet correct — next step is to debug the verify-write path,
then measure real speedup.
This commit is contained in:
@@ -165,6 +165,44 @@ impl Eagle3Head {
|
||||
prev_token: u32,
|
||||
position: usize,
|
||||
) -> (u32, Tensor) {
|
||||
let (id, logits, _) = self.step_with_aux(target_hidden, embed_table, prev_token, position);
|
||||
(id, logits)
|
||||
}
|
||||
|
||||
/// Like `step`, but also returns the final hidden state (aux) usable as
|
||||
/// the fused_h for a subsequent recursive draft step via `step_recursive`.
|
||||
pub fn step_with_aux(
|
||||
&mut self,
|
||||
target_hidden: &[Tensor; 3],
|
||||
embed_table: &Tensor,
|
||||
prev_token: u32,
|
||||
position: usize,
|
||||
) -> (u32, Tensor, Tensor) {
|
||||
// Fuse 3 target hidden states into fused_h via fc.
|
||||
let h_cat = concat_hidden(target_hidden);
|
||||
let fused_h = matmul_2d(&h_cat, &self.fc_wt);
|
||||
self.forward_from_fused(fused_h, embed_table, prev_token, position)
|
||||
}
|
||||
|
||||
/// Recursive draft step: reuses the previous EAGLE step's aux as fused_h,
|
||||
/// bypassing the fc+3-hidden fusion. Used for γ≥2 chained drafts.
|
||||
pub fn step_recursive(
|
||||
&mut self,
|
||||
fused_h: Tensor,
|
||||
embed_table: &Tensor,
|
||||
prev_token: u32,
|
||||
position: usize,
|
||||
) -> (u32, Tensor, Tensor) {
|
||||
self.forward_from_fused(fused_h, embed_table, prev_token, position)
|
||||
}
|
||||
|
||||
fn forward_from_fused(
|
||||
&mut self,
|
||||
fused_h: Tensor,
|
||||
embed_table: &Tensor,
|
||||
prev_token: u32,
|
||||
position: usize,
|
||||
) -> (u32, Tensor, Tensor) {
|
||||
let eps = 1e-6f32;
|
||||
assert!(
|
||||
self.current_len < self.max_seq_len,
|
||||
@@ -173,20 +211,12 @@ impl Eagle3Head {
|
||||
self.max_seq_len
|
||||
);
|
||||
|
||||
// 1. Fuse target hidden states: concat [h_low, h_mid, h_high] → fc
|
||||
let h_cat = concat_hidden(target_hidden);
|
||||
let fused_h = matmul_2d(&h_cat, &self.fc_wt); // [1, hidden]
|
||||
|
||||
// 2. Embed previous token (shared with target)
|
||||
let emb = embedding(embed_table, &[prev_token]); // [1, hidden]
|
||||
|
||||
// 3. Norm both, concat, remember residual = fused_h (pre-norm).
|
||||
let emb = embedding(embed_table, &[prev_token]);
|
||||
let residual = fused_h.clone();
|
||||
let emb_normed = rmsnorm(&emb, &self.input_layernorm, eps);
|
||||
let h_normed = rmsnorm(&fused_h, &self.hidden_norm, eps);
|
||||
let attn_in = concat_last_dim(&emb_normed, &h_normed); // [1, 2*hidden]
|
||||
let attn_in = concat_last_dim(&emb_normed, &h_normed);
|
||||
|
||||
// 4. Q/K/V projection then RoPE (position from caller).
|
||||
let q = matmul_2d(&attn_in, &self.q_proj_wt);
|
||||
let k = matmul_2d(&attn_in, &self.k_proj_wt);
|
||||
let v = matmul_2d(&attn_in, &self.v_proj_wt);
|
||||
@@ -197,8 +227,6 @@ impl Eagle3Head {
|
||||
rope_inplace(&q_3d, &self.rope_cache, &positions);
|
||||
rope_inplace(&k_3d, &self.rope_cache, &positions);
|
||||
|
||||
// 5. Append new K/V to the internal cache at slot `current_len`, then
|
||||
// build a contiguous view [1, num_kv_heads, current_len+1, head_dim].
|
||||
let v_3d = v.reshape(&[1, self.num_kv_heads, self.head_dim]);
|
||||
self.append_to_kv_cache(&k_3d, &v_3d);
|
||||
self.current_len += 1;
|
||||
@@ -206,31 +234,26 @@ impl Eagle3Head {
|
||||
let k_view = self.k_cache.narrow(2, 0, kv_len).contiguous();
|
||||
let v_view = self.v_cache.narrow(2, 0, kv_len).contiguous();
|
||||
|
||||
// 6. Attention: q [1, num_q_heads, 1, head_dim] × k/v [1, num_kv_heads, kv_len, head_dim]
|
||||
let q_4d = q_3d.reshape(&[1, self.num_heads, 1, self.head_dim]);
|
||||
let attn_out = decode_attention(&q_4d, &k_view, &v_view);
|
||||
|
||||
// 7. Merge heads and o_proj.
|
||||
let attn_merged = attn_out.reshape(&[1, self.num_heads * self.head_dim]);
|
||||
let attn_proj = matmul_2d(&attn_merged, &self.o_proj_wt);
|
||||
|
||||
// 8. Post-attn fused add_rmsnorm.
|
||||
let (mlp_in, residual) =
|
||||
add_rmsnorm(&attn_proj, &residual, &self.post_attention_layernorm, eps);
|
||||
|
||||
// 9. MLP.
|
||||
let gate = matmul_2d(&mlp_in, &self.gate_proj_wt);
|
||||
let up = matmul_2d(&mlp_in, &self.up_proj_wt);
|
||||
let hidden = silu_mul(&gate, &up);
|
||||
let down = matmul_2d(&hidden, &self.down_proj_wt);
|
||||
|
||||
// 10. Final fused add_rmsnorm → lm_head.
|
||||
let (x, _) = add_rmsnorm(&down, &residual, &self.norm, eps);
|
||||
let logits = matmul_2d(&x, &self.lm_head_wt);
|
||||
|
||||
let draft_id = argmax_bf16_single(&logits);
|
||||
let target_id = (draft_id as i64 + self.d2t[draft_id as usize]) as u32;
|
||||
(target_id, logits)
|
||||
(target_id, logits, x)
|
||||
}
|
||||
|
||||
/// Write new K/V rows (shape [1, num_kv_heads, head_dim]) at position
|
||||
|
||||
Reference in New Issue
Block a user