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