Skip to content

Commit 4f2de47

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

3 files changed

Lines changed: 117 additions & 6 deletions

File tree

src/maxtext/checkpoint_conversion/to_maxtext.py

Lines changed: 20 additions & 5 deletions
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

@@ -807,7 +822,7 @@ def _merged_getter(key):
807822
def main(
808823
args: Sequence[str],
809824
lazy_load_tensors: bool = False,
810-
eager_load_method: str = "transformers",
825+
eager_load_method: str = "safetensors",
811826
hf_model_path: str | None = None,
812827
revision: str | None = None,
813828
save_dtype: str = "bfloat16",
@@ -894,9 +909,9 @@ def main(
894909
# (e.g., Multi-Token Prediction weights (`layers.61`) in DeepSeek-V3).
895910
#
896911
# Recommendation:
897-
# - Use 'transformers' as the default for backward compatibility of mapping.
898-
# - 'safetensors' is an interchangeable and valid alternative for most models,
899-
# and is strictly required if the model or specific weights lack Transformers support.
912+
# - Use 'safetensors' as the default. Since transformers 5.8.0, model initialization
913+
# changed and the 'transformers' method may produce different key structures.
914+
# - Use 'transformers' only if explicitly needed for backward-compatible key mapping.
900915
if eager_load_method == "transformers":
901916
max_logging.log("Eager load with Transformers backend, from_pretrained with auto dtype")
902917
# For auto mode, loaded dtype is the same as `dtype` specified in config.json (or `torch_dtype` for older version)

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: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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+
to_mt_cmd = [
36+
sys.executable,
37+
"-m",
38+
"maxtext.checkpoint_conversion.to_maxtext",
39+
f"model_name={model_name}",
40+
f"base_output_directory={tmpdir}",
41+
f"base_num_decoder_layers={base_num_decoder_layers}",
42+
"override_model_config=True",
43+
"scan_layers=False",
44+
"hardware=cpu",
45+
"skip_jax_distributed_system=True",
46+
"checkpoint_storage_use_ocdbt=False",
47+
"checkpoint_storage_use_zarr3=False",
48+
"--save_dtype=bfloat16",
49+
"--lazy_load_tensors=True",
50+
]
51+
env = os.environ.copy()
52+
env["JAX_PLATFORMS"] = "cpu"
53+
env["HF_HOME"] = tmpdir
54+
55+
print("Running checkpoint conversion command:", " ".join(to_mt_cmd))
56+
# Inherit stdout and stderr from the parent process to stream logs in real time
57+
subprocess.run(to_mt_cmd, env=env, check=True)
58+
59+
# Verify output directory exists and contains items
60+
# Output structure: tmpdir/0/items
61+
expected_dir = os.path.join(tmpdir, "0", "items")
62+
self.assertTrue(os.path.exists(expected_dir), f"Expected checkpoint directory {expected_dir} does not exist.")
63+
self.assertTrue(len(os.listdir(expected_dir)) > 0, f"Checkpoint directory {expected_dir} is empty.")
64+
65+
# Roundtrip conversion back to HuggingFace format
66+
expected_hf_dir = os.path.join(tmpdir, "hf_safetensor", "qwen3-30b-a3b")
67+
to_hf_cmd = [
68+
sys.executable,
69+
"-m",
70+
"maxtext.checkpoint_conversion.to_huggingface",
71+
f"model_name={model_name}",
72+
f"load_parameters_path={expected_dir}",
73+
f"base_output_directory={expected_hf_dir}",
74+
f"base_num_decoder_layers={base_num_decoder_layers}",
75+
"override_model_config=True",
76+
"scan_layers=false",
77+
"weight_dtype=bfloat16",
78+
"hardware=cpu",
79+
"skip_jax_distributed_system=True",
80+
"--override_model_architecture=True",
81+
]
82+
print("Running roundtrip checkpoint conversion command (to HF):", " ".join(to_hf_cmd))
83+
subprocess.run(to_hf_cmd, env=env, check=True)
84+
85+
# Verify HF output directory exists and contains safetensors/config files
86+
self.assertTrue(
87+
os.path.exists(expected_hf_dir), f"Expected HF checkpoint directory {expected_hf_dir} does not exist."
88+
)
89+
self.assertTrue(len(os.listdir(expected_hf_dir)) > 0, f"HF checkpoint directory {expected_hf_dir} is empty.")
90+
91+
92+
if __name__ == "__main__":
93+
unittest.main()

0 commit comments

Comments
 (0)