Skip to content

Commit 3a43835

Browse files
committed
Fix double-compilation in train_step by propagating input sharding to AOT compilation shaped batch.
Previously, train_step compiled twice in various training loops (pre-train, SFT): first Ahead-Of-Time (AOT) using an unsharded shaped_batch dummy, and then again on the first step execution using a sharded example_batch from the data pipeline. This caused a slow first training step. This change propagates the input data sharding to get_shaped_batch in train.py, train_compile.py, and train_sft_native.py so the dummy AOT batch has the same sharding annotation as the runtime batch, matching their signatures and enabling JAX JIT compilation cache hits. Also added: - Unit tests for get_shaped_batch sharding in maxtext_utils_test.py. - CPU compilation cache regression test in compile_cache_test.py.
1 parent 45b0f2b commit 3a43835

6 files changed

Lines changed: 159 additions & 13 deletions

File tree

src/maxtext/trainers/post_train/sft/train_sft_native.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ def train_loop(config, recorder, state=None):
8080
)
8181

8282
with jax.set_mesh(mesh), nn_partitioning.axis_rules(config.logical_axis_rules):
83-
shaped_batch = maxtext_utils.get_shaped_batch(config)
83+
data_sharding = sharding.get_input_data_sharding(config, mesh)
84+
shaped_batch = maxtext_utils.get_shaped_batch(config, batch_sharding=data_sharding)
8485
compiled = p_train_step.lower(state, shaped_batch, init_rng).compile()
8586
compiled_stats = compiled.memory_analysis()
8687
max_utils.print_compiled_memory_stats(compiled_stats)

src/maxtext/trainers/pre_train/train.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,8 @@ def train_loop(config, recorder, state=None):
640640
)
641641

642642
with jax.set_mesh(mesh), mesh, nn_partitioning.axis_rules(config.logical_axis_rules):
643-
shaped_batch = maxtext_utils.get_shaped_batch(config)
643+
data_sharding = sharding.get_input_data_sharding(config, mesh)
644+
shaped_batch = maxtext_utils.get_shaped_batch(config, batch_sharding=data_sharding)
644645
if config.shard_optimizer_over_data and isinstance(model, nn.Module):
645646
state = sharding.maybe_shard_with_name(state, state_mesh_shardings, config.shard_mode)
646647
elif config.shard_optimizer_over_data:

src/maxtext/trainers/pre_train/train_compile.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,8 @@ def create_train_state_fn():
172172
logical_annotations = maxtext_utils.get_logical_annotations(config, topology_mesh, init_state_fn)
173173

174174
# Shaped batch
175-
shaped_batch = maxtext_utils.get_shaped_batch(config)
175+
data_sharding = sharding.get_input_data_sharding(config, topology_mesh)
176+
shaped_batch = maxtext_utils.get_shaped_batch(config, batch_sharding=data_sharding)
176177

177178
if config.pure_nnx:
178179
shaped_train_args = (

src/maxtext/utils/maxtext_utils.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ def get_reorder_callable(cp_size, shard_mode, reorder_strategy=ReorderStrategy.D
148148
)
149149

150150

151-
def get_shaped_batch(config):
151+
def get_shaped_batch(config, batch_sharding=None):
152152
"""Return the shape of the batch - this is what eval_shape would return for the
153153
output of create_data_iterator, but eval_shape doesn't work, see b/306901078."""
154154
if config.enable_diloco:
@@ -160,21 +160,21 @@ def get_shaped_batch(config):
160160
else:
161161
batch_shape = (config.global_batch_size_to_load, config.max_target_length)
162162
shaped_batch = {}
163-
shaped_batch["inputs"] = jax.ShapeDtypeStruct(batch_shape, jnp.int32)
164-
shaped_batch["inputs_position"] = jax.ShapeDtypeStruct(batch_shape, jnp.int32)
165-
shaped_batch["inputs_segmentation"] = jax.ShapeDtypeStruct(batch_shape, jnp.int32)
166-
shaped_batch["targets"] = jax.ShapeDtypeStruct(batch_shape, jnp.int32)
167-
shaped_batch["targets_position"] = jax.ShapeDtypeStruct(batch_shape, jnp.int32)
168-
shaped_batch["targets_segmentation"] = jax.ShapeDtypeStruct(batch_shape, jnp.int32)
163+
shaped_batch["inputs"] = jax.ShapeDtypeStruct(batch_shape, jnp.int32, sharding=batch_sharding)
164+
shaped_batch["inputs_position"] = jax.ShapeDtypeStruct(batch_shape, jnp.int32, sharding=batch_sharding)
165+
shaped_batch["inputs_segmentation"] = jax.ShapeDtypeStruct(batch_shape, jnp.int32, sharding=batch_sharding)
166+
shaped_batch["targets"] = jax.ShapeDtypeStruct(batch_shape, jnp.int32, sharding=batch_sharding)
167+
shaped_batch["targets_position"] = jax.ShapeDtypeStruct(batch_shape, jnp.int32, sharding=batch_sharding)
168+
shaped_batch["targets_segmentation"] = jax.ShapeDtypeStruct(batch_shape, jnp.int32, sharding=batch_sharding)
169169
if config.use_multimodal:
170170
image_shape = mm_processor.get_dummy_image_shape_for_init(
171171
config.model_name, batch_size=config.micro_batch_size_to_train_on
172172
)
173-
shaped_batch["images"] = jax.ShapeDtypeStruct(image_shape, jnp.int32)
174-
shaped_batch["image_masks"] = jax.ShapeDtypeStruct(image_shape[:2], jnp.int32)
173+
shaped_batch["images"] = jax.ShapeDtypeStruct(image_shape, jnp.int32, sharding=batch_sharding)
174+
shaped_batch["image_masks"] = jax.ShapeDtypeStruct(image_shape[:2], jnp.int32, sharding=batch_sharding)
175175
if config.use_audio:
176176
audio_shape = mm_processor.get_dummy_audio_shape_for_init(config)
177-
shaped_batch["audios"] = jax.ShapeDtypeStruct(audio_shape, jnp.float32)
177+
shaped_batch["audios"] = jax.ShapeDtypeStruct(audio_shape, jnp.float32, sharding=batch_sharding)
178178
return shaped_batch
179179

180180

tests/unit/compile_cache_test.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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+
"""Tests for JAX compilation cache hits in train.py.
16+
17+
This test ensures that the `train_step` function is compiled only once.
18+
It verifies that the Ahead-Of-Time (AOT) compilation signature (which uses
19+
a dummy `shaped_batch` constructed in `train.py`) matches the runtime
20+
compilation signature (which uses the actual `example_batch` from the data pipeline).
21+
22+
If this test fails, it likely means a regression was introduced where the AOT
23+
batch sharding/shape does not match the runtime batch sharding/shape. This causes
24+
JAX to recompile `train_step` at step 0, leading to a "double compilation"
25+
and a very slow first step.
26+
27+
To debug:
28+
1. Verify that `maxtext_utils.get_shaped_batch` in `train.py` is called with the
29+
correct `sharding` argument (matching the data pipeline sharding).
30+
2. Check if there are differences in shapes or dtypes between the AOT dummy batch
31+
and the runtime batch.
32+
"""
33+
34+
import os
35+
import tempfile
36+
import shutil
37+
import pytest
38+
import subprocess
39+
import sys
40+
41+
from tests.utils.test_helpers import (
42+
get_test_config_path,
43+
get_test_base_output_directory,
44+
)
45+
46+
47+
@pytest.mark.cpu_only
48+
def test_train_step_cache_hit():
49+
temp_dir = tempfile.mkdtemp()
50+
_base_output_directory = get_test_base_output_directory()
51+
52+
try:
53+
small_model_overrides = [
54+
"base_emb_dim=16",
55+
"base_num_query_heads=4",
56+
"base_num_kv_heads=4",
57+
"base_mlp_dim=16",
58+
"base_num_decoder_layers=1",
59+
"head_dim=64",
60+
"max_target_length=64",
61+
"vocab_size=32",
62+
"sharding_tolerance=0.1",
63+
]
64+
65+
cmd = [
66+
sys.executable,
67+
"-m",
68+
"maxtext.trainers.pre_train.train",
69+
get_test_config_path(),
70+
f"base_output_directory={_base_output_directory}",
71+
"run_name=compile_cache_test_cpu",
72+
"steps=2",
73+
"enable_checkpointing=False",
74+
"enable_goodput_recording=False",
75+
"dataset_type=synthetic",
76+
"hardware=cpu",
77+
"skip_jax_distributed_system=True",
78+
f"jax_cache_dir={temp_dir}",
79+
] + small_model_overrides
80+
81+
env = os.environ.copy()
82+
env["JAX_PLATFORMS"] = "cpu"
83+
env["JAX_ENABLE_COMPILATION_CACHE"] = "true"
84+
env["JAX_COMPILATION_CACHE_DIR"] = temp_dir
85+
env["JAX_LOG_COMPILES"] = "1"
86+
87+
print("Running CPU training subprocess:", " ".join(cmd))
88+
result = subprocess.run(cmd, env=env, capture_output=True, text=True, check=True)
89+
90+
captured_logs = result.stderr
91+
92+
# Print captured logs for debugging (will be shown by pytest if assert fails)
93+
print("=== Captured Subprocess Stderr ===")
94+
print(captured_logs)
95+
print("===================================")
96+
97+
# Check if cache dir has files
98+
cache_files = os.listdir(temp_dir)
99+
print("=== Cache Directory Content ===")
100+
print(f"Path: {temp_dir}")
101+
print(f"Files: {cache_files}")
102+
print("===============================")
103+
104+
assert len(cache_files) > 0, (
105+
"JAX compilation cache directory is empty. This suggests the compilation "
106+
"cache was not writeable or the JAX cache configuration was ignored."
107+
)
108+
109+
assert len(cache_files) == 1, (
110+
f"Expected exactly 1 JAX compilation cache file, but found {len(cache_files)}: {cache_files}. "
111+
"This indicates a cache miss where AOT compilation and runtime execution generated different keys, "
112+
"causing train_step to be compiled twice (double-compilation regression)."
113+
)
114+
115+
assert "Persistent compilation cache hit for 'jit_train_step'" in captured_logs, (
116+
"Did not find 'Persistent compilation cache hit for 'jit_train_step'' in logs. "
117+
"This means the runtime execution of train_step did not hit the cache populated by the AOT compilation. "
118+
"Check if the AOT input batch signature (shape/dtype/sharding) matches the runtime input batch."
119+
)
120+
121+
finally:
122+
if os.path.exists(temp_dir):
123+
shutil.rmtree(temp_dir)

tests/unit/maxtext_utils_test.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,6 +1104,7 @@ def test_linen_in_shardings_includes_rng(self):
11041104
self.assertEqual(len(in_shardings), 3)
11051105

11061106

1107+
@pytest.mark.cpu_only
11071108
class TestGetShapedBatch(unittest.TestCase):
11081109
"""Tests for get_shaped_batch."""
11091110

@@ -1159,6 +1160,25 @@ def test_all_values_are_shape_dtype_struct(self):
11591160
for v in batch.values():
11601161
self.assertIsInstance(v, jax.ShapeDtypeStruct)
11611162

1163+
def test_get_shaped_batch_unsharded(self):
1164+
"""Verify that get_shaped_batch returns unsharded ShapeDtypeStructs by default."""
1165+
cfg = self._make_cfg()
1166+
shaped_batch = maxtext_utils.get_shaped_batch(cfg)
1167+
self.assertIn("inputs", shaped_batch)
1168+
self.assertIsNone(shaped_batch["inputs"].sharding)
1169+
1170+
def test_get_shaped_batch_sharded(self):
1171+
"""Verify that get_shaped_batch applies the passed sharding to ShapeDtypeStructs."""
1172+
cfg = self._make_cfg()
1173+
devices = np.array(jax.local_devices()[:1]).reshape(
1174+
1,
1175+
)
1176+
mesh = Mesh(devices, ("x",))
1177+
sharding_spec = NamedSharding(mesh, PartitionSpec("x"))
1178+
shaped_batch = maxtext_utils.get_shaped_batch(cfg, batch_sharding=sharding_spec)
1179+
self.assertIn("inputs", shaped_batch)
1180+
self.assertEqual(shaped_batch["inputs"].sharding, sharding_spec)
1181+
11621182

11631183
class TestShouldPreventCseInRemat(unittest.TestCase):
11641184
"""Tests for should_prevent_cse_in_remat."""

0 commit comments

Comments
 (0)