Skip to content

Commit f6c749f

Browse files
committed
Update
[ghstack-poisoned]
1 parent dc55469 commit f6c749f

4 files changed

Lines changed: 487 additions & 0 deletions

File tree

examples/models/eagle3/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
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.

examples/models/eagle3/draft.py

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
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 draft head for vLLM speculator checkpoints.
8+
9+
The draft model fuses target auxiliary hidden states with ``fc``, runs one
10+
Llama-style decoder layer over token embeddings plus the fused feature, and
11+
projects the midlayer output to reduced-vocabulary draft logits. The midlayer
12+
output ``g`` is reused as the recurrent feature for drafting; ``fc`` is used
13+
only for target auxiliary hidden states.
14+
15+
Draft ids map back to target ids with ``target_id = draft_id + d2t[draft_id]``.
16+
Speculator checkpoints store the decoder layer under ``layers.0.*`` and may
17+
include ``embed_tokens``, ``d2t``, and ``t2d``.
18+
"""
19+
20+
import os
21+
from dataclasses import dataclass, field
22+
23+
import torch
24+
import torch.nn as nn
25+
from torch.nn import functional as F
26+
27+
28+
@dataclass
29+
class Eagle3Config:
30+
hidden_size: int = 5376
31+
target_hidden_size: int = 5376
32+
intermediate_size: int = 21504
33+
num_attention_heads: int = 32
34+
num_key_value_heads: int = 16
35+
head_dim: int = 256
36+
rope_theta: float = 10_000.0
37+
rms_norm_eps: float = 1e-6
38+
draft_vocab_size: int = 32000
39+
target_vocab_size: int = 262144
40+
aux_hidden_state_layers: list = field(default_factory=lambda: [2, 30, 57])
41+
# norm_before_residual: store the attention residual after hidden_norm.
42+
# norm_before_fc: apply an RMSNorm over the concatenated aux features before
43+
# fc (gpt-oss-style speculators checkpoints); not supported here.
44+
# has_own_embed: the head ships its own embed_tokens (set during load).
45+
norm_before_residual: bool = True
46+
norm_before_fc: bool = False
47+
has_own_embed: bool = False
48+
49+
50+
def _rotate_half(x: torch.Tensor) -> torch.Tensor:
51+
x1, x2 = x.chunk(2, dim=-1)
52+
return torch.cat((-x2, x1), dim=-1)
53+
54+
55+
class Eagle3Attention(nn.Module):
56+
"""Llama GQA attention; q/k/v project from the doubled-width (2*hidden) input."""
57+
58+
def __init__(self, config: Eagle3Config):
59+
super().__init__()
60+
self.n_heads = config.num_attention_heads
61+
self.n_kv_heads = config.num_key_value_heads
62+
self.head_dim = config.head_dim
63+
in_dim = 2 * config.hidden_size
64+
65+
self.q_proj = nn.Linear(in_dim, self.n_heads * self.head_dim, bias=False)
66+
self.k_proj = nn.Linear(in_dim, self.n_kv_heads * self.head_dim, bias=False)
67+
self.v_proj = nn.Linear(in_dim, self.n_kv_heads * self.head_dim, bias=False)
68+
self.o_proj = nn.Linear(
69+
self.n_heads * self.head_dim, config.hidden_size, bias=False
70+
)
71+
72+
inv_freq = 1.0 / (
73+
config.rope_theta
74+
** (torch.arange(0, self.head_dim, 2, dtype=torch.float32) / self.head_dim)
75+
)
76+
self.register_buffer("inv_freq", inv_freq, persistent=False)
77+
78+
def forward(self, x: torch.Tensor, positions: torch.Tensor) -> torch.Tensor:
79+
B, T, _ = x.shape
80+
q = self.q_proj(x).view(B, T, self.n_heads, self.head_dim).transpose(1, 2)
81+
k = self.k_proj(x).view(B, T, self.n_kv_heads, self.head_dim).transpose(1, 2)
82+
v = self.v_proj(x).view(B, T, self.n_kv_heads, self.head_dim).transpose(1, 2)
83+
84+
freqs = torch.outer(positions.float(), self.inv_freq)
85+
emb = torch.cat((freqs, freqs), dim=-1)
86+
cos = emb.cos().to(q.dtype)
87+
sin = emb.sin().to(q.dtype)
88+
q = q * cos + _rotate_half(q) * sin
89+
k = k * cos + _rotate_half(k) * sin
90+
91+
y = F.scaled_dot_product_attention(q, k, v, is_causal=True, enable_gqa=True)
92+
y = y.transpose(1, 2).contiguous().view(B, T, self.n_heads * self.head_dim)
93+
return self.o_proj(y)
94+
95+
96+
class Eagle3MLP(nn.Module):
97+
def __init__(self, config: Eagle3Config):
98+
super().__init__()
99+
self.gate_proj = nn.Linear(
100+
config.hidden_size, config.intermediate_size, bias=False
101+
)
102+
self.up_proj = nn.Linear(
103+
config.hidden_size, config.intermediate_size, bias=False
104+
)
105+
self.down_proj = nn.Linear(
106+
config.intermediate_size, config.hidden_size, bias=False
107+
)
108+
109+
def forward(self, x: torch.Tensor) -> torch.Tensor:
110+
return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))
111+
112+
113+
class Eagle3Midlayer(nn.Module):
114+
"""Single EAGLE-3 decoder layer with dual input norms over two streams."""
115+
116+
def __init__(self, config: Eagle3Config):
117+
super().__init__()
118+
self.input_layernorm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
119+
self.hidden_norm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
120+
self.self_attn = Eagle3Attention(config)
121+
self.post_attention_layernorm = nn.RMSNorm(
122+
config.hidden_size, eps=config.rms_norm_eps
123+
)
124+
self.mlp = Eagle3MLP(config)
125+
self.norm_before_residual = config.norm_before_residual
126+
127+
def forward(
128+
self,
129+
input_embeds: torch.Tensor,
130+
feature: torch.Tensor,
131+
positions: torch.Tensor,
132+
) -> torch.Tensor:
133+
normed_embeds = self.input_layernorm(input_embeds)
134+
normed_feature = self.hidden_norm(feature)
135+
residual = normed_feature if self.norm_before_residual else feature
136+
x = torch.cat((normed_embeds, normed_feature), dim=-1)
137+
x = self.self_attn(x, positions)
138+
x = residual + x
139+
140+
residual = x
141+
x = self.post_attention_layernorm(x)
142+
x = self.mlp(x)
143+
return residual + x
144+
145+
146+
class Eagle3Draft(nn.Module):
147+
def __init__(self, config: Eagle3Config):
148+
super().__init__()
149+
self.config = config
150+
self.fc = nn.Linear(
151+
len(config.aux_hidden_state_layers) * config.target_hidden_size,
152+
config.hidden_size,
153+
bias=False,
154+
)
155+
self.midlayer = Eagle3Midlayer(config)
156+
self.norm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
157+
self.lm_head = nn.Linear(
158+
config.hidden_size, config.draft_vocab_size, bias=False
159+
)
160+
if config.has_own_embed:
161+
self.embed_tokens = nn.Embedding(
162+
config.target_vocab_size, config.hidden_size
163+
)
164+
# d2t/t2d are loaded from the checkpoint (assign=True adopts their
165+
# shapes/dtypes): d2t[draft_id] is the offset to the target vocab id;
166+
# t2d masks which target ids are in the draft vocab.
167+
self.register_buffer(
168+
"d2t",
169+
torch.zeros(config.draft_vocab_size, dtype=torch.long),
170+
persistent=False,
171+
)
172+
self.register_buffer("t2d", torch.zeros(1, dtype=torch.bool), persistent=False)
173+
174+
def fuse(self, aux: torch.Tensor) -> torch.Tensor:
175+
"""Fuse concatenated target aux hidden states (B,T,3*D) -> feature (B,T,D)."""
176+
return self.fc(aux)
177+
178+
def embed(self, ids: torch.Tensor) -> torch.Tensor:
179+
"""Embed token ids with the head's own table.
180+
181+
Only valid when the checkpoint shipped its own ``embed_tokens``; heads
182+
that reuse the target embedding must source embeddings from the target.
183+
"""
184+
if not self.config.has_own_embed:
185+
raise RuntimeError(
186+
"this draft head has no own embed_tokens (has_own_embed=False); "
187+
"provide token embeddings from the target model instead"
188+
)
189+
return self.embed_tokens(ids)
190+
191+
def forward(
192+
self,
193+
input_embeds: torch.Tensor,
194+
feature: torch.Tensor,
195+
positions: torch.Tensor,
196+
) -> tuple[torch.Tensor, torch.Tensor]:
197+
"""Run the midlayer over a sequence.
198+
199+
Returns (draft_logits, g):
200+
draft_logits: (B, T, draft_vocab_size) over the reduced vocab.
201+
g: (B, T, hidden) midlayer output — the recurrent feature.
202+
"""
203+
g = self.midlayer(input_embeds, feature, positions)
204+
draft_logits = self.lm_head(self.norm(g))
205+
return draft_logits, g
206+
207+
def draft_to_target(self, draft_ids: torch.Tensor) -> torch.Tensor:
208+
return draft_ids + self.d2t[draft_ids]
209+
210+
@staticmethod
211+
def from_checkpoint(
212+
model_dir: str, device: str = "cuda", dtype: torch.dtype = torch.bfloat16
213+
) -> tuple["Eagle3Draft", Eagle3Config]:
214+
import json
215+
216+
with open(os.path.join(model_dir, "config.json")) as f:
217+
cfg = json.load(f)
218+
219+
tlc = cfg["transformer_layer_config"]
220+
config = Eagle3Config(
221+
hidden_size=tlc["hidden_size"],
222+
target_hidden_size=cfg.get("target_hidden_size") or tlc["hidden_size"],
223+
intermediate_size=tlc["intermediate_size"],
224+
num_attention_heads=tlc["num_attention_heads"],
225+
num_key_value_heads=tlc["num_key_value_heads"],
226+
head_dim=tlc["head_dim"],
227+
rope_theta=tlc["rope_parameters"]["rope_theta"],
228+
rms_norm_eps=tlc["rms_norm_eps"],
229+
draft_vocab_size=cfg["draft_vocab_size"],
230+
target_vocab_size=tlc.get("vocab_size", 262144),
231+
aux_hidden_state_layers=cfg["eagle_aux_hidden_state_layer_ids"],
232+
norm_before_residual=cfg.get("norm_before_residual", False),
233+
norm_before_fc=cfg.get("norm_before_fc", False),
234+
)
235+
if config.norm_before_fc:
236+
# This checkpoint variant requires an input RMSNorm before fc.
237+
raise ValueError(
238+
"norm_before_fc=True checkpoints are not supported "
239+
"(would need an input RMSNorm before fc)"
240+
)
241+
242+
raw = _load_safetensors(model_dir)
243+
config.has_own_embed = "embed_tokens.weight" in raw
244+
245+
# Cast checkpoint weights after module construction so inv_freq stays fp32.
246+
model = Eagle3Draft(config)
247+
# The single decoder layer is stored as layers.0.* on disk.
248+
state_dict = {
249+
(k.replace("layers.0.", "midlayer.") if k.startswith("layers.0.") else k): (
250+
v.to(dtype) if v.is_floating_point() else v
251+
)
252+
for k, v in raw.items()
253+
}
254+
# d2t/t2d are index/mask tensors (their checkpoint shape differs from the
255+
# placeholder buffers); register them directly, load the rest strict.
256+
model.register_buffer("d2t", state_dict.pop("d2t"), persistent=False)
257+
model.register_buffer("t2d", state_dict.pop("t2d"), persistent=False)
258+
model.load_state_dict(state_dict, strict=True, assign=True)
259+
model = model.to(device)
260+
assert (
261+
model.midlayer.self_attn.inv_freq.dtype == torch.float32
262+
), "RoPE inv_freq must remain float32"
263+
return model.eval(), config
264+
265+
266+
def _load_safetensors(model_dir: str) -> dict:
267+
"""Load a monolithic or sharded safetensors checkpoint to CPU tensors."""
268+
import json
269+
270+
from safetensors import safe_open
271+
272+
index = os.path.join(model_dir, "model.safetensors.index.json")
273+
mono = os.path.join(model_dir, "model.safetensors")
274+
if os.path.exists(mono):
275+
shards = ["model.safetensors"]
276+
elif os.path.exists(index):
277+
with open(index) as f:
278+
shards = sorted(set(json.load(f)["weight_map"].values()))
279+
else:
280+
raise FileNotFoundError(
281+
f"no model.safetensors or model.safetensors.index.json in {model_dir}"
282+
)
283+
raw = {}
284+
for shard in shards:
285+
with safe_open(
286+
os.path.join(model_dir, shard), framework="pt", device="cpu"
287+
) as f:
288+
for k in f.keys():
289+
if k in raw:
290+
raise ValueError(f"duplicate tensor {k!r} across shards ({shard})")
291+
raw[k] = f.get_tensor(k)
292+
return raw

0 commit comments

Comments
 (0)