Skip to content

Commit a534a62

Browse files
committed
minor change to only do bf16 conversion when GA > 1
Add Zero1+GA fix train_utils.py after rebase disable shardy when GA>1 add integration test address comments fix lint fix ci parallelism and lint
1 parent 07205e2 commit a534a62

7 files changed

Lines changed: 217 additions & 14 deletions

File tree

src/MaxText/configs/base.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -859,3 +859,4 @@ subslice_shape: ""
859859

860860
# NNX
861861
enable_nnx: false
862+
shard_optimizer_over_data: False

src/MaxText/layers/decoders.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ def map_fn(path, value):
432432
# Apply remat policy to layer
433433
layer = nn.remat(
434434
block_layer,
435-
prevent_cse=not self.config.scan_layers,
435+
prevent_cse=maxtext_utils.should_prevent_cse_in_remat(self.config),
436436
policy=policy,
437437
static_argnums=(4, 5), # Deterministic and model mode are static arguments.
438438
)

src/MaxText/maxtext_utils.py

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ def get_input_data_sharding(config, mesh):
5353
return nn.logical_to_mesh_sharding(P(*config.input_data_sharding_logical_axes), mesh, config.logical_axis_rules)
5454

5555

56-
def get_functional_train_with_signature(train_step, data_sharding, state_mesh_shardings, model, config):
56+
def get_functional_train_with_signature(train_step, data_sharding, state_mesh_shardings, model, config, params_shardings=None):
5757
"""Get the shardings (both state and data) for `train_step`."""
58-
functional_train = functools.partial(train_step, model, config, state_mesh_shardings)
58+
functional_train = functools.partial(train_step, model, config, state_mesh_shardings, params_shardings)
5959
functional_train.__name__ = "train_step"
6060
in_shardings = (state_mesh_shardings, data_sharding, None) # State, batch, rng
6161
out_shardings = (state_mesh_shardings, None) # State, metrics
@@ -95,6 +95,28 @@ def get_shaped_batch(config):
9595
return shaped_batch
9696

9797

98+
def should_prevent_cse_in_remat(config):
99+
"""Determines whether to prevent common subexpression elimination (CSE) in remat.
100+
101+
CSE should not be prevented when:
102+
1. Layers are being scanned (scan_layers=True), OR
103+
2. Gradient accumulation is enabled (gradient_accumulation_steps > 1) on GPU hardware
104+
105+
Args:
106+
config: Configuration object with scan_layers, gradient_accumulation_steps, and hardware
107+
108+
Returns:
109+
bool: True if CSE should be prevented, False otherwise
110+
"""
111+
if config.scan_layers:
112+
return False
113+
114+
if config.gradient_accumulation_steps > 1 and config.hardware in ("gpu", "gpu_multiprocess"):
115+
return False
116+
117+
return True
118+
119+
98120
def load_compiled(config, partial_train, state):
99121
"""# Loading a serialized compiled train step function."""
100122

@@ -939,6 +961,97 @@ def setup_training_state(model, data_iterator, tx, config, rng, mesh, checkpoint
939961
is_training,
940962
)
941963

964+
def unbox_logicallypartioned(boxed_pytree):
965+
"""Unboxes the flax.LogicallyPartitioned pieces
966+
Args:
967+
boxed_pytree: a pytree that includes LogicallyPartitioned
968+
leaves.
969+
Returns:
970+
a pytree where all all LogicallyPartitioned leaves have been unboxed.
971+
"""
972+
return jax.tree_util.tree_map(
973+
lambda x: x.unbox() if isinstance(x, nn.spmd.LogicallyPartitioned) else x,
974+
boxed_pytree,
975+
is_leaf=lambda k: isinstance(k, nn.spmd.LogicallyPartitioned),
976+
)
977+
978+
def add_data_to_sharding(mesh, path, aval, sharding):
979+
"""Adds 'data' dimension to sharding spec if compatible and not already present.
980+
981+
This function attempts to add data parallelism to a sharding specification by finding
982+
a dimension that is divisible by the 'data' mesh axis size and doesn't conflict with
983+
existing partitioning (e.g., tensor parallelism).
984+
This function is mainly used to add data parallelism to the optimizer state for Zero-1 style sharding.
985+
986+
Args:
987+
mesh: The device mesh
988+
path: JAX tree path to the value being sharded
989+
aval: Abstract value with shape information
990+
sharding: Current NamedSharding to potentially augment
991+
992+
Returns:
993+
NamedSharding: Updated sharding with 'data' dimension added, or original if unchanged
994+
995+
Raises:
996+
AssertionError: If sharding is not NamedSharding or shape cannot be sharded
997+
"""
998+
if not isinstance(sharding, jax.sharding.NamedSharding):
999+
raise AssertionError(f"Expected NamedSharding, found {sharding} of {type(sharding)=} at {jax.tree_util.keystr(path)}")
1000+
try:
1001+
sharded_shape = sharding.shard_shape(aval.shape)
1002+
except Exception as e:
1003+
raise AssertionError(f"Could not shard value {jax.tree_util.keystr(path)} of shape={aval.shape} with {sharding=}") from e
1004+
pspec = sharding.spec
1005+
1006+
if 'data' in jax.tree.leaves(pspec):
1007+
return sharding
1008+
1009+
for idx, (size, partition) in enumerate(zip(sharded_shape, pspec)):
1010+
if partition is None:
1011+
partition = ()
1012+
1013+
if isinstance(partition, str):
1014+
partition = (partition,)
1015+
1016+
if size % mesh.shape['data'] == 0 and (partition is None or 'tensor' not in partition):
1017+
added_component = ('data',) + partition
1018+
new_pspec = jax.sharding.PartitionSpec(*(pspec[:idx] + (added_component,) + pspec[idx+1:]))
1019+
new_sharding = jax.sharding.NamedSharding(sharding.mesh, new_pspec)
1020+
return new_sharding
1021+
return sharding
1022+
1023+
def maybe_update_params_sharding_with_opt(config, state_mesh_shardings):
1024+
"""Updates parameter sharding configuration when optimizer state sharding is enabled.
1025+
1026+
When shard_optimizer_over_data is enabled (Zero-1 style sharding), this function
1027+
extracts the optimizer state shardings from the Adam optimizer's first moment (mu)
1028+
and merges them with the parameter shardings. This ensures parameter sharding is
1029+
consistent with how the optimizer state is distributed across the compute mesh.
1030+
1031+
Args:
1032+
config: Configuration object with shard_optimizer_over_data flag
1033+
state_mesh_shardings: Train state mesh shardings containing params and opt_state
1034+
1035+
Returns:
1036+
A tuple of (prev_params_shardings, updated_state_mesh_shardings):
1037+
- prev_params_shardings: Original parameter shardings before the update
1038+
- updated_state_mesh_shardings: State mesh shardings with updated params field
1039+
(unchanged if shard_optimizer_over_data is False)
1040+
"""
1041+
prev_params_shardings = state_mesh_shardings.params
1042+
if config.shard_optimizer_over_data:
1043+
if isinstance(state_mesh_shardings.opt_state, optax.ScaleByAdamState):
1044+
sharded_fp32_params = state_mesh_shardings.opt_state.mu
1045+
elif isinstance(state_mesh_shardings.opt_state, tuple) and isinstance(state_mesh_shardings.opt_state[0], optax.ScaleByAdamState):
1046+
sharded_fp32_params = state_mesh_shardings.opt_state[0].mu
1047+
else:
1048+
raise NotImplementedError(f"Could not find optimizer state shardings from optimizer of type {type(state_mesh_shardings.opt_state)}")
1049+
if "params" not in sharded_fp32_params.keys():
1050+
# When quantization=fp8 is enabled the sharded_fp32_params
1051+
# are not wrapped in `params`. Here we wrap them back.
1052+
sharded_fp32_params = {"params": sharded_fp32_params}
1053+
state_mesh_shardings = state_mesh_shardings.replace(params=dict(prev_params_shardings, **sharded_fp32_params))
1054+
return prev_params_shardings, state_mesh_shardings
9421055

9431056
def setup_initial_state(
9441057
model,
@@ -1029,6 +1142,11 @@ def get_abstract_state(model, tx, config, rng, mesh, is_training=True):
10291142
state_logical_annotations = nn.get_partition_spec(abstract_state)
10301143

10311144
state_mesh_shardings = nn.logical_to_mesh_sharding(state_logical_annotations, mesh, config.logical_axis_rules)
1145+
if is_training and config.shard_optimizer_over_data:
1146+
# Add data to sharding for optimizer state
1147+
state_mesh_shardings = state_mesh_shardings.replace(
1148+
opt_state=jax.tree.map_with_path(functools.partial(add_data_to_sharding, mesh), unbox_logicallypartioned(abstract_state).opt_state, state_mesh_shardings.opt_state)
1149+
)
10321150
if is_training and config.optimizer_memory_host_offload:
10331151
opt_state = jax.tree_util.tree_map(lambda x: x.with_memory_kind(kind="pinned_host"), state_mesh_shardings.opt_state)
10341152
state_mesh_shardings = state_mesh_shardings.replace(opt_state=opt_state)

src/MaxText/pyconfig.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,21 @@ def user_init(raw_keys):
678678
1,
679679
)
680680

681+
# Automatically disable shardy when gradient accumulation is enabled on GPU
682+
# This incompatibility is specific to GPU hardware
683+
if (
684+
raw_keys["gradient_accumulation_steps"] > 1
685+
and raw_keys["shardy"]
686+
and raw_keys["hardware"] in ("gpu", "gpu_multiprocess")
687+
):
688+
max_logging.log(
689+
"WARNING: Automatically setting shardy=False because"
690+
f" gradient_accumulation_steps={raw_keys['gradient_accumulation_steps']} > 1"
691+
f" on hardware={raw_keys['hardware']}."
692+
" Shardy is not compatible with gradient accumulation on GPU."
693+
)
694+
raw_keys["shardy"] = False
695+
681696
if raw_keys["pagedattn_max_pages_per_group"] <= 0:
682697
raw_keys["pagedattn_max_pages_per_group"] = (
683698
raw_keys["max_target_length"] + raw_keys["pagedattn_tokens_per_page"] - 1

src/MaxText/train.py

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ def loss_fn(model, config, data, dropout_rng, params, is_train=True):
190190
return loss, aux
191191

192192

193-
def train_step(model, config, state_mesh_shardings, state, data, dropout_rng):
193+
def train_step(model, config, state_mesh_shardings, params_shardings, state, data, dropout_rng):
194194
"""
195195
196196
Args:
@@ -212,12 +212,27 @@ def train_step(model, config, state_mesh_shardings, state, data, dropout_rng):
212212
extra_dpo_args = [reference_params]
213213
_loss_fn = dpo_loss_fn
214214

215+
params = state.params
216+
215217
if config.gradient_accumulation_steps > 1:
218+
# When using Zero-1 optimizer sharding, cast params to lower precision and apply sharding constraints
219+
# so that all-gather is done once in the lower precision before the gradient accumulation loop
220+
if config.shard_optimizer_over_data:
221+
def convert_to_bf16(param):
222+
if param.dtype == jnp.float32:
223+
return param.astype(jnp.bfloat16)
224+
else:
225+
return param
226+
ga_params = jax.tree_util.tree_map(convert_to_bf16, params)
227+
ga_params = jax.tree.map(jax.lax.with_sharding_constraint, ga_params, params_shardings)
228+
else:
229+
ga_params = params
216230

217231
def accumulate_gradient(acc_grad_and_loss, data):
232+
ga_params = acc_grad_and_loss["ga_params"]
218233
grad_func = jax.value_and_grad(_loss_fn, argnums=4, has_aux=True)
219234
(_, aux), cur_batch_gradient = grad_func(
220-
model, config, data, dropout_rng, state.params, *extra_dpo_args, is_train=True
235+
model, config, data, dropout_rng, ga_params, *extra_dpo_args, is_train=True
221236
)
222237
acc_grad_and_loss["loss"] += aux["total_loss"]
223238
acc_grad_and_loss["moe_lb_loss"] += aux["moe_lb_loss"]
@@ -235,8 +250,16 @@ def reshape_to_microbatch_accumulations(batch_arr):
235250
return jnp.reshape(batch_arr, microbatch_shape)
236251

237252
data = jax.tree_util.tree_map(reshape_to_microbatch_accumulations, data)
238-
init_grad = jax.tree_util.tree_map(jnp.zeros_like, state.params)
239-
init_grad_and_loss = {"loss": 0.0, "grad": init_grad, "total_weights": 0, "moe_lb_loss": 0.0, "mtp_loss": 0.0}
253+
init_grad = jax.tree_util.tree_map(jnp.zeros_like, ga_params)
254+
init_grad = jax.tree.map(jax.lax.with_sharding_constraint, init_grad, params_shardings)
255+
init_grad_and_loss = {
256+
"loss": 0.0,
257+
"grad": init_grad,
258+
"total_weights": 0,
259+
"moe_lb_loss": 0.0,
260+
"mtp_loss": 0.0,
261+
"ga_params": ga_params,
262+
}
240263

241264
grad_and_loss, aux = jax.lax.scan(
242265
accumulate_gradient, init_grad_and_loss, data, length=config.gradient_accumulation_steps
@@ -246,7 +269,10 @@ def reshape_to_microbatch_accumulations(batch_arr):
246269
+ grad_and_loss["moe_lb_loss"] / config.gradient_accumulation_steps
247270
+ grad_and_loss["mtp_loss"] / config.gradient_accumulation_steps
248271
)
249-
raw_grads = jax.tree_util.tree_map(lambda arr: arr / grad_and_loss["total_weights"], grad_and_loss["grad"])
272+
raw_grads = grad_and_loss["grad"]
273+
if config.shard_optimizer_over_data:
274+
raw_grads = jax.tree.map(jax.lax.with_sharding_constraint, raw_grads, params_shardings)
275+
raw_grads = jax.tree_util.tree_map(lambda arr: arr / grad_and_loss["total_weights"], raw_grads)
250276
aux = jax.tree.map(lambda x: jnp.sum(x, axis=0), aux) # pytype: disable=module-attr
251277
else:
252278
if config.optimizer_memory_host_offload:
@@ -255,8 +281,10 @@ def reshape_to_microbatch_accumulations(batch_arr):
255281
reference_params, max_utils.with_memory_kind(reference_params_sharding, "device")
256282
)
257283
extra_dpo_args = [reference_params]
284+
if config.shard_optimizer_over_data:
285+
params = jax.tree.map(jax.lax.with_sharding_constraint, params, params_shardings)
258286
grad_func = jax.value_and_grad(_loss_fn, argnums=4, has_aux=True)
259-
(loss, aux), raw_grads = grad_func(model, config, data, dropout_rng, state.params, *extra_dpo_args, is_train=True)
287+
(loss, aux), raw_grads = grad_func(model, config, data, dropout_rng, params, *extra_dpo_args, is_train=True)
260288

261289
raw_grads = jax.tree_util.tree_map(lambda x: x.astype(config.grad_dtype) if x.dtype == jnp.float32 else x, raw_grads)
262290
intermediate_outputs = aux["intermediate_outputs"]
@@ -373,12 +401,15 @@ def train_loop(config, recorder, state=None):
373401
state = _merge_dpo_state(state, reference_params)
374402
state_mesh_shardings = _merge_dpo_state(state_mesh_shardings, state_mesh_shardings.params["params"])
375403

404+
params_shardings, state_mesh_shardings = maxtext_utils.maybe_update_params_sharding_with_opt(config, state_mesh_shardings)
405+
376406
p_train_step, p_eval_step = train_utils.jit_train_and_eval_step(
377-
config, model, mesh, state, state_mesh_shardings, train_step, eval_step, eval_data_iterator
407+
config, model, mesh, state, state_mesh_shardings, train_step, eval_step, eval_data_iterator, params_shardings
378408
)
379409

380410
with mesh, nn_partitioning.axis_rules(config.logical_axis_rules):
381411
shaped_batch = maxtext_utils.get_shaped_batch(config)
412+
state = jax.lax.with_sharding_constraint(state, state_mesh_shardings)
382413
compiled = p_train_step.lower(state, shaped_batch, init_rng).compile()
383414
compiled_stats = compiled.memory_analysis()
384415
max_utils.print_compiled_memory_stats(compiled_stats)
@@ -402,6 +433,7 @@ def train_loop(config, recorder, state=None):
402433
nextrng = jax.jit(jax.random.fold_in)(init_rng, step)
403434
with maybe_record_goodput(recorder, GoodputEvent.STEP, step):
404435
with mesh, nn_partitioning.axis_rules(config.logical_axis_rules):
436+
state = jax.lax.with_sharding_constraint(state, state_mesh_shardings)
405437
state, metrics = p_train_step(state, example_batch, nextrng)
406438

407439
step_time_delta = datetime.datetime.now() - last_step_completion
@@ -474,9 +506,9 @@ def initialize(argv: Sequence[str]) -> tuple[pyconfig.HyperParameters, Any, Any]
474506
# TODO: mazumdera@ : ensure missing mandatory fields in base.yml are filled in in argv,
475507
# or fill in here
476508
config = pyconfig.initialize(argv)
477-
jax.config.update("jax_use_shardy_partitioner", config.shardy)
478509
max_utils.print_system_information()
479510
validate_train_config(config)
511+
jax.config.update("jax_use_shardy_partitioner", config.shardy)
480512
os.environ["TFDS_DATA_DIR"] = config.dataset_path or ""
481513
vertex_tensorboard_manager = VertexTensorboardManager()
482514
if config.use_vertex_tensorboard or os.environ.get("UPLOAD_DATA_TO_TENSORBOARD"):

src/MaxText/train_utils.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,19 @@ def create_training_tools(config, model, mesh):
7676
return init_rng, checkpoint_manager, learning_rate_schedule, tx
7777

7878

79-
def jit_train_step(config, model, state, state_mesh_shardings, data_sharding, train_step):
79+
def jit_train_step(
80+
config, model, state, state_mesh_shardings, data_sharding, train_step, params_shardings
81+
):
8082
"""Returns a JIT-compiled train step function, which is loaded from a file if specified in the config."""
8183
(
8284
functional_train,
8385
in_shardings,
8486
out_shardings,
8587
static_argnums,
8688
donate_argnums,
87-
) = maxtext_utils.get_functional_train_with_signature(train_step, data_sharding, state_mesh_shardings, model, config)
89+
) = maxtext_utils.get_functional_train_with_signature(
90+
train_step, data_sharding, state_mesh_shardings, model, config, params_shardings
91+
)
8892

8993
# Define the compilation of functional_train, either by loading the compiled version or wrapping a new one in a jit
9094
if config.compiled_trainstep_file != "":
@@ -136,10 +140,13 @@ def jit_train_and_eval_step(
136140
train_step,
137141
eval_step=None,
138142
eval_data_iterator=None,
143+
params_shardings=None,
139144
):
140145
"""Returns a JIT-compiled train and eval step function."""
141146
data_sharding = maxtext_utils.get_input_data_sharding(config, mesh)
142-
p_train_step = jit_train_step(config, model, state, state_mesh_shardings, data_sharding, train_step)
147+
p_train_step = jit_train_step(
148+
config, model, state, state_mesh_shardings, data_sharding, train_step, params_shardings
149+
)
143150
p_eval_step = None
144151
if eval_data_iterator:
145152
p_eval_step = jit_eval_step(config, model, state_mesh_shardings, data_sharding, eval_step)

tests/integration_tests/train_tests.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,36 @@ def test_base_model_shardy_false(self):
347347
def test_gpu_synthetic_model_ag_once(self):
348348
train_main(TrainTests.CONFIGS["synthetic"] + ["model_fsdp_ag_once=True"])
349349

350+
@pytest.mark.integration_test
351+
@pytest.mark.gpu_only
352+
def test_gpu_zero1_gradient_accumulation(self):
353+
os.environ["NVTE_FUSED_ATTN"] = "1" # Enable fused attention
354+
zero1_ga = [ # tests Zero-1 optimizer sharding with gradient accumulation
355+
None,
356+
os.path.join(MAXTEXT_PKG_DIR, "configs", "base.yml"),
357+
"base_output_directory=gs://runner-maxtext-logs",
358+
"run_name=runner_test",
359+
"dataset_path=gs://maxtext-dataset",
360+
"steps=10",
361+
"enable_checkpointing=False",
362+
"enable_goodput_recording=False",
363+
"dataset_type=synthetic",
364+
"attention=cudnn_flash_te",
365+
"remat_policy=minimal",
366+
"scan_layers=False",
367+
"max_target_length=8192",
368+
"per_device_batch_size=2",
369+
"ici_data_parallelism=-1",
370+
"dcn_data_parallelism=1",
371+
"ici_fsdp_parallelism=1",
372+
"dcn_fsdp_parallelism=1",
373+
"gradient_accumulation_steps=8",
374+
"shard_optimizer_over_data=True",
375+
"override_model_config=True",
376+
rf"tokenizer_path={os.path.join(MAXTEXT_ASSETS_ROOT, 'tokenizer.llama2')}",
377+
]
378+
train_main(zero1_ga)
379+
350380

351381
if __name__ == "__main__":
352382
absltest.main()

0 commit comments

Comments
 (0)