@@ -245,14 +245,22 @@ def initialize_training_pipeline(self, training_args: TrainingArgs):
245245
246246 self .generator_ema : EMA_FSDP | None = None
247247 self .generator_ema_2 : EMA_FSDP | None = None
248- if (self .training_args .ema_decay is not None ) and (self .training_args .ema_decay > 0.0 ):
248+ ema_enabled = (self .training_args .ema_decay is not None ) and (self .training_args .ema_decay > 0.0 )
249+ if ema_enabled and (self .training_args .ema_start_step <= 0 ):
250+ # Only eager-construct from the cold init weights when averaging starts at step 0.
249251 self .generator_ema = EMA_FSDP (self .transformer , decay = self .training_args .ema_decay )
250252 logger .info ("Initialized generator EMA with decay=%s" , self .training_args .ema_decay )
251253
252254 # Initialize EMA for transformer_2 if it exists
253255 if self .transformer_2 is not None :
254256 self .generator_ema_2 = EMA_FSDP (self .transformer_2 , decay = self .training_args .ema_decay )
255257 logger .info ("Initialized generator EMA_2 with decay=%s" , self .training_args .ema_decay )
258+ elif ema_enabled :
259+ # Defer construction to the lazy block in the train loop, which builds the EMA AT
260+ # ema_start_step from the already-trained weights. Eager-constructing here would anchor
261+ # the shadow to the cold init and leave it base-contaminated (blurry) on short runs.
262+ logger .info ("Generator EMA deferred: built lazily at ema_start_step=%s from trained weights" ,
263+ self .training_args .ema_start_step )
256264 else :
257265 logger .info ("Generator EMA disabled (ema_decay <= 0.0)" )
258266
@@ -326,22 +334,6 @@ def apply_ema_to_model(self, model):
326334 return model
327335 return model
328336
329- def get_ema_model_copy (self ) -> torch .nn .Module | None :
330- """Get a copy of the model with EMA weights applied."""
331- if self .generator_ema is not None :
332- ema_model = copy .deepcopy (self .transformer )
333- self .generator_ema .copy_to_unwrapped (ema_model )
334- return ema_model
335- return None
336-
337- def get_ema_2_model_copy (self ) -> torch .nn .Module | None :
338- """Get a copy of the transformer_2 model with EMA weights applied."""
339- if self .generator_ema_2 is not None and self .transformer_2 is not None :
340- ema_2_model = copy .deepcopy (self .transformer_2 )
341- self .generator_ema_2 .copy_to_unwrapped (ema_2_model )
342- return ema_2_model
343- return None
344-
345337 def is_ema_ready (self , current_step : int | None = None ):
346338 """Check if EMA is ready for use (after ema_start_step)."""
347339 if current_step is None :
@@ -361,68 +353,61 @@ def save_ema_weights(self, output_dir: str, step: int):
361353 try :
362354 # Save main transformer EMA
363355 if self .generator_ema is not None :
364- ema_model = self .get_ema_model_copy ()
365- if ema_model is None :
366- logger .warning ("Failed to create EMA model copy" )
367- else :
368- ema_save_dir = os .path .join (output_dir , f"ema_checkpoint-{ step } " )
369- os .makedirs (ema_save_dir , exist_ok = True )
356+ ema_save_dir = os .path .join (output_dir , f"ema_checkpoint-{ step } " )
357+ os .makedirs (ema_save_dir , exist_ok = True )
370358
371- # save as diffusers format
372- from safetensors .torch import save_file
359+ # save as diffusers format
360+ from safetensors .torch import save_file
373361
374- from fastvideo .training .training_utils import (custom_to_hf_state_dict ,
375- gather_state_dict_on_cpu_rank0 )
376- cpu_state = gather_state_dict_on_cpu_rank0 (ema_model , device = None )
362+ from fastvideo .training .training_utils import (custom_to_hf_state_dict , gather_state_dict_on_cpu_rank0 )
363+ # Swap EMA weights into the live FSDP module in place (no deepcopy) and gather the
364+ # full state dict within the context; weights are restored on exit.
365+ with self .generator_ema .apply_to_model (self .transformer ):
366+ cpu_state = gather_state_dict_on_cpu_rank0 (self .transformer , device = None )
377367
378- if self .global_rank == 0 :
379- weight_path = os .path .join (ema_save_dir , "diffusion_pytorch_model.safetensors" )
380- diffusers_state_dict = custom_to_hf_state_dict (cpu_state , ema_model .reverse_param_names_mapping )
381- save_file (diffusers_state_dict , weight_path )
382-
383- config_dict = ema_model .hf_config
384- if "dtype" in config_dict :
385- del config_dict ["dtype" ]
386- config_path = os .path .join (ema_save_dir , "config.json" )
387- with open (config_path , "w" ) as f :
388- json .dump (config_dict , f , indent = 4 )
368+ if self .global_rank == 0 :
369+ weight_path = os .path .join (ema_save_dir , "diffusion_pytorch_model.safetensors" )
370+ diffusers_state_dict = custom_to_hf_state_dict (cpu_state ,
371+ self .transformer .reverse_param_names_mapping )
372+ save_file (diffusers_state_dict , weight_path )
389373
390- logger .info ("EMA weights saved to %s" , weight_path )
374+ # deepcopy so deleting "dtype" doesn't mutate the live model's hf_config
375+ config_dict = copy .deepcopy (self .transformer .hf_config )
376+ if "dtype" in config_dict :
377+ del config_dict ["dtype" ]
378+ config_path = os .path .join (ema_save_dir , "config.json" )
379+ with open (config_path , "w" ) as f :
380+ json .dump (config_dict , f , indent = 4 )
391381
392- del ema_model
382+ logger . info ( "EMA weights saved to %s" , weight_path )
393383
394384 # Save transformer_2 EMA
395- if self .generator_ema_2 is not None :
396- ema_2_model = self .get_ema_2_model_copy ()
397- if ema_2_model is None :
398- logger .warning ("Failed to create EMA_2 model copy" )
399- else :
400- ema_2_save_dir = os .path .join (output_dir , f"ema_2_checkpoint-{ step } " )
401- os .makedirs (ema_2_save_dir , exist_ok = True )
402-
403- # save as diffusers format
404- from safetensors .torch import save_file
405-
406- from fastvideo .training .training_utils import (custom_to_hf_state_dict ,
407- gather_state_dict_on_cpu_rank0 )
408- cpu_state_2 = gather_state_dict_on_cpu_rank0 (ema_2_model , device = None )
385+ if self .generator_ema_2 is not None and self .transformer_2 is not None :
386+ ema_2_save_dir = os .path .join (output_dir , f"ema_2_checkpoint-{ step } " )
387+ os .makedirs (ema_2_save_dir , exist_ok = True )
409388
410- if self .global_rank == 0 :
411- weight_path_2 = os .path .join (ema_2_save_dir , "diffusion_pytorch_model.safetensors" )
412- diffusers_state_dict_2 = custom_to_hf_state_dict (cpu_state_2 ,
413- ema_2_model .reverse_param_names_mapping )
414- save_file (diffusers_state_dict_2 , weight_path_2 )
389+ # save as diffusers format
390+ from safetensors .torch import save_file
415391
416- config_dict_2 = ema_2_model .hf_config
417- if "dtype" in config_dict_2 :
418- del config_dict_2 ["dtype" ]
419- config_path_2 = os .path .join (ema_2_save_dir , "config.json" )
420- with open (config_path_2 , "w" ) as f :
421- json .dump (config_dict_2 , f , indent = 4 )
392+ from fastvideo .training .training_utils import (custom_to_hf_state_dict , gather_state_dict_on_cpu_rank0 )
393+ with self .generator_ema_2 .apply_to_model (self .transformer_2 ):
394+ cpu_state_2 = gather_state_dict_on_cpu_rank0 (self .transformer_2 , device = None )
422395
423- logger .info ("EMA_2 weights saved to %s" , weight_path_2 )
424-
425- del ema_2_model
396+ if self .global_rank == 0 :
397+ weight_path_2 = os .path .join (ema_2_save_dir , "diffusion_pytorch_model.safetensors" )
398+ diffusers_state_dict_2 = custom_to_hf_state_dict (cpu_state_2 ,
399+ self .transformer_2 .reverse_param_names_mapping )
400+ save_file (diffusers_state_dict_2 , weight_path_2 )
401+
402+ # deepcopy so deleting "dtype" doesn't mutate the live model's hf_config
403+ config_dict_2 = copy .deepcopy (self .transformer_2 .hf_config )
404+ if "dtype" in config_dict_2 :
405+ del config_dict_2 ["dtype" ]
406+ config_path_2 = os .path .join (ema_2_save_dir , "config.json" )
407+ with open (config_path_2 , "w" ) as f :
408+ json .dump (config_dict_2 , f , indent = 4 )
409+
410+ logger .info ("EMA_2 weights saved to %s" , weight_path_2 )
426411
427412 except Exception as e :
428413 logger .error ("Failed to save EMA weights: %s" , str (e ))
0 commit comments