Skip to content

Commit 393ab02

Browse files
authored
fix(gemma): resolve Gemma 3 defaults a sparse config omits, and qualify gemma-3-12b (1362)
## Implementation Extends `_GEMMA3_CONFIG_DEFAULTS` with `rope_theta`, `rope_local_base_freq`, `sliding_window_pattern` and `query_pre_attn_scalar`, taken from `Gemma3TextConfig`'s own `rope_parameters` rather than from the fields that happened to fail before. The three that `graph_blocks` reads back through `_gemma_raw` are written into `config.raw`, so a nested `text_config` still wins. `gemma3_attention_schedule` now raises for a Gemma 3 `model_type` that arrives without both `sliding_window` and `rope_local_base_freq`, instead of falling back to all-global. Gemma and Gemma 2 keep the existing fallback.
1 parent 4b9cc2b commit 393ab02

6 files changed

Lines changed: 218 additions & 1 deletion

File tree

apps/benchmark/performance/release.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ excluded_profiles:
9090
gemma-2-2b, which exercises the same builder and runtime path.
9191
- model: gemma-3-270m
9292
reason: *gemma3_performance_exclusion
93+
- model: gemma-3-12b
94+
reason: *gemma3_performance_exclusion
9395
- model: gemma-3-1b
9496
reason: >-
9597
Functional and Hugging Face reference-parity qualification is present,

families/gemma/graph_blocks.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ def _gemma_raw(config) -> dict:
374374
return raw
375375

376376

377+
_GEMMA3_MODEL_TYPES = frozenset({"gemma3", "gemma3_text"})
377378
_GEMMA3_SLIDING_PATTERN = 6
378379

379380

@@ -395,6 +396,20 @@ def gemma3_attention_schedule(config, num_layers: int) -> dict:
395396
# A second rope base is what distinguishes Gemma 3 from Gemma 2 here.
396397
# Gemma 2 also interleaves windows but rotates every layer on one base, so
397398
# without this key the caller must keep building a single-base graph.
399+
#
400+
# For Gemma 3 that fallback is never right: it silently rebuilds all layers
401+
# as global attention on one base, which builds cleanly and generates
402+
# fluent, wrong text. google/gemma-3-12b-it states neither key and relies on
403+
# the transformers defaults, so refuse rather than guess - the model layer
404+
# is responsible for filling these in before the graph is built.
405+
if str(getattr(config, "model_type", "")).lower() in _GEMMA3_MODEL_TYPES and not (
406+
window and local_base
407+
):
408+
raise ValueError(
409+
"Gemma 3 needs both sliding_window and rope_local_base_freq to place "
410+
f"local attention; got sliding_window={window!r} "
411+
f"rope_local_base_freq={local_base!r}"
412+
)
398413
if not window or not local_base:
399414
return {
400415
"is_local": [False] * num_layers,

families/gemma/model.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,32 @@ def _decoder_prefix(readers) -> str:
5757
# mirror of the same weights states both, which is why local runs passed and
5858
# internal CI did not. rms_norm_eps is listed because this family would
5959
# otherwise fall back to 1e-5 where Gemma 3 uses 1e-6 - wrong, and silently so.
60+
# What Gemma3TextConfig fills in when the checkpoint stays silent. Google's own
61+
# configs state geometry and nothing else, so every value here is load-bearing;
62+
# the mirrors that write them out explicitly are why this went unnoticed.
63+
#
64+
# `rope_theta` and `rope_local_base_freq` come from that class's rope_parameters:
65+
# sliding_attention -> {"rope_type": "default", "rope_theta": 10000.0}
66+
# full_attention -> {"rope_type": "linear", "rope_theta": 1000000.0}
6067
_GEMMA3_CONFIG_DEFAULTS = {
6168
"hidden_activation": "gelu_pytorch_tanh",
6269
"rms_norm_eps": 1e-6,
6370
"max_position_embeddings": 131072,
6471
"head_dim": 256,
72+
"query_pre_attn_scalar": 256,
73+
"rope_theta": 1000000.0,
74+
"rope_local_base_freq": 10000.0,
75+
"sliding_window_pattern": 6,
6576
}
6677

78+
# Defaults that belong in config.raw rather than on the config object, because
79+
# graph_blocks reads them back through _gemma_raw.
80+
_GEMMA3_RAW_DEFAULTS = (
81+
"query_pre_attn_scalar",
82+
"rope_local_base_freq",
83+
"sliding_window_pattern",
84+
)
85+
6786

6887
def _apply_gemma3_config_defaults(config: ModelConfig, readers, model_prefix: str) -> None:
6988
"""Fill in what a Gemma 3 config may legitimately omit.
@@ -81,6 +100,17 @@ def _apply_gemma3_config_defaults(config: ModelConfig, readers, model_prefix: st
81100
config.rms_norm_eps = _GEMMA3_CONFIG_DEFAULTS["rms_norm_eps"]
82101
if raw.get("max_position_embeddings") is None:
83102
config.max_position_embeddings = _GEMMA3_CONFIG_DEFAULTS["max_position_embeddings"]
103+
if raw.get("rope_theta") is None:
104+
# Gemma 3's global layers use 1e6. The parser's own fallback is 1e4,
105+
# which is the *local* base, so staying silent here rebuilds every
106+
# global layer on the wrong rope table.
107+
config.rope_theta = _GEMMA3_CONFIG_DEFAULTS["rope_theta"]
108+
for key in _GEMMA3_RAW_DEFAULTS:
109+
# Written into raw, not onto the config: gemma3_attention_schedule and
110+
# gemma_attention_scale read these back through _gemma_raw. A nested
111+
# text_config still wins, because _gemma_raw overlays it last.
112+
if raw.get(key) is None:
113+
config.raw[key] = _GEMMA3_CONFIG_DEFAULTS[key]
84114
if raw.get("head_dim") is None:
85115
# head_dim is a derived property; _head_dim is the stated override it
86116
# reads first. Without it the property falls back to
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "gemma-3-12b",
3+
"hf_id": "google/gemma-3-12b-it",
4+
"bundle": "gemma-3-12b.bundle",
5+
"family": "gemma",
6+
"task": "text_generation",
7+
"precision": "bf16",
8+
"trust_remote_code": false,
9+
"testcases": [
10+
{
11+
"name": "gemma-3-12b",
12+
"premerge": true,
13+
"reference_precision": "fp32",
14+
"prompt": "What is the capital of France? Answer with just the name.",
15+
"max_new_tokens": 8,
16+
"use_chat_template": true,
17+
"enable_thinking": false
18+
}
19+
],
20+
"max_sequence_length": 256,
21+
"tensor_parallel_size": 1
22+
}

families/gemma/tests/test_gemma3_schedule.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,24 @@ def test_a_checkpoint_without_a_second_rope_base_stays_global() -> None:
8181
8282
It declares sliding_window but no rope_local_base_freq, so no second rope
8383
table may be built for it.
84+
85+
This case previously passed a `gemma3_text` config, which made it assert
86+
that a Gemma 3 checkpoint missing its local rope base quietly becomes an
87+
all-global model. That is the shape of google/gemma-3-12b-it, and the
88+
resulting engine built cleanly and generated fluent, wrong text. The
89+
model_type below is the one the docstring always described.
8490
"""
85-
schedule = gemma3_attention_schedule(_config(sliding_window=4096), 26)
91+
schedule = gemma3_attention_schedule(_config(model_type="gemma2", sliding_window=4096), 26)
8692
assert schedule["is_local"] == [False] * 26
8793
assert schedule["window"] is None
8894

8995

96+
def test_gemma3_without_a_second_rope_base_is_refused() -> None:
97+
"""The same inputs under a Gemma 3 model_type must not be guessed at."""
98+
with pytest.raises(ValueError, match="rope_local_base_freq"):
99+
gemma3_attention_schedule(_config(sliding_window=4096), 26)
100+
101+
90102
def test_an_absent_pattern_falls_back_to_the_gemma3_default() -> None:
91103
"""google/gemma-3-270m-it omits sliding_window_pattern.
92104
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Gemma 3 checkpoints that state only geometry must still build correctly.
5+
6+
google/gemma-3-12b-it names hidden_size, the layer and head counts,
7+
intermediate_size, sliding_window and rope_scaling, and nothing else; every
8+
other value comes from Gemma3TextConfig. Mirrors write the full set out, which
9+
is why a build against a mirror can pass while the same build against the
10+
official checkpoint silently produces a different model.
11+
"""
12+
13+
from __future__ import annotations
14+
15+
import pytest
16+
17+
from families.gemma import graph_blocks
18+
from families.gemma.model import _GEMMA3_CONFIG_DEFAULTS, _apply_gemma3_config_defaults
19+
20+
21+
class _Config:
22+
"""Minimal stand-in for the parsed config, matching the sparse official shape."""
23+
24+
def __init__(self, **raw):
25+
self.model_type = raw.pop("model_type", "gemma3")
26+
self.hidden_size = raw.get("hidden_size", 3840)
27+
self.num_hidden_layers = raw.get("num_hidden_layers", 48)
28+
self.num_attention_heads = raw.get("num_attention_heads", 16)
29+
self.num_key_value_heads = raw.get("num_key_value_heads", 8)
30+
# The parser's own fallback, which is the local base rather than the
31+
# global one; the defaults must overwrite it.
32+
self.rope_theta = 10000.0
33+
self.rms_norm_eps = None
34+
self.hidden_act = None
35+
self.max_position_embeddings = None
36+
self._head_dim = None
37+
self.raw = dict(raw)
38+
39+
@property
40+
def head_dim(self):
41+
if self._head_dim:
42+
return self._head_dim
43+
return self.hidden_size // self.num_attention_heads
44+
45+
46+
class _Readers:
47+
tensor_map: dict = {}
48+
49+
50+
def _official_12b() -> _Config:
51+
return _Config(
52+
hidden_size=3840,
53+
intermediate_size=15360,
54+
num_attention_heads=16,
55+
num_hidden_layers=48,
56+
num_key_value_heads=8,
57+
sliding_window=1024,
58+
rope_scaling={"factor": 8.0, "rope_type": "linear"},
59+
)
60+
61+
62+
def test_sparse_config_resolves_the_gemma3_rope_bases():
63+
config = _official_12b()
64+
_apply_gemma3_config_defaults(config, _Readers(), "model")
65+
66+
assert config.head_dim == 256
67+
assert config.rope_theta == 1000000.0, "global layers must not inherit the local base"
68+
schedule = graph_blocks.gemma3_attention_schedule(config, config.num_hidden_layers)
69+
assert schedule["window"] == 1024
70+
assert schedule["local_theta"] == 10000.0
71+
72+
73+
def test_sparse_config_places_local_attention_five_in_six():
74+
config = _official_12b()
75+
_apply_gemma3_config_defaults(config, _Readers(), "model")
76+
schedule = graph_blocks.gemma3_attention_schedule(config, config.num_hidden_layers)
77+
78+
# transformers resolves layer_types for this config to full attention at
79+
# indices 5, 11, 17, 23, 29, 35, 41 and 47.
80+
assert sum(schedule["is_local"]) == 40
81+
assert [i for i, local in enumerate(schedule["is_local"]) if not local] == [
82+
5,
83+
11,
84+
17,
85+
23,
86+
29,
87+
35,
88+
41,
89+
47,
90+
]
91+
92+
93+
def test_gemma3_refuses_to_fall_back_to_all_global():
94+
"""The old behavior built cleanly and generated fluent, wrong text."""
95+
config = _official_12b()
96+
# Defaults deliberately not applied, so the schedule inputs are missing.
97+
with pytest.raises(ValueError, match="rope_local_base_freq"):
98+
graph_blocks.gemma3_attention_schedule(config, config.num_hidden_layers)
99+
100+
101+
def test_a_stated_value_still_wins_over_the_default():
102+
config = _official_12b()
103+
config.raw["rope_local_base_freq"] = 12345.0
104+
config.raw["sliding_window_pattern"] = 4
105+
_apply_gemma3_config_defaults(config, _Readers(), "model")
106+
schedule = graph_blocks.gemma3_attention_schedule(config, config.num_hidden_layers)
107+
108+
assert schedule["local_theta"] == 12345.0
109+
assert [i for i, local in enumerate(schedule["is_local"]) if not local][:3] == [3, 7, 11]
110+
111+
112+
def test_gemma2_is_untouched_by_the_gemma3_defaults():
113+
config = _Config(
114+
model_type="gemma2",
115+
hidden_size=2304,
116+
num_attention_heads=8,
117+
num_hidden_layers=26,
118+
sliding_window=4096,
119+
)
120+
_apply_gemma3_config_defaults(config, _Readers(), "model")
121+
122+
assert config.rope_theta == 10000.0
123+
assert "rope_local_base_freq" not in config.raw
124+
schedule = graph_blocks.gemma3_attention_schedule(config, config.num_hidden_layers)
125+
assert not any(schedule["is_local"])
126+
127+
128+
def test_defaults_cover_every_field_the_schedule_and_scale_read():
129+
for key in (
130+
"rope_theta",
131+
"rope_local_base_freq",
132+
"sliding_window_pattern",
133+
"query_pre_attn_scalar",
134+
"head_dim",
135+
):
136+
assert key in _GEMMA3_CONFIG_DEFAULTS

0 commit comments

Comments
 (0)