Skip to content

Commit c4b9418

Browse files
committed
[Gemma4] Enable use_qk_norm in Gemma 4 configs and fix attention weight init depth_scaling
# Description This change resolves an issue with Gemma 4 attention weight initialization and QK normalization: 1. In `src/maxtext/layers/attentions.py`, `init_query_w` now checks `self.use_qk_norm` in addition to `self.config.use_qk_norm`. When `use_qk_norm` is enabled (or `query_pre_attn_scalar != 1.0`), `depth_scaling` is set to 1.0 instead of `sqrt(head_dim)`. This prevents dividing the initial query projection weights by `sqrt(head_dim)`, ensuring unscaled canonical weight initialization for Gemma 4. 2. In Gemma 4 model configs (`gemma4-26b.yml`, `gemma4-31b.yml`, `gemma4-e2b.yml`, `gemma4-e4b.yml`), explicitly enabled `use_qk_norm: true`. 3. In `gemma4-e2b.yml` and `gemma4-e4b.yml`, set `scan_layers: false` since per-layer embeddings and variable layer configs are not compatible with layer scanning. 4. Added `tests/unit/gemma4_canonical_test.py` to verify that canonical attention scaling (`query_pre_attn_scalar = 1.0`), `use_qk_norm`, and unscaled weight initialization hold for both dense/MoE Gemma 4 and small Gemma 4 models. # Tests Added unit test: - `tests/unit/gemma4_canonical_test.py` TAG=agy CONV=f731b620-dff8-4a9f-b791-657890c4604f
1 parent 6762cbb commit c4b9418

6 files changed

Lines changed: 101 additions & 2 deletions

File tree

src/maxtext/configs/models/gemma4-26b.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ global_rope_proportion: 0.25
3939
local_rope_proportion: 1.0
4040
v_norm_with_scale: false
4141
final_logits_soft_cap: 30.0
42+
use_qk_norm: true
4243

4344
# MoE configuration
4445
num_experts: 128

src/maxtext/configs/models/gemma4-31b.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ rope_max_timescale: 1000000
4242
global_rope_proportion: 0.25
4343
local_rope_proportion: 1.0
4444
final_logits_soft_cap: 30.0
45+
use_qk_norm: true
4546

4647
# Multimodal flags (need to set use_multimodal=true)
4748
vision_encoder_block: "gemma4"

src/maxtext/configs/models/gemma4-e2b.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ rope_max_timescale: 1000000
4343
global_rope_proportion: 0.25
4444
local_rope_proportion: 1.0
4545
final_logits_soft_cap: 30.0
46+
use_qk_norm: true
47+
scan_layers: false
4648

4749
# Vision encoder flags — multimodal not yet supported for E2B / E4B.
4850
vision_encoder_block: "gemma4"

src/maxtext/configs/models/gemma4-e4b.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ rope_max_timescale: 1000000
4444
global_rope_proportion: 0.25
4545
local_rope_proportion: 1.0
4646
final_logits_soft_cap: 30.0
47+
use_qk_norm: true
48+
scan_layers: false
4749

4850
# Vision encoder flags — multimodal not yet supported for E2B / E4B.
4951
vision_encoder_block: "gemma4"

src/maxtext/layers/attentions.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -665,8 +665,10 @@ def init_query_w(self, inputs_q_shape: Tuple) -> nnx.Module:
665665
# linear transformations, which is equivalent under Adafactor.
666666
# We disable depth_scaling when using qk_norm or a query_pre_attn_scalar
667667
# to avoid applying scaling twice.
668-
if getattr(self.config, "use_qk_norm", False) or (
669-
self.query_pre_attn_scalar is not None and self.query_pre_attn_scalar != 1.0
668+
if (
669+
self.use_qk_norm
670+
or getattr(self.config, "use_qk_norm", False)
671+
or (self.query_pre_attn_scalar is not None and self.query_pre_attn_scalar != 1.0)
670672
):
671673
depth_scaling = 1.0
672674
else:
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Unit tests for canonical Gemma 4 attention scaling and weight initialization."""
16+
17+
import os
18+
import unittest
19+
import jax
20+
import jax.numpy as jnp
21+
from flax import nnx
22+
23+
from maxtext.configs import pyconfig
24+
from maxtext.common import common_types
25+
from maxtext.models import gemma4, gemma4_small
26+
from maxtext.utils.globals import MAXTEXT_REPO_ROOT
27+
28+
29+
class Gemma4CanonicalAttentionTest(unittest.TestCase):
30+
"""Tests that Gemma 4 models follow canonical attention scaling and unscaled weight init."""
31+
32+
def setUp(self):
33+
super().setUp()
34+
self.base_config_path = os.path.join(MAXTEXT_REPO_ROOT, "src", "maxtext", "configs", "base.yml")
35+
36+
def test_gemma4_26b_attention_config(self):
37+
config = pyconfig.initialize(
38+
["", self.base_config_path],
39+
model_name="gemma4-26b",
40+
enable_dropout=False,
41+
)
42+
self.assertTrue(config.use_qk_norm, "gemma4-26b should enable use_qk_norm in config")
43+
44+
mesh = jax.sharding.Mesh(jax.devices()[:1], ("data",))
45+
rngs = nnx.Rngs(0)
46+
layer = gemma4.Gemma4DecoderLayer(
47+
config=config,
48+
mesh=mesh,
49+
model_mode=common_types.MODEL_MODE_PREFILL,
50+
rngs=rngs,
51+
attention_type=gemma4.AttentionType.LOCAL_SLIDING,
52+
layer_idx=0,
53+
)
54+
# Canonical Gemma 4 attention uses query_pre_attn_scalar = 1.0 (unscaled logits)
55+
self.assertEqual(layer.self_attention.query_pre_attn_scalar, 1.0)
56+
self.assertTrue(layer.self_attention.use_qk_norm)
57+
58+
# Initial query weights should NOT be divided by sqrt(head_dim) = 16.0
59+
# Expected standard deviation for fan_in=2816 is 1/sqrt(2816) ~= 0.0188
60+
q_kernel = layer.self_attention.query.kernel[...]
61+
std_q = float(jnp.std(q_kernel))
62+
self.assertGreater(std_q, 0.01, f"Query kernel std ({std_q}) should not be divided by depth_scaling")
63+
64+
def test_gemma4_small_attention_config(self):
65+
for model_name in ["gemma4-e2b", "gemma4-e4b"]:
66+
config = pyconfig.initialize(
67+
["", self.base_config_path],
68+
model_name=model_name,
69+
enable_dropout=False,
70+
)
71+
self.assertTrue(config.use_qk_norm, f"{model_name} should enable use_qk_norm in config")
72+
73+
mesh = jax.sharding.Mesh(jax.devices()[:1], ("data",))
74+
rngs = nnx.Rngs(0)
75+
layer = gemma4_small.Gemma4SmallDecoderLayer(
76+
config=config,
77+
mesh=mesh,
78+
model_mode=common_types.MODEL_MODE_PREFILL,
79+
layer_idx=0,
80+
rngs=rngs,
81+
)
82+
self.assertEqual(layer.self_attention.query_pre_attn_scalar, 1.0)
83+
self.assertTrue(layer.self_attention.use_qk_norm)
84+
85+
q_kernel = layer.self_attention.query.kernel[...]
86+
std_q = float(jnp.std(q_kernel))
87+
self.assertGreater(std_q, 0.01, f"Query kernel std ({std_q}) should not be divided by depth_scaling")
88+
89+
90+
if __name__ == "__main__":
91+
unittest.main()

0 commit comments

Comments
 (0)