Skip to content

Commit 57ef8a5

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

3 files changed

Lines changed: 240 additions & 7 deletions

File tree

examples/models/gemma4_31b/model.py

Lines changed: 98 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,26 @@ def update(
102102
# Config
103103

104104

105+
def validate_eagle_tap_layers(layers: list, num_hidden_layers: int) -> None:
106+
"""Validate EAGLE-3 tap indices (HF/vLLM convention; 0 = embedding output).
107+
108+
Indices must be non-bool ints, unique, ascending, and in
109+
``[0, num_hidden_layers]``. Order defines the fc concatenation order.
110+
"""
111+
if not layers:
112+
return
113+
if any(isinstance(t, bool) or not isinstance(t, int) for t in layers):
114+
raise ValueError(f"eagle_tap_layers must be non-bool ints, got {layers}")
115+
if len(set(layers)) != len(layers):
116+
raise ValueError(f"eagle_tap_layers has duplicates: {layers}")
117+
if any(t < 0 or t > num_hidden_layers for t in layers):
118+
raise ValueError(
119+
f"eagle_tap_layers {layers} out of range [0, {num_hidden_layers}]"
120+
)
121+
if list(layers) != sorted(layers):
122+
raise ValueError(f"eagle_tap_layers must be ascending (fc order): {layers}")
123+
124+
105125
@dataclass
106126
class Gemma4_31BConfig:
107127
# Embedding / shape
@@ -144,6 +164,11 @@ class Gemma4_31BConfig:
144164
# Runtime
145165
max_seq_len: int = 4096
146166

167+
# EAGLE-3 auxiliary hidden-state taps. Indices use the HF/vLLM convention:
168+
# 0 = embedding output, k = output after decoder layer k-1. Empty disables
169+
# tap collection.
170+
eagle_tap_layers: list = field(default_factory=list)
171+
147172
def __post_init__(self):
148173
if not self.layer_types:
149174
# Default hybrid pattern: 5 sliding then 1 full, repeated.
@@ -156,6 +181,7 @@ def __post_init__(self):
156181
f"layer_types length {len(self.layer_types)} != "
157182
f"num_hidden_layers {self.num_hidden_layers}"
158183
)
184+
validate_eagle_tap_layers(self.eagle_tap_layers, self.num_hidden_layers)
159185

160186
@staticmethod
161187
def from_hf_config(config_path: str) -> "Gemma4_31BConfig":
@@ -466,6 +492,48 @@ def _build_masks(
466492

467493
return sliding_mask, full_mask
468494

495+
def _decode(
496+
self,
497+
tokens: torch.LongTensor,
498+
input_pos: torch.LongTensor,
499+
collect_taps: bool,
500+
) -> tuple[torch.Tensor, Optional[torch.Tensor]]:
501+
"""Embed -> decoder layers -> final norm.
502+
503+
Returns the normed hidden states and, when ``collect_taps`` is set, the
504+
concatenated tap features for ``config.eagle_tap_layers`` (in ascending
505+
index order) as ``(B, T, len(tap_layers) * hidden_size)``; else None.
506+
507+
Tap indices follow the HF/vLLM hidden-state convention: index 0 is the
508+
embedding output (before any decoder layer) and index k is the output
509+
*after* decoder layer k-1.
510+
"""
511+
x = self.embed_tokens(tokens) * self.embed_normalizer
512+
513+
tap_layers = self.config.eagle_tap_layers
514+
if collect_taps:
515+
# Revalidate dynamic tap configuration before membership checks.
516+
validate_eagle_tap_layers(tap_layers, len(self.layers))
517+
taps = []
518+
if collect_taps and 0 in tap_layers:
519+
taps.append(x) # index 0 == embedding output
520+
521+
sliding_mask, full_mask = self._build_masks(input_pos)
522+
for i, layer in enumerate(self.layers):
523+
x = layer(x, input_pos, sliding_mask, full_mask)
524+
if collect_taps and (i + 1) in tap_layers:
525+
taps.append(x) # output of layer i == hidden-state index i+1
526+
527+
if collect_taps and len(taps) != len(tap_layers):
528+
raise ValueError(
529+
f"collected {len(taps)} taps but eagle_tap_layers requests "
530+
f"{len(tap_layers)} ({tap_layers}); check the index convention"
531+
)
532+
533+
x = self.norm(x)
534+
taps_out = torch.cat(taps, dim=-1) if taps else None
535+
return x, taps_out
536+
469537
def forward(
470538
self,
471539
tokens: torch.LongTensor,
@@ -482,18 +550,41 @@ def forward(
482550
Returns:
483551
(B, 1) sampled token IDs as float.
484552
"""
485-
x = self.embed_tokens(tokens) * self.embed_normalizer
486-
487-
sliding_mask, full_mask = self._build_masks(input_pos)
488-
for layer in self.layers:
489-
x = layer(x, input_pos, sliding_mask, full_mask)
490-
491-
x = self.norm(x)
553+
x, _ = self._decode(tokens, input_pos, collect_taps=False)
492554
last = self.lm_head(x[:, -1, :]).float()
493555
cap = self.logit_softcap.float()
494556
last = torch.tanh(last / cap) * cap
495557
return sample(last, temperature)
496558

559+
def set_eagle_tap_layers(self, layers: list) -> None:
560+
"""Set and validate EAGLE-3 tap layers."""
561+
validate_eagle_tap_layers(layers, self.config.num_hidden_layers)
562+
self.config.eagle_tap_layers = list(layers)
563+
564+
def forward_logits_taps(
565+
self,
566+
tokens: torch.LongTensor,
567+
input_pos: torch.LongTensor,
568+
last_logits_only: bool = True,
569+
) -> tuple[torch.Tensor, Optional[torch.Tensor]]:
570+
"""Return soft-capped logits and EAGLE-3 tap features.
571+
572+
Defaults to final-position logits. Set ``last_logits_only=False`` to
573+
materialize per-position float32 logits over the full vocabulary.
574+
575+
Returns:
576+
logits: (B, 1, vocab_size) soft-capped float32, or (B, T, vocab_size)
577+
when ``last_logits_only=False``.
578+
taps: (B, T, len(eagle_tap_layers) * hidden_size) or None.
579+
"""
580+
x, taps = self._decode(tokens, input_pos, collect_taps=True)
581+
if last_logits_only:
582+
x = x[:, -1:, :]
583+
logits = self.lm_head(x).float()
584+
cap = self.logit_softcap.float()
585+
logits = torch.tanh(logits / cap) * cap
586+
return logits, taps
587+
497588
# ---------------- checkpoint loading ----------------
498589

499590
@staticmethod
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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+
"""Unit tests for the gemma4-31B EAGLE-3 hidden-state tap.
8+
9+
Covers the tap-index convention (HF/vLLM: index 0 = embedding, index k = output
10+
after decoder layer k-1), exact concatenation order/content, config validation
11+
(including the runtime-mutation path), and that the default decode path is
12+
unaffected by enabling the tap.
13+
"""
14+
15+
import pytest
16+
import torch
17+
18+
from executorch.examples.models.gemma4_31b.model import Gemma4_31B, Gemma4_31BConfig
19+
20+
21+
def tiny_config(num_layers=6, tap_layers=None) -> Gemma4_31BConfig:
22+
return Gemma4_31BConfig(
23+
vocab_size=128,
24+
hidden_size=32,
25+
intermediate_size=64,
26+
num_hidden_layers=num_layers,
27+
num_attention_heads=4,
28+
num_key_value_heads=2,
29+
head_dim=8,
30+
num_global_key_value_heads=1,
31+
global_head_dim=8,
32+
sliding_window=8,
33+
max_seq_len=32,
34+
eagle_tap_layers=tap_layers or [],
35+
)
36+
37+
38+
def build(num_layers=6, tap_layers=None):
39+
torch.manual_seed(0)
40+
return Gemma4_31B(tiny_config(num_layers, tap_layers)).to(torch.float32).eval()
41+
42+
43+
def reset_kv(model):
44+
"""Zero the (stateful) KV caches so independent forwards don't couple."""
45+
for name, buf in model.named_buffers():
46+
if ".kv_cache." in name:
47+
buf.zero_()
48+
49+
50+
def reference_states(model, tokens, input_pos):
51+
"""Recompute _decode's per-index states: 0=embedding, k=after layer k-1."""
52+
x = model.embed_tokens(tokens) * model.embed_normalizer
53+
states = {0: x}
54+
sliding_mask, full_mask = model._build_masks(input_pos)
55+
for i, layer in enumerate(model.layers):
56+
x = layer(x, input_pos, sliding_mask, full_mask)
57+
states[i + 1] = x
58+
return states
59+
60+
61+
def test_tap_off_does_not_change_logits():
62+
model = build(tap_layers=[1, 2, 3])
63+
T = 7
64+
tokens = torch.randint(0, 128, (1, T))
65+
pos = torch.arange(T)
66+
with torch.no_grad():
67+
reset_kv(model)
68+
logits_on, taps_on = model.forward_logits_taps(
69+
tokens, pos, last_logits_only=False
70+
)
71+
model.config.eagle_tap_layers = []
72+
reset_kv(model)
73+
logits_off, taps_off = model.forward_logits_taps(
74+
tokens, pos, last_logits_only=False
75+
)
76+
assert taps_off is None
77+
assert taps_on.shape == (1, T, 3 * model.config.hidden_size)
78+
torch.testing.assert_close(logits_on, logits_off)
79+
80+
81+
@pytest.mark.parametrize(
82+
"num_layers,tap_layers",
83+
[
84+
(6, [0, 1, 3]),
85+
(60, [2, 30, 57]),
86+
],
87+
)
88+
def test_tap_collects_exact_states_in_order(num_layers, tap_layers):
89+
model = build(num_layers=num_layers, tap_layers=tap_layers)
90+
T = 5
91+
tokens = torch.randint(0, 128, (1, T))
92+
pos = torch.arange(T)
93+
with torch.no_grad():
94+
reset_kv(model)
95+
_, taps = model.forward_logits_taps(tokens, pos)
96+
reset_kv(model)
97+
states = reference_states(model, tokens, pos)
98+
expected = torch.cat([states[i] for i in tap_layers], dim=-1)
99+
assert taps.shape == (1, T, len(tap_layers) * model.config.hidden_size)
100+
torch.testing.assert_close(taps, expected, rtol=0, atol=0)
101+
102+
103+
def test_last_logits_only_default_matches_full():
104+
model = build(tap_layers=[1])
105+
T = 4
106+
tokens = torch.randint(0, 128, (1, T))
107+
pos = torch.arange(T)
108+
with torch.no_grad():
109+
reset_kv(model)
110+
full, _ = model.forward_logits_taps(tokens, pos, last_logits_only=False)
111+
reset_kv(model)
112+
last, _ = model.forward_logits_taps(tokens, pos)
113+
assert last.shape == (1, 1, model.config.vocab_size)
114+
torch.testing.assert_close(last[:, 0], full[:, -1])
115+
116+
117+
@pytest.mark.parametrize("bad", [[99], [1, 1], [1.0, 2], [True], [3, 1]])
118+
def test_invalid_tap_config_rejected(bad):
119+
with pytest.raises(ValueError):
120+
tiny_config(num_layers=6, tap_layers=bad)
121+
122+
123+
def test_set_eagle_tap_layers_validates():
124+
model = build()
125+
model.set_eagle_tap_layers([0, 2, 4])
126+
assert model.config.eagle_tap_layers == [0, 2, 4]
127+
with pytest.raises(ValueError):
128+
model.set_eagle_tap_layers([4, 2])
129+
130+
131+
def test_runtime_mutation_is_revalidated_in_decode():
132+
model = build(tap_layers=[1, 2])
133+
model.config.eagle_tap_layers = [True]
134+
tokens = torch.randint(0, 128, (1, 4))
135+
pos = torch.arange(4)
136+
with pytest.raises(ValueError):
137+
model.forward_logits_taps(tokens, pos, last_logits_only=False)
138+
139+
140+
if __name__ == "__main__":
141+
raise SystemExit(pytest.main([__file__, "-q"]))

pytest.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ testpaths =
9797
examples/models/llama3_2_vision/text_decoder/test
9898
examples/models/llava/test
9999
examples/models/eagle3/test_draft.py
100+
examples/models/gemma4_31b/test_eagle_tap.py
100101

101102
# exir
102103
exir/

0 commit comments

Comments
 (0)