Skip to content

Commit 1fae4e9

Browse files
Qualcomm AI Engine Direct - Gemma4 Text model enablement (pytorch#20979)
### Summary Gemma4 Text model enablement ### Test plan ``` bash python examples/qualcomm/oss_scripts/llama/llama.py --build_folder build-android --device ${SERIAL_NUM} --soc_model ${SOC_MODEL} --temperature 0 --model_mode kv--max_seq_len 1024 --prefill_ar_len 128 --decoder_model gemma4-e2b --prompt "I would like to learn python, could you teach me with a simple example?" --calib_tasks wikitext --calib_limit 1 --embedding-quantize '4,32' ```
1 parent 6ca9ae3 commit 1fae4e9

28 files changed

Lines changed: 1458 additions & 174 deletions

‎backends/qualcomm/tests/test_qnn_delegate.py‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8328,6 +8328,14 @@ def setUp(self):
83288328
hellaswag_acc_norm=None,
83298329
sqnr=10,
83308330
),
8331+
"gemma4-e2b": TestExampleLLMScript.LlmSpecs(
8332+
SM8650=20,
8333+
SM8750=30,
8334+
pte_size=4_500_000_000, # 4.5 GB
8335+
wikitext_ppl=120,
8336+
hellaswag_acc_norm=None,
8337+
sqnr=10,
8338+
),
83318339
"glm-1_5b": TestExampleLLMScript.LlmSpecs(
83328340
SM8650=42,
83338341
SM8750=52,
@@ -8406,7 +8414,7 @@ def setUp(self):
84068414
pte_size=210_000_000, # 210 MB
84078415
wikitext_ppl=23,
84088416
hellaswag_acc_norm=None,
8409-
sqnr=19.5,
8417+
sqnr=19,
84108418
),
84118419
"smollm3-3b": TestExampleLLMScript.LlmSpecs(
84128420
SM8650=23,
@@ -8529,6 +8537,14 @@ def test_static_llm_model(self): # noqa: C901
85298537
]
85308538
)
85318539

8540+
if self.model_name == "gemma4-e2b":
8541+
cmds.extend(
8542+
[
8543+
"--embedding-quantize",
8544+
"4,32",
8545+
]
8546+
)
8547+
85328548
self.add_default_cmds(cmds)
85338549

85348550
p = subprocess.Popen(cmds, stdout=subprocess.DEVNULL)

‎examples/models/llama/evaluate/eager_eval.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ def eot_token_id(self):
5050

5151
@property
5252
def prefix_token_id(self):
53+
if hasattr(self._tokenizer, "bos_id"):
54+
return self._tokenizer.bos_id
5355
return self.eot_token_id
5456

5557
@property
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
# Copyright (c) Qualcomm Innovation Center, Inc.
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+
from typing import List, Optional, Tuple
8+
9+
import torch
10+
11+
from executorch.examples.models.gemma4.text_decoder.gemma4_config import Gemma4Config
12+
from executorch.examples.qualcomm.oss_scripts.gemma4.text_decoder.text_model import (
13+
Gemma4TextModel,
14+
)
15+
from executorch.examples.qualcomm.oss_scripts.llama.masking_utils import (
16+
AttentionMask,
17+
CausalAttentionMask,
18+
SlidingWindowAttentionMask,
19+
)
20+
from torch import nn
21+
22+
23+
def _precompute_freqs(
24+
head_dim: int,
25+
max_context_len: int,
26+
rope_theta: float,
27+
partial_rotary_factor: float,
28+
) -> Tuple[torch.Tensor, torch.Tensor]:
29+
"""HuggingFace-style RoPE cos/sin with zero-padding for partial RoPE.
30+
31+
Returns (freqs_cos, freqs_sin) each of shape [max_context_len, head_dim // 2].
32+
Non-rotated dims are zero-padded so ROTARY_EMB_REGISTRY["partial"] handles
33+
the half-and-half split correctly.
34+
"""
35+
rotary_dim = int(head_dim * partial_rotary_factor)
36+
half_rot = rotary_dim // 2
37+
inv_freq_rot = 1.0 / (
38+
rope_theta ** (torch.arange(0, rotary_dim, 2).float() / head_dim)
39+
)
40+
total_half = head_dim // 2
41+
nope = total_half - half_rot
42+
inv_freq = (
43+
torch.cat([inv_freq_rot, torch.zeros(nope)]) if nope > 0 else inv_freq_rot
44+
)
45+
t = torch.arange(max_context_len, dtype=torch.float32)
46+
freqs = torch.outer(t, inv_freq)
47+
return torch.cos(freqs), torch.sin(freqs)
48+
49+
50+
class Gemma4TextModelWrapper(nn.Module):
51+
"""QNN-export-ready Gemma 4 text decoder (static_llama.py convention).
52+
53+
KV-cache I/O covers only the 15 self-decoder layers. Each of these layers
54+
has its own head_dim (full-attention=512, sliding=256), so cache shapes
55+
are heterogeneous.
56+
57+
forward(tokens, atten_mask, window_atten_mask, input_pos,
58+
*k_caches[0..14], *v_caches[0..14])
59+
-> (logits, out_k_caches, out_v_caches)
60+
"""
61+
62+
def __init__(
63+
self,
64+
config: Gemma4Config,
65+
ar_len: int = 1,
66+
output_new_cache_only: bool = True,
67+
output_cache: bool = True,
68+
use_i64_token: bool = False,
69+
**kwargs,
70+
):
71+
super().__init__()
72+
self.config = config
73+
self.ar_len = ar_len
74+
self.use_kv_cache = config.use_kv_cache
75+
self.output_cache = output_cache
76+
self.use_i64_token = use_i64_token
77+
self.n_self_layers = config.num_self_decoder_layers
78+
self.max_batch_size = config.max_batch_size
79+
self.vocab_size = config.vocab_size
80+
self.kv_io_bit_width = kwargs.get("kv_io_bit_width") or 32
81+
82+
self.model = Gemma4TextModel(
83+
config,
84+
output_new_cache_only,
85+
kwargs.get("enable_masked_softmax", False),
86+
)
87+
88+
# Precompute RoPE buffers.
89+
# Global: full-attention layers, partial_rotary_factor=0.25, head_dim=512.
90+
# Local: sliding-attention layers, full rotation, head_dim=256.
91+
global_cos, global_sin = _precompute_freqs(
92+
config.global_head_dim,
93+
config.max_context_len,
94+
config.rope_theta,
95+
config.partial_rotary_factor,
96+
)
97+
local_cos, local_sin = _precompute_freqs(
98+
config.head_dim,
99+
config.max_context_len,
100+
config.rope_local_base_freq,
101+
1.0,
102+
)
103+
self.register_buffer("global_freqs_cos", global_cos, persistent=False)
104+
self.register_buffer("global_freqs_sin", global_sin, persistent=False)
105+
self.register_buffer("local_freqs_cos", local_cos, persistent=False)
106+
self.register_buffer("local_freqs_sin", local_sin, persistent=False)
107+
108+
@property
109+
def layers(self) -> List[nn.Module]:
110+
"""Flat view of all 35 decoder layers (self + cross).
111+
112+
Satisfies callers that expect decoder.layers to iterate all layers,
113+
matching the LlamaModel convention. The actual nn.ModuleLists live in
114+
self.model.self_decoder.layers / self.model.cross_decoder.layers
115+
to preserve weight-path naming for checkpoint loading.
116+
"""
117+
return list(self.model.self_decoder.layers) + list(
118+
self.model.cross_decoder.layers
119+
)
120+
121+
def forward(
122+
self,
123+
tokens: torch.Tensor,
124+
atten_mask: torch.Tensor,
125+
window_atten_mask: torch.Tensor,
126+
input_pos: Optional[torch.Tensor] = None,
127+
*args,
128+
) -> Tuple[
129+
torch.Tensor, List[Optional[torch.Tensor]], List[Optional[torch.Tensor]]
130+
]:
131+
if self.use_kv_cache and input_pos is not None:
132+
global_cos = self.global_freqs_cos[input_pos][0]
133+
global_sin = self.global_freqs_sin[input_pos][0]
134+
local_cos = self.local_freqs_cos[input_pos][0]
135+
local_sin = self.local_freqs_sin[input_pos][0]
136+
else:
137+
global_cos = self.global_freqs_cos
138+
global_sin = self.global_freqs_sin
139+
local_cos = self.local_freqs_cos
140+
local_sin = self.local_freqs_sin
141+
142+
k_caches = list(args[: self.n_self_layers]) if self.use_kv_cache else []
143+
v_caches = (
144+
list(args[self.n_self_layers : 2 * self.n_self_layers])
145+
if self.use_kv_cache
146+
else []
147+
)
148+
149+
logits, output_k_cache, output_v_cache = self.model(
150+
tokens,
151+
atten_mask,
152+
window_atten_mask,
153+
input_pos,
154+
global_cos,
155+
global_sin,
156+
local_cos,
157+
local_sin,
158+
k_caches,
159+
v_caches,
160+
)
161+
162+
if self.output_cache:
163+
return logits, output_k_cache, output_v_cache
164+
return logits
165+
166+
def get_example_inputs(self):
167+
dtype = torch.int64 if self.use_i64_token else torch.int32
168+
tokens = torch.randint(
169+
self.config.vocab_size,
170+
(self.config.max_batch_size, self.ar_len),
171+
dtype=dtype,
172+
)
173+
causal_mask = CausalAttentionMask(
174+
self.config.max_batch_size, self.ar_len, self.config.max_context_len
175+
)
176+
sliding_mask = SlidingWindowAttentionMask(
177+
self.config.max_batch_size,
178+
self.ar_len,
179+
self.config.max_context_len,
180+
sliding_window=self.config.sliding_window,
181+
)
182+
attn_mask = AttentionMask([causal_mask, sliding_mask])
183+
184+
if self.use_kv_cache:
185+
pos_ids = torch.zeros(
186+
(self.config.max_batch_size, self.ar_len), dtype=torch.int32
187+
)
188+
k_caches, v_caches = [], []
189+
for i in range(self.n_self_layers):
190+
hd = self.config.get_head_dim(i)
191+
k_caches.append(
192+
torch.zeros(
193+
self.config.max_batch_size,
194+
self.config.num_key_value_heads,
195+
hd,
196+
self.config.max_context_len - self.ar_len,
197+
)
198+
)
199+
v_caches.append(
200+
torch.zeros(
201+
self.config.max_batch_size,
202+
self.config.num_key_value_heads,
203+
self.config.max_context_len - self.ar_len,
204+
hd,
205+
)
206+
)
207+
return (tokens, attn_mask, pos_ids, k_caches, v_caches)
208+
209+
return (tokens, attn_mask)
210+
211+
def get_metadata(self):
212+
return {
213+
"get_ar_len": self.ar_len,
214+
"get_bos_id": 2,
215+
"get_eos_id": 1,
216+
"get_dim": self.config.hidden_size,
217+
"get_head_dim": self.config.head_dim,
218+
"get_global_head_dim": self.config.global_head_dim,
219+
"get_max_batch_size": self.config.max_batch_size,
220+
"get_max_seq_len": self.config.max_seq_len,
221+
"get_max_context_len": self.config.max_context_len,
222+
"get_n_bos": 1,
223+
"get_n_eos": 1,
224+
"get_n_kv_heads": self.config.num_key_value_heads,
225+
"get_n_layers": self.config.num_hidden_layers,
226+
"get_n_self_layers": self.n_self_layers,
227+
"get_vocab_size": self.config.vocab_size,
228+
"get_use_kv_cache": self.use_kv_cache,
229+
"get_sliding_window": self.config.sliding_window,
230+
"get_num_kv_shared_layers": self.config.num_kv_shared_layers,
231+
"get_kv_io_bit_width": self.kv_io_bit_width,
232+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Copyright (c) Qualcomm Innovation Center, Inc.
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+
from .text_model import Gemma4TextModel # noqa: F401
8+
9+
__all__ = ["Gemma4TextModel"]

0 commit comments

Comments
 (0)