Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
309 changes: 54 additions & 255 deletions src/maxtext/experimental/rl/grpo_trainer.py

Large diffs are not rendered by default.

702 changes: 244 additions & 458 deletions src/maxtext/trainers/pre_train/train.py

Large diffs are not rendered by default.

90 changes: 31 additions & 59 deletions src/maxtext/trainers/pre_train/train_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,8 @@
import jax.numpy as jnp
from jax.sharding import AxisType, Mesh
from maxtext.common import train_state_nnx
from maxtext.common.common_types import MODEL_MODE_TRAIN, ShardMode
from maxtext.common.common_types import ShardMode
from maxtext.configs import pyconfig
from maxtext.layers import quantizations
from maxtext.models import models
from maxtext.optimizers import optimizers
from maxtext.trainers.diloco import diloco
Expand Down Expand Up @@ -130,68 +129,49 @@ def _nnx_forward(decoder_input_tokens, decoder_positions, decoder_segment_ids):
def get_shaped_inputs(topology_mesh, config):
"""Get shaped abstractions of inputs to train_step: state, batch and rng"""
# Construct the model and optimizer to get shaped versions of the state
quant = quantizations.configure_quantization(config)
if config.pure_nnx:
_create_model_partial, model = model_creation_utils.create_nnx_abstract_model(config, topology_mesh)
else:
model = Transformer(config, topology_mesh, quant=quant, model_mode=MODEL_MODE_TRAIN)
_create_model_partial, model = model_creation_utils.create_nnx_abstract_model(config, topology_mesh)
# The learning_rate_schedule is baked into the compiled object.
learning_rate_schedule = maxtext_utils.create_learning_rate_schedule(config)
# pass in model for muon
tx = optimizers.get_optimizer(config, learning_rate_schedule, model)

# Shaped RNG keys
_, example_rng = jax.random.split(jax.random.PRNGKey(0), 2)
shaped_rng = jax.ShapeDtypeStruct(example_rng.shape, example_rng.dtype)

if config.pure_nnx:

def create_train_state_fn():
nnx_model = _create_model_partial()
wrt = (
getattr(nnx, "LoRAParam", nnx.Param)
if getattr(getattr(config, "lora", None), "enable_lora", False)
else nnx.Param
)
optimizer = nnx.Optimizer(nnx_model, tx, wrt=wrt)
return train_state_nnx.TrainStateNNX(nnx_model, optimizer)
def create_train_state_fn():
nnx_model = _create_model_partial()
wrt = (
getattr(nnx, "LoRAParam", nnx.Param)
if getattr(getattr(config, "lora", None), "enable_lora", False)
else nnx.Param
)
optimizer = nnx.Optimizer(nnx_model, tx, wrt=wrt)
return train_state_nnx.TrainStateNNX(nnx_model, optimizer)

init_state_fn = create_train_state_fn
else:
init_state_fn = functools.partial(maxtext_utils.init_initial_state, model, tx, config, True, example_rng)
init_state_fn = create_train_state_fn

# Shaped state
abstract_state, _, state_mesh_shardings = maxtext_utils.get_abstract_state(config, topology_mesh, init_state_fn, True)

if config.pure_nnx:
# NNX doesn't use Linen logical annotations; derive PartitionSpecs from the physical shardings.
logical_annotations = maxtext_utils_nnx.get_partition_spec_nnx(state_mesh_shardings)
# For NNX, get_functional_train_with_signature expects the graphdef (static structure),
# not the raw model — mirroring how the training loop does nnx.split(train_state).
with nn_partitioning.axis_rules(config.logical_axis_rules):
abs_train_state = nnx.eval_shape(init_state_fn)
graphdef, _ = nnx.split(abs_train_state)
model = graphdef
else:
# unsharded logical annotations
logical_annotations = maxtext_utils.get_logical_annotations(config, topology_mesh, init_state_fn)
# NNX doesn't use Linen logical annotations; derive PartitionSpecs from the physical shardings.
logical_annotations = maxtext_utils_nnx.get_partition_spec_nnx(state_mesh_shardings)
# For NNX, get_functional_train_with_signature expects the graphdef (static structure),
# not the raw model — mirroring how the training loop does nnx.split(train_state).
with nn_partitioning.axis_rules(config.logical_axis_rules):
abs_train_state = nnx.eval_shape(init_state_fn)
graphdef, _ = nnx.split(abs_train_state)
model = graphdef

# Shaped batch
data_sharding = sharding.get_input_data_sharding(config, topology_mesh)
shaped_batch = maxtext_utils.get_shaped_batch(config, batch_sharding=data_sharding)

if config.pure_nnx:
shaped_train_args = (
abstract_state,
shaped_batch,
) # NNX doesn't use dropout_rng
else:
shaped_train_args = (abstract_state, shaped_batch, shaped_rng)
shaped_train_args = (
abstract_state,
shaped_batch,
) # NNX doesn't use dropout_rng
shaped_train_kwargs = {}

# Collect NNX activation shardings via an abstract forward pass (must run
# after get_abstract_state, which only traces __init__).
if config.debug_sharding and config.pure_nnx:
if config.debug_sharding:
_collect_nnx_activation_shardings(_create_model_partial, config, topology_mesh) # pyrefly: ignore[unbound-name]

return (
Expand Down Expand Up @@ -392,20 +372,12 @@ def main(argv: Sequence[str]) -> None:
# print weights sharding info under debug sharding mode
if config.debug_sharding:
max_utils.print_non_trivial_mesh_axis(topology_mesh)
if config.pure_nnx:
maxtext_utils.print_shardings_params(
shaped_train_args[0],
state_mesh_shardings,
topology_mesh,
logical_annotations,
)
else:
maxtext_utils.print_shardings_params(
shaped_train_args[0].params,
state_mesh_shardings.params,
topology_mesh,
logical_annotations.params,
)
maxtext_utils.print_shardings_params(
shaped_train_args[0],
state_mesh_shardings,
topology_mesh,
logical_annotations,
)

# Compile
print("Jitting and compiling train step...", flush=True)
Expand Down
18 changes: 9 additions & 9 deletions tests/assets/golden_logits/golden_dpo_correctness.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@
"explicit_prompt_len_3_column": {
"loss_step_1": 0.6931471824645996,
"margin_step_1": 0.0,
"loss": 0.6573728919029236,
"margin": 0.0728759765625,
"chosen_logps": -854.4208984375,
"rejected_logps": -530.0099487304688
"loss": 0.690365,
"margin": 0.005573,
"chosen_logps": -882.7361450195312,
"rejected_logps": -587.572693
},
"default_prompt_len_2_column": {
"loss_step_1": 0.6931471824645996,
"margin_step_1": 0.0,
"loss": 0.6954630613327026,
"margin": -0.004626465495675802,
"chosen_logps": -875.7848510742188,
"rejected_logps": -502.052978515625
"loss": 0.620875,
"margin": 0.150177,
"chosen_logps": -907.599609375,
"rejected_logps": -578.226013
}
}
}
93 changes: 25 additions & 68 deletions tests/assets/logits_generation/generate_grpo_golden_logits.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,17 @@
import unittest

from datasets import load_dataset
from flax import linen as nn
from flax import nnx
import jax
import jax.numpy as jnp
from jax.sharding import Mesh
import jsonlines
from maxtext.configs import pyconfig, types
from maxtext.utils.globals import MAXTEXT_PKG_DIR, MAXTEXT_TEST_ASSETS_ROOT
from maxtext.common.common_types import Array, MODEL_MODE_TRAIN
from maxtext.experimental.rl.grpo_trainer import _merge_grpo_state, generate_completions, grpo_loss_fn, grpo_loss_fn_nnx
from maxtext.experimental.rl.grpo_utils import compute_log_probs, compute_log_probs_nnx
from maxtext.common.common_types import Array
from maxtext.experimental.rl.grpo_trainer import generate_completions, grpo_loss_fn_nnx
from maxtext.experimental.rl.grpo_utils import compute_log_probs_nnx
from maxtext.inference.maxengine import maxengine
from maxtext.models import models
from maxtext.utils import maxtext_utils
from maxtext.utils import model_creation_utils
from tests.post_training.integration.grpo_trainer_correctness_test import prepare_maxtext_inputs
Expand All @@ -49,40 +47,28 @@


def _setup_model(config, mesh, rng):
"""Builds the model, and for NNX a frozen reference clone, dispatching on pure_nnx.
"""Builds the model and a frozen reference clone.

Returns (model, reference_model, state). For NNX the model carries its own params
(from_pretrained loads the checkpoint or inits) and state is None; for Linen the
model is a ToLinen module with a separate decode state.
Returns (model, reference_model). The model carries its own params (from_pretrained
loads the checkpoint or inits) and the reference is a clone of the policy.
"""
if config.pure_nnx:
model = model_creation_utils.from_pretrained(config, mesh=mesh, rng_key=rng)
return model, nnx.clone(model), None
model = models.transformer_as_linen(config=config, mesh=mesh, quant=None, model_mode=MODEL_MODE_TRAIN)
init_state_fn = functools.partial(maxtext_utils.init_initial_state, model, None, config, False, rng)
state, state_mesh_annotations = maxtext_utils.setup_decode_state(config, mesh, None, init_state_fn)
return model, None, (state, state_mesh_annotations)
model = model_creation_utils.from_pretrained(config, mesh=mesh, rng_key=rng)
return model, nnx.clone(model)


def _logps(config, model, state, ids, pos, seg, comp_seg):
"""Policy per-token log-probs, dispatching between NNX and Linen."""
if config.pure_nnx:
return compute_log_probs_nnx(model, ids, pos, seg, comp_seg, config, is_train=False)
return compute_log_probs(model, state.params, ids, pos, seg, comp_seg, config, is_train=False)
def _logps(config, model, ids, pos, seg, comp_seg):
"""Policy per-token log-probs."""
return compute_log_probs_nnx(model, ids, pos, seg, comp_seg, config, is_train=False)


def _reference_logps(config, model, reference_model, reference_params, ids, pos, seg, comp_seg):
"""Reference per-token log-probs. NNX uses the cloned reference model; Linen uses the saved params."""
if config.pure_nnx:
return compute_log_probs_nnx(reference_model, ids, pos, seg, comp_seg, config, is_train=False)
return compute_log_probs(model, {"params": reference_params}, ids, pos, seg, comp_seg, config, is_train=False)
def _reference_logps(config, reference_model, ids, pos, seg, comp_seg):
"""Reference per-token log-probs, using the cloned reference model."""
return compute_log_probs_nnx(reference_model, ids, pos, seg, comp_seg, config, is_train=False)


def _grpo_loss(config, model, reference_model, state, reference_params, data, rng):
"""GRPO loss, dispatching between NNX (reference model) and Linen (reference params)."""
if config.pure_nnx:
return grpo_loss_fn_nnx(model, config, data, rng, None, reference_model)
return grpo_loss_fn(model, config, data, rng, state.params, reference_params)
def _grpo_loss(config, model, reference_model, data, rng):
"""GRPO loss, using the cloned reference model."""
return grpo_loss_fn_nnx(model, config, data, rng, None, reference_model)


class GRPOTest(unittest.TestCase):
Expand Down Expand Up @@ -116,19 +102,12 @@ def setUp(self):
mesh = Mesh(devices_array, self.cfg.mesh_axes)
self.mesh = mesh
# With checkpoint
self.model, self.reference_model, linen_state = _setup_model(self.cfg, mesh, self.rng)
if self.cfg.pure_nnx:
self.state = None
self.state_mesh_shardings = None # NNX param shardings are derived in the generation step.
else:
self.state, state_mesh_annotations = linen_state
self.state_mesh_shardings = nn.logical_to_mesh_sharding(state_mesh_annotations, mesh, self.cfg.logical_axis_rules)
self.model, self.reference_model = _setup_model(self.cfg, mesh, self.rng)
self.data_sharding = jax.NamedSharding(mesh, jax.sharding.PartitionSpec(None))
# Without checkpoint
self.model_no_ckpt_loading, self.reference_model_no_ckpt_loading, linen_state_no_ckpt = _setup_model(
self.model_no_ckpt_loading, self.reference_model_no_ckpt_loading = _setup_model(
self.cfg_no_ckpt_loading, mesh, self.rng
)
self.state_no_ckpt_loading = None if self.cfg_no_ckpt_loading.pure_nnx else linen_state_no_ckpt[0]

self.tokenizer_model = transformers.AutoTokenizer.from_pretrained(
"meta-llama/Llama-3.1-8B",
Expand Down Expand Up @@ -217,43 +196,28 @@ def test_w_trl_and_write_golden_data(self):
self.cfg.prompt, self.tokenizer_model
)
maxtext_per_token_logps, _ = _logps(
self.cfg, self.model, self.state, input_ids, input_position, input_segmentation, completion_segmentation
self.cfg, self.model, input_ids, input_position, input_segmentation, completion_segmentation
)

# The reference is a frozen copy of the step-0 policy. NNX holds it as a cloned
# model (built in setUp); Linen snapshots the params and merges them into the state.
reference_params = None
reference_params_no_ckpt_loading = None
if not self.cfg.pure_nnx:
reference_params = jax.tree.map(jnp.copy, self.state.params["params"])
self.state = _merge_grpo_state(self.state, reference_params)
if not self.cfg_no_ckpt_loading.pure_nnx:
reference_params_no_ckpt_loading = jax.tree.map(jnp.copy, self.state_no_ckpt_loading.params["params"])
self.state_no_ckpt_loading = _merge_grpo_state(self.state_no_ckpt_loading, reference_params_no_ckpt_loading)

data = {
"prompt_completions": input_ids,
"prompt_completions_position": input_position,
"prompt_completions_segmentation": input_segmentation,
"ar_completions_segmentation": completion_segmentation,
}
maxtext_loss, aux = _grpo_loss(
self.cfg, self.model, self.reference_model, self.state, reference_params, data, self.rng
)
maxtext_loss, aux = _grpo_loss(self.cfg, self.model, self.reference_model, data, self.rng)
# pylint: disable=protected-access
self.assertEqual(self.trainer._metrics["train"]["kl"][0], aux.avg_kl.tolist())
self.assertEqual(hf_loss.item(), maxtext_loss.tolist())
# since this is on-policy
self.assertEqual(aux.avg_advantage.tolist(), 0.0)
# since we are at step 0
maxtext_per_token_logps, _ = _logps(
self.cfg, self.model, self.state, input_ids, input_position, input_segmentation, completion_segmentation
self.cfg, self.model, input_ids, input_position, input_segmentation, completion_segmentation
)
maxtext_per_token_logps_ref, _ = _reference_logps(
self.cfg,
self.model,
self.reference_model,
reference_params,
input_ids,
input_position,
input_segmentation,
Expand All @@ -274,7 +238,6 @@ def test_w_trl_and_write_golden_data(self):
maxtext_per_token_logps_no_ckpt_loading, _ = _logps(
self.cfg_no_ckpt_loading,
self.model_no_ckpt_loading,
self.state_no_ckpt_loading,
input_ids,
input_position,
input_segmentation,
Expand All @@ -285,8 +248,6 @@ def test_w_trl_and_write_golden_data(self):
self.cfg_no_ckpt_loading,
self.model_no_ckpt_loading,
self.reference_model_no_ckpt_loading,
self.state_no_ckpt_loading,
reference_params_no_ckpt_loading,
data,
self.rng,
)
Expand All @@ -301,13 +262,9 @@ def test_w_trl_and_write_golden_data(self):
)
prompt_true_length = jnp.array([len(prompt_tokens)] * 4)
engine_data = {"prompt": prompt, "prompt_true_length": prompt_true_length}
if self.cfg_no_ckpt_loading.pure_nnx:
# NNX params live on the model; the inference engine is NNX-aware (config.pure_nnx).
gen_params = nnx.state(self.model_no_ckpt_loading, nnx.Param)
gen_param_shardings = jax.tree.map(lambda _: jax.NamedSharding(self.mesh, jax.sharding.PartitionSpec()), gen_params)
else:
gen_params = {"params": self.state_no_ckpt_loading.params["params"]}
gen_param_shardings = self.state_mesh_shardings.params
# Params live on the model; the inference engine reads them directly.
gen_params = nnx.state(self.model_no_ckpt_loading, nnx.Param)
gen_param_shardings = jax.tree.map(lambda _: jax.NamedSharding(self.mesh, jax.sharding.PartitionSpec()), gen_params)
p_generate_completions: Callable[[dict, dict, Array], Array] = jax.jit(
functools.partial(generate_completions, self.cfg, self.tokenizer_model, engine),
in_shardings=(self.data_sharding, gen_param_shardings, None),
Expand Down
Loading
Loading