Skip to content

Commit a931efe

Browse files
authored
[bugfix] EMA in distillation pipeline (#1440)
1 parent 041e5e9 commit a931efe

1 file changed

Lines changed: 54 additions & 69 deletions

File tree

fastvideo/training/distillation_pipeline.py

Lines changed: 54 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)