Skip to content

Commit 49cc98b

Browse files
Merge pull request #484 from AI-Hypercomputer:ninatu/fix_wan_optimizer
PiperOrigin-RevId: 988227487
2 parents 389fff5 + b8a3fba commit 49cc98b

3 files changed

Lines changed: 127 additions & 21 deletions

File tree

‎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/tests/input_pipeline_interface_test.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import pathlib
2222
import shutil
2323
import subprocess
24+
import tempfile
2425
import unittest
2526
import pytest
2627
from absl.testing import absltest
@@ -75,7 +76,8 @@ def setUp(self):
7576
def test_make_dreambooth_train_iterator(self):
7677
instance_class_gcs_dir = "gs://maxdiffusion-github-runner-test-assets/datasets/dreambooth/instance_class"
7778
class_class_gcs_dir = "gs://maxdiffusion-github-runner-test-assets/datasets/dreambooth/class_class"
78-
local_dir = "/tmp/"
79+
self.temp_dir = tempfile.TemporaryDirectory()
80+
local_dir = self.temp_dir.name
7981
instance_class_local_dir = max_utils.download_blobs(instance_class_gcs_dir, local_dir)
8082
class_class_local_dir = max_utils.download_blobs(class_class_gcs_dir, local_dir)
8183

@@ -136,6 +138,7 @@ def test_make_dreambooth_train_iterator(self):
136138

137139
cleanup(instance_class_local_dir)
138140
cleanup(class_class_local_dir)
141+
self.temp_dir.cleanup()
139142

140143
@pytest.mark.skip(
141144
"This test is deprecated and will be removed in a future version. Reason: stable diffusion 2 base is no longer in HuggingFace"

‎src/maxdiffusion/trainers/base_wan_trainer.py‎

Lines changed: 118 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
@@ -181,9 +230,11 @@ def start_training(self):
181230
with nn_partitioning.axis_rules(self.config.logical_axis_rules):
182231
pipeline, opt_state, step = self.checkpointer.load_checkpoint()
183232
restore_args = {}
184-
if opt_state and step:
185-
restore_args = {"opt_state": opt_state, "step": step}
233+
if opt_state is not None:
234+
restore_args["opt_state"] = opt_state
186235
del opt_state
236+
if step is not None:
237+
restore_args["step"] = step
187238
if self.config.enable_ssim:
188239
# Generate a sample before training to compare against generated sample after training.
189240
pretrained_video_path = generate_sample(self.config, pipeline, filename_prefix="pre-training-")
@@ -259,26 +310,76 @@ def eval(self, mesh, eval_rng_key, step, p_eval_step, state, scheduler_state, wr
259310
def training_loop(self, pipeline, optimizer, learning_rate_scheduler, train_data_iterator, restore_args: dict = {}):
260311
mesh = pipeline.mesh
261312
graphdef, params, rest_of_state = nnx.split(pipeline.transformer, nnx.Param, ...)
313+
start_step = 0
262314

263315
with mesh, nn_partitioning.axis_rules(self.config.logical_axis_rules):
264316
state = TrainState.create(
265317
apply_fn=graphdef.apply, params=params, tx=optimizer, graphdef=graphdef, rest_of_state=rest_of_state
266318
)
267-
if restore_args:
268-
step = restore_args.get("step", 0)
269-
max_logging.log(f"Restoring optimizer and resuming from step {step}")
270-
state.replace(opt_state=restore_args.get("opt_state"), step=restore_args.get("step", 0))
271-
del restore_args["opt_state"]
272-
del optimizer
273319
state = jax.tree.map(_to_array, state)
274320
state_spec = nnx.get_partition_spec(state)
275321
state = jax.lax.with_sharding_constraint(state, state_spec)
276322
state_shardings = nnx.get_named_sharding(state, mesh)
277-
if jax.process_index() == 0 and restore_args:
278-
max_logging.log("--- Optimizer State Sharding Spec (opt_state) ---")
279-
pretty_string = pprint.pformat(state_spec.opt_state, indent=4, width=60)
280-
max_logging.log(pretty_string)
281-
max_logging.log("------------------------------------------------")
323+
if restore_args.get("opt_state") is not None:
324+
max_logging.log("Restoring optimizer")
325+
resharded_opt_state = restore_optimizer_state_by_path(
326+
target_opt_shardings=state_shardings.opt_state,
327+
restored_opt_state=restore_args["opt_state"],
328+
)
329+
state = state.replace(opt_state=resharded_opt_state)
330+
del restore_args["opt_state"]
331+
if jax.process_index() == 0:
332+
max_logging.log("--- Optimizer State Sharding Spec (opt_state) ---")
333+
pretty_string = pprint.pformat(state_spec.opt_state, indent=4, width=60)
334+
max_logging.log(pretty_string)
335+
max_logging.log("------------------------------------------------")
336+
337+
if restore_args.get("step") is not None:
338+
step = restore_args["step"]
339+
# A checkpoint labelled N is written *after* iteration N completes,
340+
# and the loop starts at step 0, so it already reflects N+1 applied
341+
# updates. Resume at N+1 so that iteration N is not re-run, and so
342+
# that TrainState.step stays aligned with the optimizer's internal
343+
# step count, which actually drives the LR schedule.
344+
state = state.replace(step=step + 1)
345+
start_step = step + 1
346+
max_logging.log(f"Resuming training from step {start_step}")
347+
else:
348+
# Weights-only: no optimizer state means no schedule position.
349+
# Resetting to 0 keeps the applied LR and the logged LR consistent.
350+
if "step" in restore_args:
351+
max_logging.log(
352+
"Weights-only checkpoint detected (no opt_state); resetting step"
353+
" to 0. The LR schedule will restart from the beginning. If you"
354+
" intended to resume training, re-save with optimizer state."
355+
)
356+
weights_step = restore_args.pop("step")
357+
358+
# The loop restarts at 0 but checkpoint_dir still holds steps up to
359+
# weights_step, and Orbax will skip saving the checkpoints at those
360+
# steps. Therefore, we redirect the three output paths that
361+
# pyconfig derives from run_name, then rebuild the CheckpointManager.
362+
previous_run_name = self.config.run_name
363+
previous_checkpoint_dir = self.config.checkpoint_dir
364+
new_run_name = f"{previous_run_name}_from_weights_{weights_step}"
365+
keys = self.config.get_keys()
366+
keys["run_name"] = new_run_name
367+
for key, subdir in (
368+
("tensorboard_dir", "tensorboard"),
369+
("checkpoint_dir", "checkpoints"),
370+
):
371+
keys[key] = os.path.join(self.config.output_dir, new_run_name, subdir, "")
372+
max_logging.log(
373+
f"Redirecting outputs from run_name {previous_run_name} to"
374+
f" {new_run_name}: {previous_checkpoint_dir} already holds"
375+
f" checkpoints up to step {weights_step}, which this run would"
376+
" otherwise fail to save over."
377+
)
378+
max_logging.log(f" checkpoint_dir: {self.config.checkpoint_dir}")
379+
max_logging.log(f" tensorboard_dir: {self.config.tensorboard_dir}")
380+
self.checkpointer.checkpoint_manager.close()
381+
self.checkpointer = self._get_checkpointer()
382+
282383
if self.config.hardware != "gpu":
283384
max_utils.delete_pytree(params)
284385
data_shardings = self.get_data_shardings(mesh)
@@ -304,8 +405,6 @@ def training_loop(self, pipeline, optimizer, learning_rate_scheduler, train_data
304405

305406
rng = jax.random.key(self.config.seed)
306407
rng, eval_rng_key = jax.random.split(rng)
307-
start_step = 0
308-
last_step_completion = datetime.datetime.now()
309408
local_metrics_file = open(self.config.metrics_file, "a", encoding="utf8") if self.config.metrics_file else None
310409
running_gcs_metrics = [] if self.config.gcs_metrics else None
311410
first_profiling_step = self.config.skip_first_n_steps_for_profiler
@@ -314,9 +413,6 @@ def training_loop(self, pipeline, optimizer, learning_rate_scheduler, train_data
314413
last_profiling_step = np.clip(
315414
first_profiling_step + self.config.profiler_steps - 1, first_profiling_step, self.config.max_train_steps - 1
316415
)
317-
if restore_args.get("step", 0):
318-
max_logging.log(f"Resuming training from step {step}")
319-
start_step = restore_args.get("step", 0)
320416
per_device_tflops, _, _ = BaseWanTrainer.calculate_tflops(pipeline)
321417
scheduler_state = pipeline.scheduler_state
322418
example_batch = load_next_batch(train_data_iterator, None, self.config)
@@ -374,7 +470,10 @@ def training_loop(self, pipeline, optimizer, learning_rate_scheduler, train_data
374470
writer.flush()
375471
if self.config.save_final_checkpoint:
376472
max_logging.log(f"Saving final checkpoint for step {step}")
377-
self.checkpointer.save_checkpoint(self.config.max_train_steps - 1, pipeline, state.params)
473+
if self.config.save_optimizer:
474+
self.checkpointer.save_checkpoint(self.config.max_train_steps - 1, pipeline, state)
475+
else:
476+
self.checkpointer.save_checkpoint(self.config.max_train_steps - 1, pipeline, state.params)
378477
self.checkpointer.checkpoint_manager.wait_until_finished()
379478
# load new state for trained transformer
380479
pipeline.transformer = nnx.merge(state.graphdef, state.params, state.rest_of_state)

0 commit comments

Comments
 (0)