Two fixes to bring EAGLE3 forward in line with vllm's llama_eagle3.py
reference:
1. Residual chain: previously the residual added into post_attention_layernorm
was the token embedding (wrong). Reference uses _norm_after_residual:
residual = fused_h (pre-norm)
hidden_states = hidden_norm(fused_h)
Then post_attention_layernorm is a fused add_rmsnorm(attn_out, residual),
and the final norm is another add_rmsnorm(mlp_out, residual_after_attn).
Neither residual carries the embedding — both carry fused_h forward.
2. KV cache: previously the attention was approximated as "output = V"
because seq_len=1 (no cache), effectively giving EAGLE no history.
Add a real per-Eagle3Head KV cache (1 layer × [1, num_kv_heads,
max_seq_len, head_dim] BF16) that grows as we call step(). Use the
existing decode_attention kernel with a fresh contiguous slice of the
cache each step. reset() clears current_len for a new sequence.
Result on 10 prompts × 32 tokens (γ=1, no batched verify yet):
matched=true across all prompts
acceptance_rate = 20.0% (was 4.7% before residual fix, 1.3% originally)
- Prompt 00 "The capital of France is": 60% (18/30) — best case
- Other prompts: 10-25% — matches EAGLE paper's observation that
structured/factual prompts get higher acceptance
Sanity check (check-eagle3) on Paris prompt now shows:
EAGLE top-5 pairing A: "." / " is" / "," / " Paris" / ".\n"
MATCH: EAGLE agrees with target on next token.
speedup_e2e still 0.95x because γ=1 does 1 target decode per token
regardless of acceptance. Real speedup requires γ≥2 with a single
batched target-verify covering all γ draft tokens; that's the next step.
175 lines
6.0 KiB
Rust
175 lines
6.0 KiB
Rust
//! EAGLE3 sanity check: load weights, run one draft step, print top-5 predictions.
|
||
//!
|
||
//! This verifies that:
|
||
//! - Eagle3Head weights load without shape mismatches
|
||
//! - Target hidden states can be captured via decode_core_with_hidden
|
||
//! - Eagle3Head::step produces a valid token id (in target vocab)
|
||
//!
|
||
//! Does NOT measure speedup — that requires a full γ≥2 speculative loop, which
|
||
//! is more complex integration work.
|
||
|
||
use std::path::PathBuf;
|
||
|
||
use xserv_model::eagle3::{EAGLE_HOOK_LAYERS, Eagle3Head};
|
||
use xserv_model::{BLOCK_SIZE, ModelConfig, PagedKVCache, Qwen3, loader};
|
||
use xserv_tensor::{DType, Device, Tensor};
|
||
use xserv_tokenizer::Tokenizer;
|
||
|
||
fn main() {
|
||
let args: Vec<String> = std::env::args().collect();
|
||
if args.len() < 3 {
|
||
eprintln!("Usage: check-eagle3 <target-model-dir> <eagle3-model-dir> [prompt]");
|
||
std::process::exit(1);
|
||
}
|
||
let target_dir = PathBuf::from(&args[1]);
|
||
let eagle_dir = PathBuf::from(&args[2]);
|
||
let prompt = args
|
||
.get(3)
|
||
.cloned()
|
||
.unwrap_or_else(|| "The capital of France is".to_string());
|
||
let device: u32 = 0;
|
||
|
||
xserv_cuda::device::set_device(device).unwrap();
|
||
|
||
let target_config = ModelConfig::from_file(&target_dir.join("config.json"));
|
||
eprintln!("Loading target Qwen3-8B...");
|
||
let target_weights = loader::load_model_dir(&target_dir, Device::Cuda(device));
|
||
let target = Qwen3::from_weights(target_config.clone(), target_weights);
|
||
xserv_cuda::allocator::cached_trim();
|
||
|
||
eprintln!("Loading EAGLE3 head from {}", eagle_dir.display());
|
||
let mut eagle = Eagle3Head::load(&eagle_dir, device);
|
||
xserv_cuda::allocator::cached_trim();
|
||
|
||
let tokenizer = Tokenizer::from_file(&target_dir.join("tokenizer.json"));
|
||
let embed_tokens = target.embed_tokens_tensor();
|
||
|
||
let ids = tokenizer.encode(&prompt);
|
||
let max_seq_len = 512;
|
||
|
||
let num_blocks = (max_seq_len + BLOCK_SIZE - 1) / BLOCK_SIZE + 2;
|
||
let mut cache = PagedKVCache::new(
|
||
&target_config,
|
||
num_blocks,
|
||
0,
|
||
1,
|
||
num_blocks,
|
||
DType::BF16,
|
||
device,
|
||
);
|
||
cache.register_sequence(0).unwrap();
|
||
|
||
// Prefill target.
|
||
let logits = target.forward_prefill_paged(&ids, 0, &mut cache);
|
||
let target_first = *xserv_kernels::argmax_bf16_to_host(&logits).last().unwrap();
|
||
let target_first_text = tokenizer.decode(&[target_first]);
|
||
println!("Prompt: {:?}", prompt);
|
||
println!(
|
||
"Target argmax after prefill: {} ({:?})",
|
||
target_first, target_first_text
|
||
);
|
||
|
||
// Now run one target decode step with target_first to get hidden states at the
|
||
// hook layers.
|
||
let pos = cache.seq_len(0);
|
||
target.decode_prepare(&[pos], &[0], &mut cache);
|
||
let ids_gpu = upload_u32(&[target_first]);
|
||
let pos_gpu = upload_u32(&[pos as u32]);
|
||
let (target_next_logits, hooks) = target.decode_core_with_hidden(
|
||
ids_gpu.as_ptr() as *const std::ffi::c_void,
|
||
pos_gpu.as_ptr() as *const std::ffi::c_void,
|
||
1,
|
||
&[0],
|
||
&mut cache,
|
||
&EAGLE_HOOK_LAYERS,
|
||
);
|
||
let target_next = xserv_kernels::argmax_bf16_single(&target_next_logits);
|
||
let target_next_text = tokenizer.decode(&[target_next]);
|
||
println!(
|
||
"Target argmax after 1 decode step: {} ({:?})",
|
||
target_next, target_next_text
|
||
);
|
||
|
||
for (i, h) in hooks.iter().enumerate() {
|
||
println!(
|
||
"hook[{}] (layer {}): shape={:?} dtype={:?}",
|
||
i,
|
||
EAGLE_HOOK_LAYERS[i],
|
||
h.shape(),
|
||
h.dtype()
|
||
);
|
||
}
|
||
|
||
// Ask EAGLE what it thinks the NEXT token is (given target_first as prev_token
|
||
// and the hidden states from the position where target_first lives).
|
||
// EAGLE should predict target_next (or close to it) to be useful.
|
||
eagle.reset();
|
||
let (eagle_pred, eagle_logits) = eagle.step(&hooks, embed_tokens, target_first, pos);
|
||
let eagle_pred_text = tokenizer.decode(&[eagle_pred]);
|
||
println!(
|
||
"EAGLE draft prediction (pairing A: prev=target_first): {} ({:?})",
|
||
eagle_pred, eagle_pred_text
|
||
);
|
||
|
||
if eagle_pred == target_next {
|
||
println!("MATCH: EAGLE agrees with target on next token.");
|
||
} else {
|
||
println!(
|
||
"MISMATCH: EAGLE draft={} vs target={} (this is fine per-step; check top-5 below)",
|
||
eagle_pred, target_next
|
||
);
|
||
}
|
||
|
||
// Show top-5 from eagle logits (in draft vocab space, mapped to target).
|
||
print_top5(
|
||
&eagle_logits,
|
||
"EAGLE draft top-5 (pairing A)",
|
||
&eagle,
|
||
&tokenizer,
|
||
);
|
||
|
||
// Alternative pairing B: pair hooks with target_next (the token those hooks produced
|
||
// via lm_head), predict token after target_next. Position advances by 1.
|
||
eagle.reset();
|
||
let (eagle_pred_b, eagle_logits_b) = eagle.step(&hooks, embed_tokens, target_next, pos + 1);
|
||
let eagle_pred_b_text = tokenizer.decode(&[eagle_pred_b]);
|
||
println!(
|
||
"\nEAGLE draft prediction (pairing B: prev=target_next): {} ({:?})",
|
||
eagle_pred_b, eagle_pred_b_text
|
||
);
|
||
print_top5(
|
||
&eagle_logits_b,
|
||
"EAGLE draft top-5 (pairing B)",
|
||
&eagle,
|
||
&tokenizer,
|
||
);
|
||
}
|
||
|
||
fn upload_u32(vals: &[u32]) -> xserv_cuda::GpuBuffer {
|
||
let bytes = unsafe { std::slice::from_raw_parts(vals.as_ptr() as *const u8, vals.len() * 4) };
|
||
let mut buf = xserv_cuda::allocator::cached_alloc(bytes.len()).unwrap();
|
||
buf.copy_from_host(bytes).unwrap();
|
||
buf
|
||
}
|
||
|
||
fn print_top5(logits: &Tensor, label: &str, eagle: &Eagle3Head, tokenizer: &Tokenizer) {
|
||
use half::bf16;
|
||
let cpu = logits.to_device(Device::Cpu);
|
||
let data = cpu.as_slice::<bf16>();
|
||
let mut vals: Vec<(usize, f32)> = data
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(i, v)| (i, v.to_f32()))
|
||
.collect();
|
||
vals.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
|
||
println!("{label}:");
|
||
for (i, val) in vals.iter().take(5) {
|
||
let target_id = eagle.map_draft_to_target(*i as u32);
|
||
let text = tokenizer.decode(&[target_id]);
|
||
println!(
|
||
" draft_id={} target_id={} val={:.3} text={:?}",
|
||
i, target_id, val, text
|
||
);
|
||
}
|
||
}
|