Skip to content

Commit e6fcf3a

Browse files
committed
Add ckpt conversion test in CI
1 parent 91b4dd5 commit e6fcf3a

3 files changed

Lines changed: 114 additions & 2 deletions

File tree

src/maxtext/checkpoint_conversion/to_maxtext.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151

5252
import argparse
5353
from functools import partial
54+
import importlib.util
5455
import json
5556
import os
5657
import sys
@@ -59,7 +60,6 @@
5960
from typing import Any, Callable, List, Sequence
6061
import absl
6162
import ml_dtypes
62-
import torch
6363
import flax.linen as nn
6464
from huggingface_hub import hf_hub_download, list_repo_files
6565
import jax
@@ -79,6 +79,21 @@
7979
from safetensors import safe_open
8080

8181

82+
def _lazy_import(name: str):
83+
spec = importlib.util.find_spec(name)
84+
if spec is None:
85+
return None
86+
loader = importlib.util.LazyLoader(spec.loader)
87+
spec.loader = loader
88+
module = importlib.util.module_from_spec(spec)
89+
sys.modules[name] = module
90+
loader.exec_module(module)
91+
return module
92+
93+
94+
torch = _lazy_import("torch")
95+
96+
8297
absl.logging.set_verbosity(absl.logging.INFO) # for max_logging.log
8398

8499

src/maxtext/configs/pyconfig.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,10 @@ def __getattr__(self, attr: str) -> Any:
295295
# This is necessary for proper pickling/unpickling support
296296
flat_config = object.__getattribute__(self, "_flat_config")
297297
if attr in flat_config:
298-
return flat_config[attr]
298+
val = flat_config[attr]
299+
if isinstance(val, dict) and attr in ("debug", "rl", "lora"):
300+
return getattr(object.__getattribute__(self, "_pydantic_config"), attr)
301+
return val
299302
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{attr}'")
300303

301304
def __setattr__(self, attr: str, value: Any) -> None:
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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+
"""Integration test for checkpoint conversion."""
16+
17+
import os
18+
import subprocess
19+
import sys
20+
import tempfile
21+
import unittest
22+
import pytest
23+
24+
pytestmark = [pytest.mark.integration_test]
25+
26+
27+
class Qwen3CheckpointConversionTest(unittest.TestCase):
28+
"""Tests HuggingFace to Orbax checkpoint conversion."""
29+
30+
@pytest.mark.cpu_only
31+
def test_qwen3_30b_a3b_roundtrip_conversion(self):
32+
model_name = "qwen3-30b-a3b"
33+
base_num_decoder_layers = 2
34+
with tempfile.TemporaryDirectory() as tmpdir:
35+
cmd = [
36+
sys.executable,
37+
"-m",
38+
"maxtext.checkpoint_conversion.to_maxtext",
39+
"src/maxtext/configs/base.yml",
40+
f"model_name={model_name}",
41+
f"base_output_directory={tmpdir}",
42+
f"base_num_decoder_layers={base_num_decoder_layers}",
43+
"override_model_config=True",
44+
"scan_layers=False",
45+
"weight_dtype=bfloat16",
46+
"hardware=cpu",
47+
"skip_jax_distributed_system=True",
48+
"checkpoint_storage_use_ocdbt=False",
49+
"checkpoint_storage_use_zarr3=False",
50+
"--lazy_load_tensors=True",
51+
]
52+
env = os.environ.copy()
53+
env["JAX_PLATFORMS"] = "cpu"
54+
env["HF_HOME"] = tmpdir
55+
56+
print("Running checkpoint conversion command:", " ".join(cmd))
57+
# Inherit stdout and stderr from the parent process to stream logs in real time
58+
subprocess.run(cmd, env=env, check=True)
59+
60+
# Verify output directory exists and contains items
61+
# Output structure: tmpdir/0/items
62+
expected_dir = os.path.join(tmpdir, "0", "items")
63+
self.assertTrue(os.path.exists(expected_dir), f"Expected checkpoint directory {expected_dir} does not exist.")
64+
self.assertTrue(len(os.listdir(expected_dir)) > 0, f"Checkpoint directory {expected_dir} is empty.")
65+
66+
# Roundtrip conversion back to HuggingFace format
67+
expected_hf_dir = os.path.join(tmpdir, "hf_safetensor", "qwen3-30b-a3b")
68+
hf_cmd = [
69+
sys.executable,
70+
"-m",
71+
"maxtext.checkpoint_conversion.to_huggingface",
72+
f"model_name={model_name}",
73+
f"load_parameters_path={expected_dir}",
74+
f"base_output_directory={expected_hf_dir}",
75+
f"base_num_decoder_layers={base_num_decoder_layers}",
76+
"override_model_config=True",
77+
"scan_layers=false",
78+
"weight_dtype=bfloat16",
79+
"hardware=cpu",
80+
"skip_jax_distributed_system=True",
81+
"--override_model_architecture=True",
82+
]
83+
print("Running roundtrip checkpoint conversion command (to HF):", " ".join(hf_cmd))
84+
subprocess.run(hf_cmd, env=env, check=True)
85+
86+
# Verify HF output directory exists and contains safetensors/config files
87+
self.assertTrue(
88+
os.path.exists(expected_hf_dir), f"Expected HF checkpoint directory {expected_hf_dir} does not exist."
89+
)
90+
self.assertTrue(len(os.listdir(expected_hf_dir)) > 0, f"HF checkpoint directory {expected_hf_dir} is empty.")
91+
92+
93+
if __name__ == "__main__":
94+
unittest.main()

0 commit comments

Comments
 (0)