2020import os
2121import pprint
2222import threading
23+ from typing import Any
2324from flax import nnx
2425from flax .linen import partitioning as nn_partitioning
2526from 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+
52101def 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,76 @@ 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+ weights_step = restore_args .pop ("step" )
354+
355+ # The loop restarts at 0 but checkpoint_dir still holds steps up to
356+ # weights_step, and Orbax will skip saving the checkpoints at those
357+ # steps. Therefore, we redirect the three output paths that
358+ # pyconfig derives from run_name, then rebuild the CheckpointManager.
359+ previous_run_name = self .config .run_name
360+ previous_checkpoint_dir = self .config .checkpoint_dir
361+ new_run_name = f"{ previous_run_name } _from_weights_{ weights_step } "
362+ keys = self .config .get_keys ()
363+ keys ["run_name" ] = new_run_name
364+ for key , subdir in (
365+ ("tensorboard_dir" , "tensorboard" ),
366+ ("checkpoint_dir" , "checkpoints" ),
367+ ):
368+ keys [key ] = os .path .join (self .config .output_dir , new_run_name , subdir , "" )
369+ max_logging .log (
370+ f"Redirecting outputs from run_name { previous_run_name } to"
371+ f" { new_run_name } : { previous_checkpoint_dir } already holds"
372+ f" checkpoints up to step { weights_step } , which this run would"
373+ " otherwise fail to save over."
374+ )
375+ max_logging .log (f" checkpoint_dir: { self .config .checkpoint_dir } " )
376+ max_logging .log (f" tensorboard_dir: { self .config .tensorboard_dir } " )
377+ self .checkpointer .checkpoint_manager .close ()
378+ self .checkpointer = self ._get_checkpointer ()
379+
279380 if self .config .hardware != "gpu" :
280381 max_utils .delete_pytree (params )
281382 data_shardings = self .get_data_shardings (mesh )
@@ -301,8 +402,6 @@ def training_loop(self, pipeline, optimizer, learning_rate_scheduler, train_data
301402
302403 rng = jax .random .key (self .config .seed )
303404 rng , eval_rng_key = jax .random .split (rng )
304- start_step = 0
305- last_step_completion = datetime .datetime .now ()
306405 local_metrics_file = open (self .config .metrics_file , "a" , encoding = "utf8" ) if self .config .metrics_file else None
307406 running_gcs_metrics = [] if self .config .gcs_metrics else None
308407 first_profiling_step = self .config .skip_first_n_steps_for_profiler
@@ -311,9 +410,6 @@ def training_loop(self, pipeline, optimizer, learning_rate_scheduler, train_data
311410 last_profiling_step = np .clip (
312411 first_profiling_step + self .config .profiler_steps - 1 , first_profiling_step , self .config .max_train_steps - 1
313412 )
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 )
317413 per_device_tflops , _ , _ = BaseWanTrainer .calculate_tflops (pipeline )
318414 scheduler_state = pipeline .scheduler_state
319415 example_batch = load_next_batch (train_data_iterator , None , self .config )
@@ -367,7 +463,10 @@ def training_loop(self, pipeline, optimizer, learning_rate_scheduler, train_data
367463 writer .flush ()
368464 if self .config .save_final_checkpoint :
369465 max_logging .log (f"Saving final checkpoint for step { step } " )
370- self .checkpointer .save_checkpoint (self .config .max_train_steps - 1 , pipeline , state .params )
466+ if self .config .save_optimizer :
467+ self .checkpointer .save_checkpoint (self .config .max_train_steps - 1 , pipeline , state )
468+ else :
469+ self .checkpointer .save_checkpoint (self .config .max_train_steps - 1 , pipeline , state .params )
371470 self .checkpointer .checkpoint_manager .wait_until_finished ()
372471 # load new state for trained transformer
373472 pipeline .transformer = nnx .merge (state .graphdef , state .params , state .rest_of_state )
0 commit comments