Spaces:
Running on Zero
Running on Zero
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |
| # SPDX-License-Identifier: Apache-2.0 | |
| from __future__ import annotations | |
| from functools import partial | |
| from typing import Dict, Any, TYPE_CHECKING, Callable, Optional | |
| import torch | |
| import torch.nn.functional as F | |
| from lipforcing.configs.opt import get_scheduler | |
| from lipforcing.utils import instantiate | |
| from lipforcing.methods import FastGenModel | |
| from lipforcing.methods.common_loss import ( | |
| denoising_score_matching_loss, | |
| variational_score_distillation_loss, | |
| gan_loss_generator, | |
| gan_loss_discriminator, | |
| ) | |
| import lipforcing.utils.logging_utils as logger | |
| from lipforcing.utils.distributed import synchronize, is_rank0 | |
| from lipforcing.utils.basic_utils import convert_cfg_to_dict | |
| if TYPE_CHECKING: | |
| from lipforcing.configs.methods.config_dmd2 import ModelConfig | |
| class DMD2Model(FastGenModel): | |
| def __init__(self, config: ModelConfig): | |
| """ | |
| Args: | |
| config (ModelConfig): The configuration for the DMD model | |
| """ | |
| super().__init__(config) | |
| self.config = config | |
| def build_model(self): | |
| super().build_model() | |
| self.build_teacher() | |
| self.load_student_weights_and_ema() | |
| # instantiate the fake_score — use separate config if provided (e.g. 1.3B critic with 14B teacher) | |
| fake_score_cfg = getattr(self.config, "fake_score_net", None) | |
| if fake_score_cfg is not None: | |
| logger.info("Instantiating the fake_score (custom architecture, different from teacher)") | |
| else: | |
| fake_score_cfg = self.teacher_config | |
| logger.info("Instantiating the fake_score (same architecture as teacher)") | |
| with self._get_meta_init_context(): | |
| self.fake_score = instantiate(fake_score_cfg) | |
| model_path = self.config.pretrained_model_path | |
| if model_path is not None and len(model_path) > 0: | |
| if getattr(self.config, "fake_score_net", None) is not None: | |
| logger.warning("Skipping teacher->fake_score weight copy (fake_score uses a different architecture)") | |
| else: | |
| if (not self.config.fsdp_meta_init) or is_rank0(): | |
| # Only rank 0 loads weights if using meta initialization | |
| self.fake_score.load_state_dict(self.teacher.state_dict()) | |
| synchronize() | |
| if self.config.gan_loss_weight_gen > 0: | |
| logger.info(f"gan_loss_weight_gen: {self.config.gan_loss_weight_gen}") | |
| # instantiate the discriminator in DMD2 | |
| logger.info("Instantiating the discriminator") | |
| if getattr(self.config.discriminator, "disc_type", None) is not None: | |
| logger.info(f"Discriminator type: {self.config.discriminator.disc_type}") | |
| # TODO: Discriminators do not yet support meta initialization | |
| self.discriminator = instantiate(self.config.discriminator) | |
| synchronize() | |
| torch.cuda.empty_cache() | |
| def _setup_grad_requirements(self, iteration: int) -> None: | |
| if iteration % self.config.student_update_freq == 0: | |
| # update the student | |
| self.fake_score.eval().requires_grad_(False) | |
| if self.config.gan_loss_weight_gen > 0: | |
| self.discriminator.eval().requires_grad_(False) | |
| else: | |
| # update the fake_score and discriminator | |
| self.fake_score.train().requires_grad_(True) | |
| if self.config.gan_loss_weight_gen > 0: | |
| self.discriminator.train().requires_grad_(True) | |
| def _generate_noise_and_time( | |
| self, real_data: torch.Tensor | |
| ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: | |
| """Generate random noises and time step | |
| Args: | |
| batch_size: Batch size | |
| real_data: Real data tensor for dtype/device reference | |
| Returns: | |
| rand_z_max: Random noise used by the student | |
| t_max: Time step used by the student | |
| t: Time step | |
| eps: Random noise used by a forward process | |
| """ | |
| batch_size = real_data.shape[0] | |
| eps_student = torch.randn(batch_size, *self.input_shape, device=self.device, dtype=real_data.dtype) | |
| if self.config.student_sample_steps == 1: | |
| # perform single-step distillation | |
| # input noise to student (sigma * eps) | |
| t_student = torch.full( | |
| (batch_size,), | |
| self.net.noise_scheduler.max_t, | |
| device=self.device, | |
| dtype=self.net.noise_scheduler.t_precision, | |
| ) | |
| input_student = self.net.noise_scheduler.latents(noise=eps_student) | |
| else: | |
| # perform multiple-step distillation | |
| # Add noise to real image data (for multistep generation) | |
| t_student = self.net.noise_scheduler.sample_from_t_list( | |
| batch_size, | |
| sample_steps=self.config.student_sample_steps, | |
| t_list=self.config.sample_t_cfg.t_list, | |
| device=self.device, | |
| ) | |
| input_student = self.net.noise_scheduler.forward_process(real_data, eps_student, t_student) | |
| t = self.net.noise_scheduler.sample_t( | |
| batch_size, **convert_cfg_to_dict(self.config.sample_t_cfg), device=self.device | |
| ) | |
| eps = torch.randn_like(real_data, device=self.device, dtype=real_data.dtype) | |
| return input_student, t_student, t, eps | |
| def _compute_teacher_prediction_gan_loss( | |
| self, perturbed_data: torch.Tensor, t: torch.Tensor, condition: Optional[Any] = None, | |
| gan_reduction: str = "mean", | |
| ) -> tuple[torch.Tensor, torch.Tensor]: | |
| """Compute teacher prediction and optionally GAN loss for generator. | |
| Args: | |
| perturbed_data: Perturbed data tensor | |
| t: Time steps | |
| condition: Conditioning information | |
| gan_reduction: 'mean' returns scalar GAN loss, 'none' returns per-sample [B]. | |
| Returns: | |
| tuple of (teacher_x0, gan_loss_gen) | |
| """ | |
| if self.config.gan_loss_weight_gen > 0: | |
| teacher_x0, fake_feat = self.teacher( | |
| perturbed_data, | |
| t, | |
| condition=condition, | |
| feature_indices=self.discriminator.feature_indices, | |
| fwd_pred_type="x0", | |
| ) | |
| # Compute the GAN loss for the generator | |
| gan_loss_gen = gan_loss_generator(self.discriminator(fake_feat), reduction=gan_reduction) | |
| else: | |
| with torch.no_grad(): | |
| teacher_x0 = self.teacher( | |
| perturbed_data, | |
| t, | |
| condition=condition, | |
| fwd_pred_type="x0", | |
| ) | |
| gan_loss_gen = torch.tensor(0.0, device=self.device, dtype=teacher_x0.dtype) | |
| return teacher_x0.detach(), gan_loss_gen | |
| def _apply_classifier_free_guidance( | |
| self, | |
| perturbed_data: torch.Tensor, | |
| t: torch.Tensor, | |
| teacher_x0: torch.Tensor, | |
| neg_condition: Optional[Any] = None, | |
| ) -> torch.Tensor: | |
| """Apply classifier-free guidance to teacher predictions. | |
| Args: | |
| perturbed_data: Perturbed data | |
| t: Time step | |
| teacher_x0: Original teacher x0 prediction | |
| neg_condition: Negative conditioning for CFG | |
| Returns: | |
| CFG-adjusted teacher_x0 | |
| """ | |
| assert self.config.guidance_scale is not None, "guidance_scale must be provided" | |
| # classifier-free guidance (always run negative pass for FSDP consistency) | |
| with torch.no_grad(): | |
| kwargs = {"condition": neg_condition, "fwd_pred_type": "x0"} | |
| if self.config.skip_layers is not None: | |
| kwargs["skip_layers"] = self.config.skip_layers | |
| teacher_x0_neg = self.teacher(perturbed_data, t, **kwargs) | |
| # Compute effective per-sample guidance scale. | |
| # Sync-Window DMD (SW-DMD; paper Sec. 4.3 / Eq. 6): gate the teacher CFG scale | |
| # by the DMD re-noising timestep — the full guidance_scale applies inside the | |
| # sync window [t_lo, t_hi], else 1.0 (no-CFG). | |
| sw_cfg = self.config.sync_window_cfg | |
| if sw_cfg.enabled: | |
| in_range = (t >= sw_cfg.t_lo) & (t <= sw_cfg.t_hi) | |
| if getattr(sw_cfg, "reverse", False): | |
| in_range = ~in_range | |
| view_shape = [-1] + [1] * (teacher_x0.ndim - 1) | |
| effective_scale = torch.where( | |
| in_range, | |
| torch.tensor(self.config.guidance_scale, device=t.device, dtype=teacher_x0.dtype), | |
| torch.tensor(1.0, device=t.device, dtype=teacher_x0.dtype), | |
| ).view(view_shape) | |
| else: | |
| effective_scale = self.config.guidance_scale | |
| teacher_x0 = teacher_x0 + (effective_scale - 1) * (teacher_x0 - teacher_x0_neg) | |
| return teacher_x0 | |
| def _student_update_step( | |
| self, | |
| input_student: torch.Tensor, | |
| t_student: torch.Tensor, | |
| t: torch.Tensor, | |
| eps: torch.Tensor, | |
| data: Dict[str, Any], | |
| condition: Optional[Any] = None, | |
| neg_condition: Optional[Any] = None, | |
| ) -> tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]]: | |
| """Perform student model update step. | |
| Args: | |
| input_student: Input tensor to student network | |
| t_student: Input time to student network | |
| t: Time step | |
| eps: Noise tensor | |
| data: Original data batch | |
| condition: Conditioning information | |
| neg_condition: Negative conditioning | |
| Returns: | |
| tuple of (loss_map, outputs) | |
| """ | |
| # Generate data from student | |
| gen_data = self.gen_data_from_net(input_student, t_student, condition=condition) | |
| perturbed_data = self.net.noise_scheduler.forward_process(gen_data, eps, t) | |
| # Compute the fake score with x0-prediction | |
| with torch.no_grad(): | |
| fake_score_x0 = self.fake_score(perturbed_data, t, condition=condition, fwd_pred_type="x0") | |
| # Compute the teacher x0-prediction and gan loss for generator | |
| assert ( | |
| perturbed_data.dtype == data["real"].dtype == input_student.dtype | |
| ), f"perturbed_data.dtype: {perturbed_data.dtype}, data['real'].dtype: {data['real'].dtype}, input_student.dtype: {input_student.dtype}" | |
| assert ( | |
| t.dtype == t_student.dtype == self.net.noise_scheduler.t_precision | |
| ), f"t.dtype: {t.dtype}, t_student.dtype: {t_student.dtype}, self.net.noise_scheduler.t_precision: {self.net.noise_scheduler.t_precision}" | |
| teacher_x0, gan_loss_gen = self._compute_teacher_prediction_gan_loss(perturbed_data, t, condition=condition) | |
| # Apply classifier-free guidance if needed | |
| if self.config.guidance_scale is not None: | |
| teacher_x0 = self._apply_classifier_free_guidance( | |
| perturbed_data, t, teacher_x0, neg_condition=neg_condition | |
| ) | |
| # Compute the VSD loss | |
| vsd_loss = variational_score_distillation_loss(gen_data, teacher_x0, fake_score_x0) | |
| # Compute the final loss | |
| loss = vsd_loss + self.config.gan_loss_weight_gen * gan_loss_gen | |
| # Build output dictionaries | |
| loss_map = { | |
| "total_loss": loss, | |
| "vsd_loss": vsd_loss, | |
| "gan_loss_gen": gan_loss_gen, | |
| } | |
| outputs = self._get_outputs(gen_data, input_student, condition=condition) | |
| return loss_map, outputs | |
| def _compute_real_feat( | |
| self, real_data: torch.Tensor, t: torch.Tensor, eps: torch.Tensor, condition: Optional[Any] = None | |
| ) -> tuple[torch.Tensor, torch.Tensor]: | |
| """Compute discriminator features for both real and fake data. | |
| Args: | |
| real_data: Real data tensor | |
| t: Time step | |
| eps: Noise tensor | |
| condition: Conditioning information | |
| Returns: | |
| tuple of (real_feat, t_real) | |
| """ | |
| # decide whether to use the same t and noise for real and fake data | |
| if self.config.gan_use_same_t_noise: | |
| t_real = t | |
| eps_real = eps | |
| else: | |
| t_real = self.net.noise_scheduler.sample_t( | |
| real_data.shape[0], | |
| **convert_cfg_to_dict(self.config.sample_t_cfg), | |
| device=self.device, | |
| ) | |
| eps_real = torch.randn_like(real_data) | |
| # Perturb the real data according to the given forward process | |
| perturbed_real = self.net.noise_scheduler.forward_process(real_data, eps_real, t_real) | |
| real_feat = self.teacher( | |
| perturbed_real, | |
| t_real, | |
| condition=condition, | |
| return_features_early=True, | |
| feature_indices=self.discriminator.feature_indices, | |
| ) | |
| return real_feat, t_real | |
| def _compute_r1_regularization( | |
| self, | |
| real_feat_logit: torch.Tensor, | |
| real_data: torch.Tensor, | |
| t_real: torch.Tensor, | |
| condition: Optional[Any] = None, | |
| ) -> torch.Tensor: | |
| """Compute R1 regularization loss for discriminator. | |
| Args: | |
| real_feat_logit: Real feature logits | |
| real_data: Real data tensor | |
| t_real: Time step for real data | |
| condition: Conditioning information | |
| Returns: | |
| R1 regularization loss | |
| """ | |
| perturbed_real_alpha = real_data.add(self.config.gan_r1_reg_alpha * torch.randn_like(real_data)) | |
| with torch.no_grad(): | |
| real_feat_alpha = self.teacher( | |
| perturbed_real_alpha, | |
| t_real, | |
| condition=condition, | |
| return_features_early=True, | |
| feature_indices=self.discriminator.feature_indices, | |
| ) | |
| real_feat_alpha_logit = self.discriminator(real_feat_alpha) | |
| gan_loss_ar1 = F.mse_loss(real_feat_logit, real_feat_alpha_logit, reduction="mean") | |
| return gan_loss_ar1 | |
| def _fake_score_discriminator_update_step( | |
| self, | |
| input_student: torch.Tensor, | |
| t_student: torch.Tensor, | |
| t: torch.Tensor, | |
| eps: torch.Tensor, | |
| real_data: torch.Tensor, | |
| condition: Optional[Any] = None, | |
| ) -> tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]]: | |
| """Perform fake score and discriminator update step. | |
| Args: | |
| input_student: Input tensor to student network | |
| t_student: Input time to student network | |
| t: Time steps | |
| eps: Noise tensor | |
| condition: Conditioning information | |
| real_data: Real data tensor | |
| Returns: | |
| tuple of (loss_map, outputs) | |
| """ | |
| # Generate data and compute fake score loss | |
| with torch.no_grad(): | |
| gen_data = self.gen_data_from_net(input_student, t_student, condition=condition) | |
| x_t_sg = self.net.noise_scheduler.forward_process(gen_data, eps, t) | |
| # The fake score matches the teacher, but we want to do SDS in x0 space | |
| fake_score_pred_type = self.config.fake_score_pred_type or self.teacher.net_pred_type | |
| assert ( | |
| x_t_sg.dtype == real_data.dtype == input_student.dtype | |
| ), f"x_t_sg.dtype: {x_t_sg.dtype}, real_data.dtype: {real_data.dtype}, input_student.dtype: {input_student.dtype}" | |
| assert ( | |
| t.dtype == t_student.dtype == self.net.noise_scheduler.t_precision | |
| ), f"t.dtype: {t.dtype}, t_student.dtype: {t_student.dtype}, self.net.noise_scheduler.t_precision: {self.net.noise_scheduler.t_precision}" | |
| fake_score_pred = self.fake_score(x_t_sg, t, condition=condition, fwd_pred_type=fake_score_pred_type) | |
| loss_fakescore = denoising_score_matching_loss( | |
| fake_score_pred_type, | |
| net_pred=fake_score_pred, | |
| noise_scheduler=self.net.noise_scheduler, | |
| x0=gen_data, | |
| eps=eps, | |
| t=t, | |
| ) | |
| gan_loss_disc = torch.zeros_like(loss_fakescore) | |
| gan_loss_ar1 = torch.zeros_like(loss_fakescore) | |
| if self.config.gan_loss_weight_gen > 0: | |
| # Compute the GAN loss for the discriminator | |
| with torch.no_grad(): | |
| fake_feat = self.teacher( | |
| x_t_sg, | |
| t, | |
| condition=condition, | |
| return_features_early=True, | |
| feature_indices=self.discriminator.feature_indices, | |
| ) | |
| real_feat, t_real = self._compute_real_feat(real_data=real_data, t=t, eps=eps, condition=condition) | |
| real_feat_logit = self.discriminator(real_feat) | |
| gan_loss_disc = gan_loss_discriminator(real_feat_logit, self.discriminator(fake_feat)) | |
| # Use approximate R1 regularization in the APT paper to regularize the discriminator head | |
| if self.config.gan_r1_reg_weight > 0: | |
| gan_loss_ar1 = self._compute_r1_regularization(real_feat_logit, real_data, t_real, condition=condition) | |
| loss = loss_fakescore + gan_loss_disc + self.config.gan_r1_reg_weight * gan_loss_ar1 | |
| loss_map = { | |
| "total_loss": loss, | |
| "fake_score_loss": loss_fakescore, | |
| "gan_loss_disc": gan_loss_disc, | |
| } | |
| if self.config.gan_loss_weight_gen > 0 and self.config.gan_r1_reg_weight > 0: | |
| loss_map.update({"gan_loss_ar1": gan_loss_ar1}) | |
| outputs = self._get_outputs(gen_data, input_student, condition=condition) | |
| return loss_map, outputs | |
| def _get_outputs( | |
| self, | |
| gen_data: torch.Tensor, | |
| input_student: torch.Tensor = None, | |
| condition: Any = None, | |
| ) -> Dict[str, torch.Tensor | Callable]: | |
| if self.config.student_sample_steps == 1: | |
| assert input_student is not None, "input_student must be provided" | |
| noise = input_student / self.net.noise_scheduler.max_sigma | |
| return {"gen_rand": gen_data, "input_rand": noise} | |
| else: | |
| noise = torch.randn_like(gen_data, dtype=self.precision) | |
| gen_rand_func = partial( | |
| self.generator_fn, | |
| net=self.net_inference, | |
| noise=noise, | |
| condition=condition, | |
| student_sample_steps=self.config.student_sample_steps, | |
| student_sample_type=self.config.student_sample_type, | |
| t_list=self.config.sample_t_cfg.t_list, | |
| precision_amp=self.precision_amp_infer, | |
| ) | |
| return {"gen_rand": gen_rand_func, "input_rand": noise, "gen_rand_train": gen_data} | |
| def single_train_step( | |
| self, data: Dict[str, Any], iteration: int | |
| ) -> tuple[dict[str, torch.Tensor], dict[str, torch.Tensor | Callable]]: | |
| """ | |
| Single training step for distribution matching distillation (DMD) | |
| Args: | |
| data (Dict[str, Any]): Data dict for the current iteration. | |
| iteration (int): Current training iteration | |
| Returns: | |
| loss_map (dict[str, torch.Tensor]): Dictionary containing the loss values | |
| outputs (dict[str, torch.Tensor]): Dictionary containing the network output | |
| """ | |
| # Prepare training data and conditions | |
| real_data, condition, neg_condition = self._prepare_training_data(data) | |
| # Set up gradient requirements based on training phase | |
| self._setup_grad_requirements(iteration) | |
| # Generate noise and time steps | |
| input_student, t_student, t, eps = self._generate_noise_and_time(real_data) | |
| # Choose between student update or fake_score/discriminator update | |
| if iteration % self.config.student_update_freq == 0: | |
| return self._student_update_step( | |
| input_student, t_student, t, eps, data, condition=condition, neg_condition=neg_condition | |
| ) | |
| else: | |
| return self._fake_score_discriminator_update_step( | |
| input_student, t_student, t, eps, real_data, condition=condition | |
| ) | |
| def init_optimizers(self): | |
| """Initialize optimizers, lr_schedulers and grad_scalers""" | |
| super().init_optimizers() | |
| # instantiate the optimizers for fake_score and discriminator | |
| self.fake_score_optimizer = instantiate(self.config.fake_score_optimizer, model=self.fake_score) | |
| # instantiate the lr schedulers for fake_score and discriminator | |
| self.fake_score_lr_scheduler = get_scheduler(self.fake_score_optimizer, self.config.fake_score_scheduler) | |
| if self.config.gan_loss_weight_gen > 0: | |
| # instantiate the discriminator in DMD2 | |
| self.discriminator_optimizer = instantiate(self.config.discriminator_optimizer, model=self.discriminator) | |
| self.discriminator_lr_scheduler = get_scheduler( | |
| self.discriminator_optimizer, self.config.discriminator_scheduler | |
| ) | |
| def get_optimizers(self, iteration: int) -> list[torch.optim.Optimizer]: | |
| """ | |
| Get the optimizers for the current iteration | |
| Args: | |
| iteration (int): The current training iteration | |
| """ | |
| if iteration % self.config.student_update_freq == 0: | |
| return [self.net_optimizer] | |
| else: | |
| if self.config.gan_loss_weight_gen > 0: | |
| return [self.fake_score_optimizer, self.discriminator_optimizer] | |
| else: | |
| return [self.fake_score_optimizer] | |
| def get_lr_schedulers(self, iteration: int) -> list[torch.optim.lr_scheduler]: | |
| """ | |
| Get the lr schedulers for the current iteration | |
| Args: | |
| iteration (int): The current training iteration | |
| """ | |
| if iteration % self.config.student_update_freq == 0: | |
| return [self.net_lr_scheduler] | |
| else: | |
| if self.config.gan_loss_weight_gen > 0: | |
| return [self.fake_score_lr_scheduler, self.discriminator_lr_scheduler] | |
| else: | |
| return [self.fake_score_lr_scheduler] | |
| def model_dict(self): | |
| """Return the model dict containing the student, fake_score, and discriminator models""" | |
| _model_dict = super().model_dict | |
| _model_dict["fake_score"] = self.fake_score | |
| if self.config.gan_loss_weight_gen > 0: | |
| _model_dict["discriminator"] = self.discriminator | |
| return _model_dict | |
| def optimizer_dict(self): | |
| """Return a dict containing all the optimizers""" | |
| _optimizer_dict = super().optimizer_dict | |
| _optimizer_dict["fake_score"] = self.fake_score_optimizer | |
| if self.config.gan_loss_weight_gen > 0: | |
| _optimizer_dict["discriminator"] = self.discriminator_optimizer | |
| return _optimizer_dict | |
| def scheduler_dict(self): | |
| """Return a dict containing all the lr schedulers""" | |
| _scheduler_dict = super().scheduler_dict | |
| _scheduler_dict["fake_score"] = self.fake_score_lr_scheduler | |
| if self.config.gan_loss_weight_gen > 0: | |
| _scheduler_dict["discriminator"] = self.discriminator_lr_scheduler | |
| return _scheduler_dict | |