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
@@ -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