1515"""Tests for Qwix LoRA utils in lora_utils.py"""
1616import re
1717import sys
18+ import tempfile
1819import unittest
1920from unittest import mock
21+
22+ from etils import epath
2023import jax
24+ import jax .numpy as jnp
2125import optax
2226import pytest
2327from flax import nnx
2630pytestmark = [pytest .mark .post_training ]
2731
2832# Now safe to do top-level imports
33+ from maxtext .common import checkpointing
2934from tunix .sft import peft_trainer
3035from maxtext .utils import lora_utils
3136from maxtext .utils import model_creation_utils
5964
6065def _make_config (** overrides ):
6166 """Return a MaxTextConfig object suitable for unit tests."""
67+ config_dict = _BASE_CONFIG .copy ()
68+ config_dict .update (overrides )
6269 # Use initialize_pydantic to get nested models as objects (attribute access)
6370 return pyconfig .initialize_pydantic (
6471 [sys .argv [0 ], get_test_config_path ()],
65- ** _BASE_CONFIG ,
66- ** overrides ,
72+ ** config_dict ,
6773 )
6874
6975
@@ -121,7 +127,12 @@ def test_build_lora_provider(self):
121127 with mock .patch ("qwix.LoraProvider" ) as mock_provider :
122128 lora_utils ._build_lora_provider (mock_config )
123129 mock_provider .assert_called_once_with (
124- module_path = "custom/path" , rank = 8 , alpha = 16.0 , dropout = 0.0 , weight_qtype = "int8" , tile_size = 32
130+ module_path = "custom/path" ,
131+ rank = 8 ,
132+ alpha = 16.0 ,
133+ dropout = 0.0 ,
134+ weight_qtype = "int8" ,
135+ tile_size = 32 ,
125136 )
126137
127138 def test_prepare_dummy_inputs (self ):
@@ -173,7 +184,13 @@ def test_apply_lora_to_model_adapters_loaded(self):
173184 # If we skip Qwix, it should stay False.
174185 self .assertFalse (lora_utils .is_lora_enabled (result ))
175186
176- def _run_apply_lora_test (self , scan_layers : bool , weight_qtype = None , tile_size = None , mock_multihost : bool = False ):
187+ def _run_apply_lora_test (
188+ self ,
189+ scan_layers : bool ,
190+ weight_qtype = None ,
191+ tile_size = None ,
192+ mock_multihost : bool = False ,
193+ ):
177194 """Helper to run LoRA application test with/without scanned layers and optional QLoRA."""
178195 # Passing nested dict as 'lora' kwarg to _make_config
179196 cfg = _make_config (
@@ -246,7 +263,12 @@ def test_apply_lora_multihost_mock(self):
246263 def test_restore_lora_from_path (self ):
247264 """Test restoration of LoRA parameters from a path."""
248265 cfg = _make_config (
249- lora = {"enable_lora" : True , "lora_restore_path" : "some/path" , "lora_rank" : 4 , "lora_alpha" : 8.0 },
266+ lora = {
267+ "enable_lora" : True ,
268+ "lora_restore_path" : "some/path" ,
269+ "lora_rank" : 4 ,
270+ "lora_alpha" : 8.0 ,
271+ },
250272 scan_layers = False ,
251273 )
252274 model , _ = model_creation_utils .from_pretrained (cfg , mesh = None , model_mode = model_creation_utils .MODEL_MODE_TRAIN )
@@ -271,6 +293,135 @@ def test_restore_lora_from_path(self):
271293 self .assertTrue (kwargs ["args" ].partial_restore )
272294 mock_update .assert_called_once ()
273295
296+ def test_sync_lora_metadata_default_syncs (self ):
297+ """Test that default lora rank/alpha are successfully synced from checkpoint metadata."""
298+ cfg = _make_config (
299+ lora = {
300+ "enable_lora" : True ,
301+ "lora_restore_path" : "dummy/path" ,
302+ "lora_rank" : 0 ,
303+ "lora_alpha" : 0.0 ,
304+ }
305+ )
306+ mock_metadata = mock .MagicMock ()
307+ mock_metadata .custom_metadata = {"lora" : {"lora_rank" : 32 , "lora_alpha" : 64.0 }}
308+
309+ with mock .patch ("orbax.checkpoint.StandardCheckpointer.metadata" , return_value = mock_metadata ):
310+ lora_utils .sync_lora_metadata (cfg )
311+ self .assertEqual (cfg .lora .lora_rank , 32 )
312+ self .assertEqual (cfg .lora .lora_alpha , 64.0 )
313+
314+ def test_sync_lora_metadata_matching_passes (self ):
315+ """Test that matching non-default parameters pass without errors."""
316+ cfg = _make_config (
317+ lora = {
318+ "enable_lora" : True ,
319+ "lora_restore_path" : "dummy/path" ,
320+ "lora_rank" : 32 ,
321+ "lora_alpha" : 64.0 ,
322+ }
323+ )
324+ mock_metadata = mock .MagicMock ()
325+ mock_metadata .custom_metadata = {"lora" : {"lora_rank" : 32 , "lora_alpha" : 64.0 }}
326+
327+ with mock .patch ("orbax.checkpoint.StandardCheckpointer.metadata" , return_value = mock_metadata ):
328+ # Should not raise ValueError
329+ lora_utils .sync_lora_metadata (cfg )
330+ self .assertEqual (cfg .lora .lora_rank , 32 )
331+ self .assertEqual (cfg .lora .lora_alpha , 64.0 )
332+
333+ def test_sync_lora_metadata_rank_mismatch_fails (self ):
334+ """Test that configured rank mismatching checkpoint metadata rank raises ValueError."""
335+ cfg = _make_config (
336+ lora = {
337+ "enable_lora" : True ,
338+ "lora_restore_path" : "dummy/path" ,
339+ "lora_rank" : 8 ,
340+ "lora_alpha" : 64.0 ,
341+ }
342+ )
343+ mock_metadata = mock .MagicMock ()
344+ mock_metadata .custom_metadata = {"lora" : {"lora_rank" : 32 , "lora_alpha" : 64.0 }}
345+
346+ with mock .patch ("orbax.checkpoint.StandardCheckpointer.metadata" , return_value = mock_metadata ):
347+ with self .assertRaisesRegex (ValueError , "Configured lora_rank .* does not match" ):
348+ lora_utils .sync_lora_metadata (cfg )
349+
350+ def test_sync_lora_metadata_alpha_mismatch_fails (self ):
351+ """Test that configured alpha mismatching checkpoint metadata alpha raises ValueError."""
352+ cfg = _make_config (
353+ lora = {
354+ "enable_lora" : True ,
355+ "lora_restore_path" : "dummy/path" ,
356+ "lora_rank" : 32 ,
357+ "lora_alpha" : 16.0 ,
358+ }
359+ )
360+ mock_metadata = mock .MagicMock ()
361+ mock_metadata .custom_metadata = {"lora" : {"lora_rank" : 32 , "lora_alpha" : 64.0 }}
362+
363+ with mock .patch ("orbax.checkpoint.StandardCheckpointer.metadata" , return_value = mock_metadata ):
364+ with self .assertRaisesRegex (ValueError , "Configured lora_alpha .* does not match" ):
365+ lora_utils .sync_lora_metadata (cfg )
366+
367+ def test_save_checkpoint_passes_metadata (self ):
368+ """Test that save_checkpoint correctly generates and passes custom lora metadata to CheckpointManager."""
369+ cfg = _make_config (
370+ lora = {"enable_lora" : True , "lora_rank" : 8 , "lora_alpha" : 16.0 },
371+ enable_checkpointing = True ,
372+ )
373+ mock_manager = mock .MagicMock ()
374+ mock_state = mock .MagicMock ()
375+
376+ with mock .patch ("jax.block_until_ready" ):
377+ checkpointing .save_checkpoint (mock_manager , step = 10 , state = mock_state , config = cfg )
378+ mock_manager .save .assert_called_once ()
379+ _ , kwargs = mock_manager .save .call_args
380+ self .assertIn ("custom_metadata" , kwargs )
381+ self .assertEqual (kwargs ["custom_metadata" ], {"lora" : cfg .lora .model_dump ()})
382+
383+ def test_save_and_restore_metadata_integration (self ):
384+ """Integration test checking that Orbax CheckpointManager writes and reads custom LoRA metadata."""
385+
386+ cfg_save = _make_config (
387+ lora = {"enable_lora" : True , "lora_rank" : 8 , "lora_alpha" : 16.0 },
388+ enable_checkpointing = True ,
389+ )
390+
391+ with tempfile .TemporaryDirectory () as tmpdir :
392+ manager = checkpointing .create_orbax_checkpoint_manager (
393+ tmpdir ,
394+ enable_checkpointing = True ,
395+ use_async = False ,
396+ save_interval_steps = 1 ,
397+ use_ocdbt = False ,
398+ use_zarr3 = False ,
399+ )
400+
401+ # Use save_checkpoint wrapper with a simple state
402+ dummy_state = {"weight" : jnp .array ([1.0 , 2.0 ])}
403+ checkpointing .save_checkpoint (manager , step = 0 , state = dummy_state , config = cfg_save )
404+ manager .wait_until_finished ()
405+
406+ # Now verify that the saved checkpoint contains metadata on disk
407+ checkpoint_dir = epath .Path (tmpdir ) / "0"
408+ self .assertTrue ((checkpoint_dir / "_CHECKPOINT_METADATA" ).exists ())
409+
410+ # Restore using sync_lora_metadata on a config with default rank/alpha
411+ cfg_restore = _make_config (
412+ lora = {
413+ "enable_lora" : True ,
414+ "lora_restore_path" : str (checkpoint_dir ),
415+ "lora_rank" : 0 ,
416+ "lora_alpha" : 0.0 ,
417+ }
418+ )
419+ lora_utils .sync_lora_metadata (cfg_restore )
420+
421+ # Verify values were successfully synced back
422+ self .assertEqual (cfg_restore .lora .lora_rank , 8 )
423+ self .assertEqual (cfg_restore .lora .lora_alpha , 16.0 )
424+
274425 def test_gemma4_lora_path_matching (self ):
275426 """Test that the Gemma4 LoRA regex correctly matches all expected parameter paths."""
276427 mock_config = mock .MagicMock (spec = pyconfig .HyperParameters )
@@ -309,10 +460,6 @@ def test_gemma4_lora_path_matching(self):
309460 "decoder/layers_remainder/layers/0/mlp/shared_experts/wi_0/kernel" ,
310461 "decoder/layers_remainder/layers/0/mlp/shared_experts/wi_1/kernel" ,
311462 "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" ,
316463 ]
317464
318465 for path in matching_paths :
0 commit comments