Skip to content

Commit ca6a7e8

Browse files
committed
Add unit tests for Gemma 4 LoRA targeting and NNX graph caching
1 parent c5bc2e4 commit ca6a7e8

3 files changed

Lines changed: 113 additions & 1 deletion

File tree

src/maxtext/configs/post_train/lora_module_path.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ mistral: "decoder/layers/.*(attention/(query|key|value|out)|mlp/(wi_0|wi_1|wo))"
2121
deepseek2: "decoder/(dense_layers|moe_stack)/self_attention/(query|out|wkv_a|wkv_b)|decoder/(dense_layers|moe_stack)/(mlp|shared_experts)/(wi_0|wi_1|wo)"
2222
gemma2: "decoder/(scanned_blocks|layers_remainder|layers)/(self_attention_local|self_attention_global)/(query|key|value|out)|decoder/(scanned_blocks|layers_remainder|layers)/(mlp_local|mlp_global)/(wi_0|wi_1|wo)"
2323
gemma3: "decoder/(scanned_blocks|layers_remainder|layers)/.*(self_attention/(query|key|value|out)|mlp/(wi_0|wi_1|wo|gate|up|down))"
24-
gemma4: "decoder/(scanned_blocks|layers_remainder)/layers.*/.*(self_attention/(query|key|value|out)|mlp/.*(wi_0|wi_1|wo|shared_experts/(wi_0|wi_1|wo)))"
24+
gemma4: "decoder/((scanned_blocks|layers_remainder)/)?layers.*/.*(self_attention/(query|key|value|out)|mlp/.*(wi_0|wi_1|wo|shared_experts/(wi_0|wi_1|wo)))"
2525
olmo3: "decoder/layers/.*(attention/(query|key|value|out)|mlp/(wi_0|wi_1|wo))"
2626
gpt3: "decoder/layers/(self_attention/(qkv_proj|out)|mlp/(wi|wo))"
2727

tests/post_training/unit/lora_utils_test.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
"""Tests for Qwix LoRA utils in lora_utils.py"""
16+
import re
1617
import sys
1718
import unittest
1819
from unittest import mock
@@ -84,6 +85,14 @@ def test_get_lora_module_path(self):
8485
"decoder/layers/(?:[0-9]+/)?.*(self_attention/(query|key|value|out)|mlp/(wi_0|wi_1|wo))",
8586
)
8687

88+
mock_config.model_name = "gemma4-9b"
89+
path = lora_utils._get_lora_module_path(mock_config)
90+
self.assertEqual(
91+
path,
92+
"decoder/((scanned_blocks|layers_remainder)/)?layers.*/.*"
93+
"(self_attention/(query|key|value|out)|mlp/.*(wi_0|wi_1|wo|shared_experts/(wi_0|wi_1|wo)))",
94+
)
95+
8796
mock_config.model_name = "unknown_model"
8897
# Ensure lora.lora_module_path is still empty string to trigger fallback
8998
mock_config.lora.lora_module_path = ""
@@ -262,6 +271,66 @@ def test_restore_lora_from_path(self):
262271
self.assertTrue(kwargs["args"].partial_restore)
263272
mock_update.assert_called_once()
264273

274+
def test_gemma4_lora_path_matching(self):
275+
"""Test that the Gemma4 LoRA regex correctly matches all expected parameter paths."""
276+
mock_config = mock.MagicMock(spec=pyconfig.HyperParameters)
277+
mock_config.lora = mock.MagicMock()
278+
mock_config.lora.lora_module_path = ""
279+
mock_config.model_name = "gemma4-9b"
280+
281+
path_regex = lora_utils._get_lora_module_path(mock_config)
282+
compiled = re.compile(path_regex)
283+
284+
# Expected matching paths:
285+
matching_paths = [
286+
# Scan layers = True, Dense/MoE attention
287+
"decoder/scanned_blocks/layers/self_attention/query/kernel",
288+
"decoder/scanned_blocks/layers/self_attention/key/kernel",
289+
"decoder/scanned_blocks/layers/self_attention/value/kernel",
290+
"decoder/scanned_blocks/layers/self_attention/out/kernel",
291+
# Scan layers = True, Dense MLP
292+
"decoder/scanned_blocks/layers/mlp/wi_0/kernel",
293+
"decoder/scanned_blocks/layers/mlp/wi_1/kernel",
294+
"decoder/scanned_blocks/layers/mlp/wo/kernel",
295+
# Scan layers = True, MoE MLP
296+
"decoder/scanned_blocks/layers/mlp/shared_experts/wi_0/kernel",
297+
"decoder/scanned_blocks/layers/mlp/shared_experts/wi_1/kernel",
298+
"decoder/scanned_blocks/layers/mlp/shared_experts/wo/kernel",
299+
# Scan layers = False, Dense/MoE attention
300+
"decoder/layers_remainder/layers/0/self_attention/query/kernel",
301+
"decoder/layers_remainder/layers/0/self_attention/key/kernel",
302+
"decoder/layers_remainder/layers/0/self_attention/value/kernel",
303+
"decoder/layers_remainder/layers/0/self_attention/out/kernel",
304+
# Scan layers = False, Dense MLP
305+
"decoder/layers_remainder/layers/0/mlp/wi_0/kernel",
306+
"decoder/layers_remainder/layers/0/mlp/wi_1/kernel",
307+
"decoder/layers_remainder/layers/0/mlp/wo/kernel",
308+
# Scan layers = False, MoE MLP
309+
"decoder/layers_remainder/layers/0/mlp/shared_experts/wi_0/kernel",
310+
"decoder/layers_remainder/layers/0/mlp/shared_experts/wi_1/kernel",
311+
"decoder/layers_remainder/layers/0/mlp/shared_experts/wo/kernel",
312+
# No scanned_blocks/layers_remainder prefix (e.g. fallback or direct structure)
313+
"decoder/layers/0/self_attention/query/kernel",
314+
"decoder/layers/0/mlp/wi_0/kernel",
315+
"decoder/layers/layers/0/mlp/shared_experts/wi_0/kernel",
316+
]
317+
318+
for path in matching_paths:
319+
self.assertTrue(compiled.search(path), f"Failed to match valid path: {path}")
320+
321+
# Expected non-matching paths (e.g. layernorm, embedding):
322+
non_matching_paths = [
323+
"decoder/scanned_blocks/layers/pre_self_attention_norm/scale",
324+
"decoder/scanned_blocks/layers/post_self_attention_norm/scale",
325+
"decoder/layers_remainder/layers/0/pre_self_attention_norm/scale",
326+
"decoder/layers_remainder/layers/0/post_self_attention_norm/scale",
327+
"decoder/final_norm/scale",
328+
"token_embedder/embedding",
329+
]
330+
331+
for path in non_matching_paths:
332+
self.assertFalse(compiled.search(path), f"Incorrectly matched invalid path: {path}")
333+
265334

266335
if __name__ == "__main__":
267336
unittest.main()

tests/post_training/unit/train_sft_test.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""Unit tests for train_sft.py."""
1616

1717
import unittest
18+
from unittest import mock
1819
from types import SimpleNamespace
1920
import pytest
2021

@@ -42,6 +43,48 @@ def test_validate_config_invalid_offload(self):
4243
with self.assertRaisesRegex(ValueError, "optimizer_memory_host_offload=True is not supported"):
4344
train_sft.validate_config(config)
4445

46+
@pytest.mark.cpu_only
47+
def test_train_model_caching_moe(self):
48+
"""Test that NNX graph caching is disabled for MoE models (num_experts > 1)."""
49+
mt_config = SimpleNamespace(
50+
logical_axis_rules=[],
51+
num_experts=8,
52+
)
53+
trainer = mock.MagicMock()
54+
trainer.data_hooks.train_data_iterator = "train_iter"
55+
trainer.data_hooks.eval_data_iterator = "eval_iter"
56+
mesh = mock.MagicMock()
57+
58+
with mock.patch("jax.set_mesh"):
59+
train_sft.train_model(mt_config, trainer, mesh)
60+
61+
trainer.train.assert_called_once_with(
62+
"train_iter",
63+
"eval_iter",
64+
cache_nnx_graph=False,
65+
)
66+
67+
@pytest.mark.cpu_only
68+
def test_train_model_caching_dense(self):
69+
"""Test that NNX graph caching is enabled for dense models (num_experts <= 1)."""
70+
mt_config = SimpleNamespace(
71+
logical_axis_rules=[],
72+
num_experts=1,
73+
)
74+
trainer = mock.MagicMock()
75+
trainer.data_hooks.train_data_iterator = "train_iter"
76+
trainer.data_hooks.eval_data_iterator = "eval_iter"
77+
mesh = mock.MagicMock()
78+
79+
with mock.patch("jax.set_mesh"):
80+
train_sft.train_model(mt_config, trainer, mesh)
81+
82+
trainer.train.assert_called_once_with(
83+
"train_iter",
84+
"eval_iter",
85+
cache_nnx_graph=True,
86+
)
87+
4588

4689
if __name__ == "__main__":
4790
unittest.main()

0 commit comments

Comments
 (0)