Skip to content

Commit bb66331

Browse files
committed
Fix optimizer state restoration and multi-host sharding in WAN trainer
- Fix no-op state.replace(...) when restoring opt_state and step by reassigning state. - Add restore_optimizer_state_by_path to normalize and align Orbax checkpoint paths with live Optax PyTree structure. - Update max_utils.device_put_replicated to convert to NumPy host arrays before callback slicing to avoid multi-host dispatch failures, and fast-path matching shardings.
1 parent a4ff3ae commit bb66331

3 files changed

Lines changed: 99 additions & 23 deletions

File tree

‎src/maxdiffusion/generate_ltx_video.py‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,9 +213,7 @@ def run(config):
213213

214214
for prompt_idx, current_prompt in enumerate(prompts):
215215
prompt_word_count = len(current_prompt.split())
216-
enhance_prompt = (
217-
prompt_enhancement_words_threshold > 0 and prompt_word_count < prompt_enhancement_words_threshold
218-
)
216+
enhance_prompt = prompt_enhancement_words_threshold > 0 and prompt_word_count < prompt_enhancement_words_threshold
219217

220218
s0 = time.perf_counter()
221219
images = pipeline(

‎src/maxdiffusion/max_utils.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,9 +503,13 @@ def device_put_replicated(x, sharding):
503503
504504
to also shard an array based on sharding.
505505
"""
506+
if isinstance(x, jax.Array) and hasattr(x, "sharding") and x.sharding == sharding:
507+
return x
508+
506509
arr = getattr(x, "value", x)
510+
arr_np = np.asarray(arr)
507511
shd = getattr(sharding, "value", sharding)
508-
res = jax.make_array_from_callback(arr.shape, shd, lambda index: arr[index])
512+
res = jax.make_array_from_callback(arr_np.shape, shd, lambda index: arr_np[index])
509513
if hasattr(x, "set_value"):
510514
x.set_value(res)
511515
return x

‎src/maxdiffusion/trainers/base_wan_trainer.py‎

Lines changed: 93 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import os
2121
import pprint
2222
import threading
23+
from typing import Any
2324
from flax import nnx
2425
from flax.linen import partitioning as nn_partitioning
2526
from flax.training import train_state
@@ -49,6 +50,54 @@ def _to_array(x):
4950
return x
5051

5152

53+
def _normalize_path(path: tuple[Any, ...]) -> tuple[Any, ...]:
54+
"""Normalizes a PyTree path tuple for optimizer state matching."""
55+
normalized = []
56+
for p in path:
57+
if hasattr(p, "key"):
58+
val = p.key
59+
elif hasattr(p, "idx"):
60+
val = p.idx
61+
elif hasattr(p, "name"):
62+
val = p.name
63+
else:
64+
val = p
65+
66+
if isinstance(val, str) and val.isdigit():
67+
val = int(val)
68+
normalized.append(val)
69+
70+
if normalized and normalized[-1] == "value":
71+
normalized = normalized[:-1]
72+
73+
return tuple(normalized)
74+
75+
76+
def restore_optimizer_state_by_path(
77+
target_opt_shardings: Any,
78+
restored_opt_state: Any,
79+
) -> Any:
80+
"""Restores optimizer state by matching exact parameter paths and applying sharding."""
81+
raw_restored_map, _ = jax.tree_util.tree_flatten_with_path(restored_opt_state)
82+
83+
normalized_restored_map = {}
84+
for raw_path, val in raw_restored_map:
85+
norm_path = _normalize_path(raw_path)
86+
normalized_restored_map[norm_path] = val
87+
88+
def _restore_leaf(path, target_sharding):
89+
norm_path = _normalize_path(path)
90+
if norm_path not in normalized_restored_map:
91+
raise KeyError(f"Optimizer checkpoint missing path: {norm_path}.")
92+
val = normalized_restored_map[norm_path]
93+
return max_utils.device_put_replicated(val, target_sharding)
94+
95+
return jax.tree_util.tree_map_with_path(
96+
_restore_leaf,
97+
target_opt_shardings,
98+
)
99+
100+
52101
def generate_sample(config, pipeline, filename_prefix):
53102
"""
54103
Generates a video to validate training did not corrupt the model
@@ -178,9 +227,11 @@ def start_training(self):
178227
with nn_partitioning.axis_rules(self.config.logical_axis_rules):
179228
pipeline, opt_state, step = self.checkpointer.load_checkpoint()
180229
restore_args = {}
181-
if opt_state and step:
182-
restore_args = {"opt_state": opt_state, "step": step}
230+
if opt_state is not None:
231+
restore_args["opt_state"] = opt_state
183232
del opt_state
233+
if step is not None:
234+
restore_args["step"] = step
184235
if self.config.enable_ssim:
185236
# Generate a sample before training to compare against generated sample after training.
186237
pretrained_video_path = generate_sample(self.config, pipeline, filename_prefix="pre-training-")
@@ -256,26 +307,51 @@ def eval(self, mesh, eval_rng_key, step, p_eval_step, state, scheduler_state, wr
256307
def training_loop(self, pipeline, optimizer, learning_rate_scheduler, train_data_iterator, restore_args: dict = {}):
257308
mesh = pipeline.mesh
258309
graphdef, params, rest_of_state = nnx.split(pipeline.transformer, nnx.Param, ...)
310+
start_step = 0
259311

260312
with mesh, nn_partitioning.axis_rules(self.config.logical_axis_rules):
261313
state = TrainState.create(
262314
apply_fn=graphdef.apply, params=params, tx=optimizer, graphdef=graphdef, rest_of_state=rest_of_state
263315
)
264-
if restore_args:
265-
step = restore_args.get("step", 0)
266-
max_logging.log(f"Restoring optimizer and resuming from step {step}")
267-
state.replace(opt_state=restore_args.get("opt_state"), step=restore_args.get("step", 0))
268-
del restore_args["opt_state"]
269-
del optimizer
270316
state = jax.tree.map(_to_array, state)
271317
state_spec = nnx.get_partition_spec(state)
272318
state = jax.lax.with_sharding_constraint(state, state_spec)
273319
state_shardings = nnx.get_named_sharding(state, mesh)
274-
if jax.process_index() == 0 and restore_args:
275-
max_logging.log("--- Optimizer State Sharding Spec (opt_state) ---")
276-
pretty_string = pprint.pformat(state_spec.opt_state, indent=4, width=60)
277-
max_logging.log(pretty_string)
278-
max_logging.log("------------------------------------------------")
320+
if restore_args.get("opt_state") is not None:
321+
max_logging.log("Restoring optimizer")
322+
resharded_opt_state = restore_optimizer_state_by_path(
323+
target_opt_shardings=state_shardings.opt_state,
324+
restored_opt_state=restore_args["opt_state"],
325+
)
326+
state = state.replace(opt_state=resharded_opt_state)
327+
del restore_args["opt_state"]
328+
if jax.process_index() == 0:
329+
max_logging.log("--- Optimizer State Sharding Spec (opt_state) ---")
330+
pretty_string = pprint.pformat(state_spec.opt_state, indent=4, width=60)
331+
max_logging.log(pretty_string)
332+
max_logging.log("------------------------------------------------")
333+
334+
if restore_args.get("step") is not None:
335+
step = restore_args["step"]
336+
# A checkpoint labelled N is written *after* iteration N completes,
337+
# and the loop starts at step 0, so it already reflects N+1 applied
338+
# updates. Resume at N+1 so that iteration N is not re-run, and so
339+
# that TrainState.step stays aligned with the optimizer's internal
340+
# step count, which actually drives the LR schedule.
341+
state = state.replace(step=step + 1)
342+
start_step = step + 1
343+
max_logging.log(f"Resuming training from step {start_step}")
344+
else:
345+
# Weights-only: no optimizer state means no schedule position.
346+
# Resetting to 0 keeps the applied LR and the logged LR consistent.
347+
if "step" in restore_args:
348+
max_logging.log(
349+
"Weights-only checkpoint detected (no opt_state); resetting step"
350+
" to 0. The LR schedule will restart from the beginning. If you"
351+
" intended to resume training, re-save with optimizer state."
352+
)
353+
del restore_args["step"]
354+
279355
if self.config.hardware != "gpu":
280356
max_utils.delete_pytree(params)
281357
data_shardings = self.get_data_shardings(mesh)
@@ -301,8 +377,6 @@ def training_loop(self, pipeline, optimizer, learning_rate_scheduler, train_data
301377

302378
rng = jax.random.key(self.config.seed)
303379
rng, eval_rng_key = jax.random.split(rng)
304-
start_step = 0
305-
last_step_completion = datetime.datetime.now()
306380
local_metrics_file = open(self.config.metrics_file, "a", encoding="utf8") if self.config.metrics_file else None
307381
running_gcs_metrics = [] if self.config.gcs_metrics else None
308382
first_profiling_step = self.config.skip_first_n_steps_for_profiler
@@ -311,9 +385,6 @@ def training_loop(self, pipeline, optimizer, learning_rate_scheduler, train_data
311385
last_profiling_step = np.clip(
312386
first_profiling_step + self.config.profiler_steps - 1, first_profiling_step, self.config.max_train_steps - 1
313387
)
314-
if restore_args.get("step", 0):
315-
max_logging.log(f"Resuming training from step {step}")
316-
start_step = restore_args.get("step", 0)
317388
per_device_tflops, _, _ = BaseWanTrainer.calculate_tflops(pipeline)
318389
scheduler_state = pipeline.scheduler_state
319390
example_batch = load_next_batch(train_data_iterator, None, self.config)
@@ -367,7 +438,10 @@ def training_loop(self, pipeline, optimizer, learning_rate_scheduler, train_data
367438
writer.flush()
368439
if self.config.save_final_checkpoint:
369440
max_logging.log(f"Saving final checkpoint for step {step}")
370-
self.checkpointer.save_checkpoint(self.config.max_train_steps - 1, pipeline, state.params)
441+
if self.config.save_optimizer:
442+
self.checkpointer.save_checkpoint(self.config.max_train_steps - 1, pipeline, state)
443+
else:
444+
self.checkpointer.save_checkpoint(self.config.max_train_steps - 1, pipeline, state.params)
371445
self.checkpointer.checkpoint_manager.wait_until_finished()
372446
# load new state for trained transformer
373447
pipeline.transformer = nnx.merge(state.graphdef, state.params, state.rest_of_state)

0 commit comments

Comments
 (0)