Skip to content

Commit 11d8d07

Browse files
committed
Update
[ghstack-poisoned]
1 parent 57ef8a5 commit 11d8d07

1 file changed

Lines changed: 241 additions & 0 deletions

File tree

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
"""EAGLE-3 eager reference: greedy chain speculative decoding.
8+
9+
Loads a gemma4-31B target with EAGLE-3 hidden-state taps and an EAGLE-3 draft
10+
head, proposes a fixed-length draft chain, verifies it with target logits, and
11+
emits accepted draft tokens plus the target bonus token.
12+
13+
The script compares speculative output with greedy target decoding and reports
14+
per-position acceptance rates ``n-alpha`` plus average emitted tokens per
15+
verification round ``tau``. It recomputes full sequences instead of using a KV
16+
cache.
17+
18+
Usage:
19+
python -m executorch.examples.models.eagle3.eager_reference \\
20+
--target /path/to/gemma4-31b-int4 \\
21+
--draft /path/to/eagle3-draft-head \\
22+
--prompt "Explain why the sky is blue." \\
23+
--num-gen 64 --chain 3
24+
"""
25+
26+
import argparse
27+
import os
28+
29+
import torch
30+
31+
from executorch.examples.models.eagle3.draft import Eagle3Draft
32+
from executorch.examples.models.gemma4_31b.export import load_prequantized_model
33+
from executorch.examples.models.gemma4_31b.inference import (
34+
_move_to_cuda,
35+
apply_chat_template,
36+
)
37+
38+
EOS_TOKEN_IDS = {1, 50, 106}
39+
BOS_TOKEN_ID = 2
40+
41+
42+
def load_target(target_dir: str, max_seq_len: int, bf16: bool = False):
43+
"""Load the gemma4-31B target from an INT4 directory or bf16 HF checkpoint."""
44+
if bf16:
45+
from executorch.examples.models.gemma4_31b.model import Gemma4_31B
46+
47+
model, config = Gemma4_31B.from_hf_checkpoint(
48+
target_dir, max_seq_len=max_seq_len
49+
)
50+
_move_to_cuda(model, config)
51+
model.eval()
52+
return model
53+
54+
model, config = load_prequantized_model(
55+
target_dir, max_seq_len=max_seq_len, backend="cuda"
56+
)
57+
_move_to_cuda(model, config)
58+
model.eval()
59+
import executorch.backends.cuda.int4_dispatch # noqa: F401
60+
61+
return model
62+
63+
64+
class Target:
65+
"""Wraps the gemma4-31B target: full-sequence forward returning logits + taps."""
66+
67+
def __init__(self, model, tap_layers):
68+
self.model = model
69+
if tap_layers:
70+
model.set_eagle_tap_layers(tap_layers)
71+
72+
@torch.no_grad()
73+
def forward(self, token_ids: list[int]):
74+
toks = torch.tensor([token_ids], dtype=torch.long, device="cuda")
75+
pos = torch.arange(len(token_ids), dtype=torch.long, device="cuda")
76+
# The verifier reads logits for every proposed-token position.
77+
logits, taps = self.model.forward_logits_taps(toks, pos, last_logits_only=False)
78+
return logits[0], taps[0] # (L, vocab), (L, 3*hidden)
79+
80+
81+
@torch.no_grad()
82+
def embed_tokens(draft: Eagle3Draft, token_ids: list[int]) -> torch.Tensor:
83+
ids = torch.tensor(token_ids, dtype=torch.long, device="cuda")
84+
return draft.embed(ids)
85+
86+
87+
@torch.no_grad()
88+
def draft_chain(
89+
draft: Eagle3Draft,
90+
confirmed_ids: list[int],
91+
taps_confirmed: torch.Tensor,
92+
chain_len: int,
93+
) -> list[int]:
94+
"""Propose ``chain_len`` tokens with target taps followed by recurrent features."""
95+
feats = draft.fuse(taps_confirmed.unsqueeze(0)) # (1, L, hidden)
96+
tokens = list(confirmed_ids)
97+
proposals = []
98+
for _ in range(chain_len):
99+
emb = embed_tokens(draft, tokens).unsqueeze(0) # (1, L, hidden)
100+
pos = torch.arange(len(tokens), dtype=torch.long, device="cuda")
101+
dlogits, g = draft(emb, feats, pos)
102+
draft_id = int(dlogits[0, -1].argmax())
103+
tgt_id = int(draft_id + draft.d2t[draft_id])
104+
proposals.append(tgt_id)
105+
tokens.append(tgt_id)
106+
feats = torch.cat([feats, g[:, -1:, :]], dim=1)
107+
return proposals
108+
109+
110+
@torch.no_grad()
111+
def speculative_decode(draft, target, prompt_ids, num_gen, chain_len):
112+
seq = list(prompt_ids)
113+
emitted = []
114+
reached = [0] * chain_len
115+
accepted = [0] * chain_len
116+
accept_lengths = []
117+
118+
while len(emitted) < num_gen:
119+
L = len(seq)
120+
_, taps = target.forward(seq)
121+
proposals = draft_chain(draft, seq, taps, chain_len)
122+
123+
vlogits, _ = target.forward(seq + proposals)
124+
a = 0
125+
for j in range(chain_len):
126+
reached[j] += 1
127+
tgt_tok = int(vlogits[L - 1 + j].argmax())
128+
if tgt_tok == proposals[j]:
129+
accepted[j] += 1
130+
a += 1
131+
else:
132+
break
133+
134+
accepted_tokens = proposals[:a]
135+
eos_pos = next(
136+
(i for i, tok in enumerate(accepted_tokens) if tok in EOS_TOKEN_IDS),
137+
None,
138+
)
139+
if eos_pos is not None:
140+
new_tokens = accepted_tokens[: eos_pos + 1]
141+
else:
142+
corrected = int(vlogits[L - 1 + a].argmax()) # target's own greedy token
143+
new_tokens = accepted_tokens + [corrected]
144+
145+
remaining = num_gen - len(emitted)
146+
new_tokens = new_tokens[:remaining]
147+
seq += new_tokens
148+
emitted += new_tokens
149+
accept_lengths.append(min(len(new_tokens), len(accepted_tokens)))
150+
if any(t in EOS_TOKEN_IDS for t in new_tokens):
151+
break
152+
153+
n_alpha = [
154+
accepted[j] / reached[j] if reached[j] else 0.0 for j in range(chain_len)
155+
]
156+
return emitted, n_alpha, accept_lengths
157+
158+
159+
@torch.no_grad()
160+
def greedy_decode(target, prompt_ids, num_gen):
161+
seq = list(prompt_ids)
162+
out = []
163+
while len(out) < num_gen:
164+
logits, _ = target.forward(seq)
165+
t = int(logits[-1].argmax())
166+
seq.append(t)
167+
out.append(t)
168+
if t in EOS_TOKEN_IDS:
169+
break
170+
return out
171+
172+
173+
def main():
174+
p = argparse.ArgumentParser(description="EAGLE-3 eager reference (greedy chain).")
175+
p.add_argument("--target", required=True, help="gemma4-31B prequantized dir.")
176+
p.add_argument("--draft", required=True, help="EAGLE-3 draft head dir.")
177+
p.add_argument("--tokenizer-path", default=None)
178+
p.add_argument("--prompt", default="Explain why the sky is blue.")
179+
p.add_argument("--raw-prompt", action="store_true")
180+
p.add_argument("--num-gen", type=int, default=64)
181+
p.add_argument("--chain", type=int, default=3)
182+
p.add_argument("--max-seq-len", type=int, default=4096)
183+
p.add_argument(
184+
"--bf16", action="store_true", help="Target is a bf16 HF checkpoint dir."
185+
)
186+
args = p.parse_args()
187+
188+
if not torch.cuda.is_available():
189+
p.error("CUDA required.")
190+
if args.num_gen < 1 or args.chain < 1:
191+
p.error("--num-gen and --chain must be >= 1.")
192+
193+
tok_path = args.tokenizer_path or os.path.join(args.target, "tokenizer.json")
194+
from tokenizers import Tokenizer
195+
196+
tokenizer = Tokenizer.from_file(tok_path)
197+
prompt_str = args.prompt if args.raw_prompt else apply_chat_template(args.prompt)
198+
prompt_ids = tokenizer.encode(prompt_str).ids
199+
if not prompt_ids or prompt_ids[0] != BOS_TOKEN_ID:
200+
prompt_ids = [BOS_TOKEN_ID] + prompt_ids
201+
202+
print(f"Loading target from {args.target} (bf16={args.bf16}) ...")
203+
target_model = load_target(args.target, args.max_seq_len, bf16=args.bf16)
204+
draft, dcfg = Eagle3Draft.from_checkpoint(args.draft, device="cuda")
205+
target = Target(target_model, dcfg.aux_hidden_state_layers)
206+
207+
print(
208+
f"\nPrompt: {args.prompt}\nPrompt tokens: {len(prompt_ids)}, chain={args.chain}"
209+
)
210+
print("-" * 60)
211+
212+
emitted, n_alpha, accept_lengths = speculative_decode(
213+
draft, target, prompt_ids, args.num_gen, args.chain
214+
)
215+
greedy_out = greedy_decode(target, prompt_ids, len(emitted))
216+
217+
n = min(len(emitted), len(greedy_out))
218+
lossless = emitted[:n] == greedy_out[:n]
219+
rounds = len(accept_lengths)
220+
tau = len(emitted) / rounds if rounds else 0.0
221+
avg_accepted = sum(accept_lengths) / rounds if rounds else 0.0
222+
223+
print(tokenizer.decode(emitted))
224+
print("-" * 60)
225+
print(f"lossless (== greedy): {lossless}")
226+
if not lossless:
227+
for i in range(n):
228+
if emitted[i] != greedy_out[i]:
229+
print(
230+
f" first divergence at {i}: spec={emitted[i]} greedy={greedy_out[i]}"
231+
)
232+
break
233+
print(f"rounds: {rounds}, emitted: {len(emitted)}")
234+
print(f"tau (avg acceptance length, incl. bonus): {tau:.3f}")
235+
print(f"avg accepted draft tokens/round: {avg_accepted:.3f} / {args.chain}")
236+
for j, a in enumerate(n_alpha):
237+
print(f" {j}-alpha: {a:.3f}")
238+
239+
240+
if __name__ == "__main__":
241+
main()

0 commit comments

Comments
 (0)