Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
88334d0
Fix Tunix is_update_step compatibility and Qwix LoRA FSDP mesh shardi…
RexBearIU Aug 13, 2026
4dfd80a
refactor: remove **kwargs and use explicit parameter list in train_step
RexBearIU Aug 13, 2026
3d553f6
refactor: encapsulate mesh dimension parsing inside _prepare_dummy_in…
RexBearIU Aug 13, 2026
36c76c5
fix(e2e): Remove hardcoded ici_fsdp_parallelism=64 from test_gpt_oss.…
RexBearIU Aug 13, 2026
ccc3ba7
fix(e2e): Install torchvision for qwen3-vl tests
RexBearIU Aug 13, 2026
5ba1a0a
fix(e2e): Adjust gemma3-4b logit check max_kl_div tolerance to 0.05
RexBearIU Aug 13, 2026
b084039
fix(e2e): add checkpoint_period=1 to qwen3-vl-2b multimodal sft
RexBearIU Aug 13, 2026
8275719
Remove redundant checkpoint_period=1 from qwen3 multimodal sft test
RexBearIU Aug 13, 2026
c2c4c37
Keep checkpoint_period=1 for qwen3 multimodal sft step 4 decode
RexBearIU Aug 13, 2026
7b5e642
Allow kwargs in MaxTextPeftTrainer train_step
RexBearIU Aug 13, 2026
8f49b3b
fix(test_gemma4_to_mt): add matmul_precision=highest to forward_pass_…
RexBearIU Aug 13, 2026
15fd9b0
Directly use is_update_step in train_sft and remove redundant checkpo…
RexBearIU Aug 13, 2026
2948c31
Fix rollout parallelism in test_gpt_oss_rl.sh for v5p-8
RexBearIU Aug 13, 2026
241fe34
Adjust max_kl_div tolerance to 0.8 for Gemma4-26B CPU forward pass
RexBearIU Aug 13, 2026
1a2ef3f
Keep max_kl_div=0.05 for Gemma4-26B
RexBearIU Aug 13, 2026
395e965
Add --clip_logits_epsilon=1e-5 to Gemma4-26B and Qwen3-30B logit chec…
RexBearIU Aug 13, 2026
eda946e
Fix MoE 2D vs 3D expert weight transposition bug in Qwen param mapping
RexBearIU Aug 13, 2026
91a7dbf
Enable remat_policy=full for LLaMA 3.1 70B SFT and RL scripts
RexBearIU Aug 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/maxtext/checkpoint_conversion/utils/param_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,16 @@ def reshape_kernel(input_tensor, target_shape):
else:
return input_tensor.T.reshape(target_shape)

def reshape_expert_kernel(input_tensor, target_shape=None):
"""Transposes expert weights.

3D (num_experts, in_dim, out_dim) -> (num_experts, out_dim, in_dim).
2D (in_dim, out_dim) -> (out_dim, in_dim).
"""
if input_tensor.ndim == 3:
return input_tensor.transpose(0, 2, 1)
return input_tensor.transpose(1, 0)

def reshape_bias(input_tensor, target_shape=None):
"""Reshapes biases between MaxText 2D (heads, dim) and HF 1D (hidden)."""
# saving_to_hf: MaxText [heads, head_dim] -> HF [hidden_dim] (flatten)
Expand Down Expand Up @@ -826,7 +836,7 @@ def reshape_bias(input_tensor, target_shape=None):
mapping[f"params-decoder-layers-{key}"] = reshape_bias
if num_experts > 1:
for key in moe_kernel_hooks:
mapping[f"params-decoder-layers-{key}"] = reshape_kernel
mapping[f"params-decoder-layers-{key}"] = reshape_expert_kernel
else:
for i in range(n_layers):
for key in kernel_hooks:
Expand All @@ -835,7 +845,7 @@ def reshape_bias(input_tensor, target_shape=None):
mapping[f"params-decoder-layers_{i}-{key}"] = reshape_bias
if num_experts > 1:
for key in moe_kernel_hooks:
mapping[f"params-decoder-layers_{i}-{key}"] = reshape_kernel
mapping[f"params-decoder-layers_{i}-{key}"] = reshape_expert_kernel
return mapping


Expand Down
39 changes: 34 additions & 5 deletions src/maxtext/trainers/post_train/sft/train_sft.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from absl import app
import os
import jax
import jax.numpy as jnp
import optax
import pathwaysutils

Expand Down Expand Up @@ -111,8 +112,9 @@ def create_train_step_fn(self):
def train_step(
model: nnx.Module,
optimizer: nnx.Optimizer,
grad_accumulator: Any,
inputs: Any,
grad_accumulator: Any = None,
is_update_step: Any = True,
):
inputs = gen_fn(inputs)

Expand Down Expand Up @@ -157,13 +159,40 @@ def loss_wrapper(diff_params, rest, **inputs_kw):

nnx.update(model, new_rest)

# Apply optimizer update. grads has the same nnx.State(wrt) structure
# as diff_params, which is compatible with optimizer.update.
optimizer.update(model, grads)
# Handle gradient accumulation and conditional/direct optimizer update
if (
grad_accumulator is not None
and hasattr(grad_accumulator, "add")
and hasattr(grad_accumulator, "grads")
and bool(getattr(grad_accumulator, "grads", None))
):
Comment on lines +163 to +168

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Checking bool(getattr(grad_accumulator, "grads", None)) can cause gradient accumulation to be completely bypassed. If grad_accumulator.grads is initialized to None or is empty on the first step, this condition evaluates to False. As a result, the code will fall back to the else block, directly updating the optimizer and never calling grad_accumulator.add(grads). This means gradient accumulation will be permanently disabled. To fix this, simplify the condition to only check if grad_accumulator is not None and has the add method.

      if grad_accumulator is not None and hasattr(grad_accumulator, "add"):

grad_accumulator.add(grads)

def apply_updates(model, optimizer, grad_accumulator):
acc_grads = grad_accumulator.get()
norm = optax.global_norm(jax.tree_util.tree_map(lambda x: x.astype(jnp.float32), acc_grads))
optimizer.update(model, acc_grads)
grad_accumulator.reset()
return norm

def skip_updates(model, optimizer, grad_accumulator):
return jnp.array(0.0, dtype=jnp.float32)

grad_norm = nnx.cond(
is_update_step,
apply_updates,
skip_updates,
model,
optimizer,
grad_accumulator,
)
else:
optimizer.update(model, grads)
grad_norm = optax.global_norm(jax.tree_util.tree_map(lambda x: x.astype(jnp.float32), grads))

aux_out = aux if has_aux else None
if tunix_expects_grad_norm:
return out_val, aux_out, optax.global_norm(grads)
return out_val, aux_out, grad_norm
return out_val, aux_out

return train_step
Expand Down
22 changes: 13 additions & 9 deletions src/maxtext/utils/lora_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,10 +470,18 @@ def _build_lora_provider(mt_config: pyconfig.HyperParameters) -> qwix.LoraProvid
return qwix.LoraProvider(**lora_kwargs)


def _prepare_dummy_inputs(dummy_bs: int = 1) -> tuple[jnp.ndarray, jnp.ndarray]:
"""Builds dummy decoder inputs used to materialize LoRA parameters."""
# Keep LoRA warmup as small as possible to minimize compile/memory overhead.
seq_len = 1
def _prepare_dummy_inputs(
mesh: Optional[jax.sharding.Mesh] = None,
dummy_bs: int = 1,
seq_len: int = 1,
) -> tuple[jnp.ndarray, jnp.ndarray]:
"""Builds minimal dummy decoder inputs partitioned appropriately for the mesh."""
if mesh is not None:
for axis in ("data", "fsdp", "fsdp_transpose", "expert"):
dummy_bs *= mesh.shape.get(axis, 1)
for axis in ("tensor_sequence", "context"):
seq_len *= mesh.shape.get(axis, 1)

decoder_input_tokens = jnp.zeros((dummy_bs, seq_len), dtype=jnp.int32)
decoder_positions = jnp.zeros((dummy_bs, seq_len), dtype=jnp.int32)
return decoder_input_tokens, decoder_positions
Expand Down Expand Up @@ -598,12 +606,8 @@ def apply_lora_to_model(

lora_provider = _build_lora_provider(mt_config)

dp_size = 1
if mesh is not None and "data" in mesh.shape:
dp_size = mesh.shape["data"]

model_rngs = getattr(model.decoder, "rngs", None) # pyrefly: ignore[missing-attribute]
decoder_input_tokens, decoder_positions = _prepare_dummy_inputs(dummy_bs=dp_size)
decoder_input_tokens, decoder_positions = _prepare_dummy_inputs(mesh)

lora_model = qwix.apply_lora_to_model(
model,
Expand Down
2 changes: 1 addition & 1 deletion tests/end_to_end/tpu/gemma3/4b/test_gemma3_to_mt.sh
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ if [ "${USE_MULTIMODAL}" = "false" ]; then
use_multimodal=${USE_MULTIMODAL} \
scan_layers=false \
--hf_model_path=${HF_GOLDEN_MODEL} \
--max_kl_div=0.03 \
--max_kl_div=0.05 \
--run_hf_model=true \
attention=dot_product \
hardware=cpu \
Expand Down
3 changes: 2 additions & 1 deletion tests/end_to_end/tpu/gemma4/26b/test_gemma4_to_mt.sh
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,19 @@ echo "Scanned checkpoint path: ${SCANNED_CKPT_PATH}"

# Step 3: Test whether the forward pass logits match the original HF model
# to get higher precision (eg. float32) run on CPU with `JAX_PLATFORMS=cpu`
# ToDo: improve forward_pass_logit_checker to test multi-modal prompt
if [ "${USE_MULTIMODAL}" = "false" ]; then
python3 -m tests.utils.forward_pass_logit_checker \
load_parameters_path=${UNSCANNED_CKPT_PATH} \
model_name=${MODEL_NAME} \
use_multimodal=${USE_MULTIMODAL} \
per_device_batch_size=1 \
dtype=float32 \
matmul_precision=highest \
attention=dot_product \
scan_layers=false \
--hf_model_path=${HF_GOLDEN_MODEL} \
--max_kl_div=0.05 \
--clip_logits_epsilon=1e-5 \
--run_hf_model=true \
hardware=cpu skip_jax_distributed_system=True
fi
1 change: 0 additions & 1 deletion tests/end_to_end/tpu/gpt_oss/20b/test_gpt_oss.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ python3 -m maxtext.trainers.pre_train.train \
async_checkpointing=false \
checkpoint_storage_use_zarr3=False \
checkpoint_storage_use_ocdbt=False \
ici_fsdp_parallelism=64 \
model_name=${MODEL_NAME} \
scan_layers=false \
use_multimodal=false
Expand Down
4 changes: 2 additions & 2 deletions tests/end_to_end/tpu/gpt_oss/20b/test_gpt_oss_rl.sh
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,6 @@ python3 -m maxtext.trainers.post_train.rl.train_rl \
enable_single_controller=${use_pathways} \
checkpoint_storage_use_zarr3=False \
checkpoint_storage_use_ocdbt=False \
rollout_data_parallelism=4 \
rollout_tensor_parallelism=8 \
rollout_data_parallelism=-1 \
rollout_tensor_parallelism=4 \
hbm_utilization_vllm=0.8
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ python3 -m maxtext.trainers.post_train.rl.train_rl \
num_batches=5 batch_size=1 num_test_batches=5 \
model_name=${MODEL_NAME} tokenizer_path='meta-llama/Llama-3.1-70B-Instruct' \
enable_single_controller=${use_pathways} \
remat_policy=full \
checkpoint_storage_use_zarr3=False checkpoint_storage_use_ocdbt=False \
rollout_tensor_parallelism=4 \
vllm_hf_overrides='{architectures: ["MaxTextForCausalLM"]}' \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ python3 -m maxtext.trainers.post_train.sft.train_sft \
steps=5 scan_layers=true \
model_name=${MODEL_NAME} tokenizer_path='meta-llama/Llama-3.1-70B-Instruct' \
enable_single_controller=${use_pathways} \
remat_policy=full \
checkpoint_storage_use_zarr3=False checkpoint_storage_use_ocdbt=False

# Step 3: Run inference on the checkpoint generated from the previous run
Expand Down
1 change: 1 addition & 0 deletions tests/end_to_end/tpu/qwen3/30b/test_qwen3_to_mt.sh
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ python3 -m tests.utils.forward_pass_logit_checker \
scan_layers=false \
--hf_model_path=${HF_GOLDEN_MODEL} \
--max_kl_div=0.03 \
--clip_logits_epsilon=1e-5 \
--run_hf_model=true \
attention=dot_product \
hardware=cpu skip_jax_distributed_system=True
2 changes: 1 addition & 1 deletion tests/end_to_end/tpu/qwen3/vl_2b/test_qwen3_to_mt.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ HF_GOLDEN_MODEL=Qwen/Qwen3-VL-2B-Instruct
BASE_OUTPUT_DIRECTORY=gs://runner-maxtext-logs/${MODEL_NAME}/to_maxtext

# Step 1: Install torch
python3 -m pip install torch --index-url https://download.pytorch.org/whl/cpu
python3 -m pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
python3 -m pip install decord

# Step 2: Convert to scanned multimodal checkpoint (for multimodal training)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export LOCAL_PATH=<your_local_path>/hf/${MODEL_NAME}/${idx}


# Installing torch for deps in forward_pass_logit_checker.py
python3 -m pip install torch --index-url https://download.pytorch.org/whl/cpu
python3 -m pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
python3 -m pip install decord

# Check point conversion
Expand Down
6 changes: 6 additions & 0 deletions tests/post_training/unit/lora_utils_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ def test_prepare_dummy_inputs(self):
self.assertEqual(tokens.shape, (1, 1))
self.assertEqual(positions.shape, (1, 1))

mock_mesh = mock.MagicMock()
mock_mesh.shape = {"data": 2, "fsdp": 16, "tensor_sequence": 4}
tokens, positions = lora_utils._prepare_dummy_inputs(mock_mesh)
self.assertEqual(tokens.shape, (32, 4))
self.assertEqual(positions.shape, (32, 4))

def test_verify_lora_parameters_success(self):
"""Test verification of LoRA parameters with matches and enabled LoRA."""
mock_model = mock.MagicMock()
Expand Down
22 changes: 22 additions & 0 deletions tests/post_training/unit/train_sft_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

"""Unit tests for train_sft.py."""

import inspect
import unittest
from unittest import mock
from types import SimpleNamespace
Expand All @@ -27,6 +28,8 @@
class TrainSFTTest(unittest.TestCase):
"""Tests for train_sft.py."""

# pylint: disable=protected-access

def test_validate_config_valid(self):
config = SimpleNamespace(
optimizer_memory_host_offload=False,
Expand Down Expand Up @@ -81,6 +84,25 @@ def test_train_model_caching_dense(self):
cache_nnx_graph=True,
)

def test_maxtext_peft_trainer_train_step_signature(self):
"""Test that MaxTextPeftTrainer train_step accepts Tunix args including is_update_step."""
mock_model = mock.MagicMock()

with mock.patch("flax.nnx.pop"), mock.patch("flax.nnx.split", return_value=(mock.MagicMock(), {}, {})):
trainer = mock.MagicMock()
trainer.loss_fn = mock.MagicMock()
trainer._has_aux = False
trainer.gen_model_input_fn = lambda x: x
trainer._lora_enabled = False
trainer.model = mock_model

train_step_fn = train_sft.MaxTextPeftTrainer.create_train_step_fn(trainer)

# Should accept positional and keyword args from Tunix PeftTrainer
sig = inspect.signature(train_step_fn)
params = list(sig.parameters.keys())
self.assertEqual(params, ["model", "optimizer", "grad_accumulator", "inputs", "is_update_step"])


if __name__ == "__main__":
unittest.main()
Loading