Fix audio losses for DOP and other preservation losses

This commit is contained in:
Jaret Burkett
2026-08-04 22:08:48 -06:00
parent 0f9094db95
commit 3afa270ab5
4 changed files with 70 additions and 18 deletions

View File

@@ -852,13 +852,15 @@ class LTX2Model(BaseModel):
batch: "DataLoaderBatchDTO" = None, batch: "DataLoaderBatchDTO" = None,
**kwargs, **kwargs,
): ):
# the primary (loss carrying) prediction is the first one made with grad # a grad-enabled prediction is the primary (loss carrying) one unless
# enabled. Prior/cfg/guidance passes run under no_grad, and the # the trainer declared a secondary slot on the batch (prior /
# preservation pass (diff_output_preservation, blank_prompt_preservation) # guidance-unconditional / preservation passes). Trainers that make
# runs with grad but after the loss, so it must not restate the audio # several grad predictions per step (e.g. turbo rollouts) get one
# prediction the primary pass stored on the batch. # primary per prediction, last writer wins.
is_primary_pred = ( is_primary_pred = (
torch.is_grad_enabled() and batch is not None and batch.audio_pred is None torch.is_grad_enabled()
and batch is not None
and batch.audio_pred_slot is None
) )
with torch.no_grad(): with torch.no_grad():
if self.model.device == torch.device("cpu"): if self.model.device == torch.device("cpu"):
@@ -1064,7 +1066,7 @@ class LTX2Model(BaseModel):
if is_primary_pred: if is_primary_pred:
batch.audio_pred = noise_pred_audio batch.audio_pred = noise_pred_audio
else: else:
batch.audio_pred_uncond = noise_pred_audio batch.set_secondary_audio_pred(noise_pred_audio)
unpacked_output = self.pipeline._unpack_latents( unpacked_output = self.pipeline._unpack_latents(
latents=noise_pred_video, latents=noise_pred_video,

View File

@@ -719,13 +719,15 @@ class MinimaxH3Model(BaseModel):
if self.model.device == torch.device("cpu"): if self.model.device == torch.device("cpu"):
self.model.to(device) self.model.to(device)
# the primary (loss carrying) prediction is the first one made with grad # a grad-enabled prediction is the primary (loss carrying) one unless
# enabled. Prior/cfg/guidance passes run under no_grad, and the # the trainer declared a secondary slot on the batch (prior /
# preservation pass (diff_output_preservation, blank_prompt_preservation) # guidance-unconditional / preservation passes). Trainers that make
# runs with grad but after the loss, so it must not restate the audio # several grad predictions per step (e.g. turbo rollouts) get one
# prediction the primary pass stored on the batch. # primary per prediction, last writer wins.
is_primary_pred = ( is_primary_pred = (
torch.is_grad_enabled() and batch is not None and batch.audio_pred is None torch.is_grad_enabled()
and batch is not None
and batch.audio_pred_slot is None
) )
batch_size, _, t_lat, h_lat, w_lat = latent_model_input.shape batch_size, _, t_lat, h_lat, w_lat = latent_model_input.shape
@@ -809,10 +811,16 @@ class MinimaxH3Model(BaseModel):
batch.audio_latents = raw_audio batch.audio_latents = raw_audio
if batch.audio_target is None: if batch.audio_target is None:
# model predicts clean - noise; audio_pred is negated below # model predicts clean - noise; audio_pred is negated below
# so the stored target follows ai-toolkit's noise - clean # so the stored target follows ai-toolkit's noise - clean.
# With the shared noise this is the same value on every
# pass, so first writer is fine (and it keeps a guidance
# extrapolated target from being overwritten).
batch.audio_target = (audio_noise - raw_audio).detach() batch.audio_target = (audio_noise - raw_audio).detach()
if is_primary_pred:
# expose what audio perceptual losses need to rebuild the # expose what audio perceptual losses need to rebuild the
# clean estimate (x0 = noisy - sigma_a * pred) # clean estimate (x0 = noisy - sigma_a * pred). Tied to the
# primary pass so they always match audio_pred, even when a
# trainer makes primary predictions at several sigmas.
batch.audio_noisy = audio_rows batch.audio_noisy = audio_rows
batch.audio_sigma = sigma_a batch.audio_sigma = sigma_a
else: else:
@@ -893,7 +901,7 @@ class MinimaxH3Model(BaseModel):
if is_primary_pred: if is_primary_pred:
batch.audio_pred = -audio_pred batch.audio_pred = -audio_pred
else: else:
batch.audio_pred_uncond = -audio_pred batch.set_secondary_audio_pred(-audio_pred)
video_pred = video_pred[:, num_cond:] video_pred = video_pred[:, num_cond:]
noise_pred = unpatchify_video_tokens(video_pred, t_lat, h_lat, w_lat) noise_pred = unpatchify_video_tokens(video_pred, t_lat, h_lat, w_lat)

View File

@@ -700,6 +700,9 @@ class SDTrainer(BaseSDTrainProcess):
unconditional_embeds = concat_prompt_embeds( unconditional_embeds = concat_prompt_embeds(
[self.unconditional_embeds] * noisy_latents.shape[0], [self.unconditional_embeds] * noisy_latents.shape[0],
) )
# joint audio models route this pass's audio pred to its own
# slot so it cannot stomp the primary pred on the batch
batch.audio_pred_slot = 'audio_pred_uncond'
unconditional_target = self.predict_noise( unconditional_target = self.predict_noise(
noisy_latents=noisy_latents, noisy_latents=noisy_latents,
timesteps=timesteps, timesteps=timesteps,
@@ -707,6 +710,7 @@ class SDTrainer(BaseSDTrainProcess):
unconditional_embeds=None, unconditional_embeds=None,
batch=batch, batch=batch,
) )
batch.audio_pred_slot = None
is_video = len(target.shape) == 5 is_video = len(target.shape) == 5
if self.train_config.do_guidance_loss_cfg_zero: if self.train_config.do_guidance_loss_cfg_zero:
@@ -1910,6 +1914,10 @@ class SDTrainer(BaseSDTrainProcess):
[blank_embeds] * noisy_latents.shape[0] [blank_embeds] * noisy_latents.shape[0]
) )
# joint audio models stash their audio pred on the batch.
# Give this pass its own slot so the preservation loss
# can pair it with the preservation pass below.
batch.audio_pred_slot = 'audio_pred_prior'
prior_pred = self.get_prior_prediction( prior_pred = self.get_prior_prediction(
noisy_latents=noisy_latents, noisy_latents=noisy_latents,
conditional_embeds=prior_embeds_to_use, conditional_embeds=prior_embeds_to_use,
@@ -1922,6 +1930,7 @@ class SDTrainer(BaseSDTrainProcess):
unconditional_embeds=unconditional_embeds, unconditional_embeds=unconditional_embeds,
conditioned_prompts=conditioned_prompts conditioned_prompts=conditioned_prompts
) )
batch.audio_pred_slot = None
if prior_pred is not None: if prior_pred is not None:
prior_pred = prior_pred.detach() prior_pred = prior_pred.detach()
@@ -2110,6 +2119,7 @@ class SDTrainer(BaseSDTrainProcess):
preservation_embeds = concat_prompt_embeds( preservation_embeds = concat_prompt_embeds(
[blank_embeds] * noisy_latents.shape[0] [blank_embeds] * noisy_latents.shape[0]
) )
batch.audio_pred_slot = 'audio_pred_preservation'
preservation_pred = self.predict_noise( preservation_pred = self.predict_noise(
noisy_latents=noisy_latents.to(self.device_torch, dtype=dtype), noisy_latents=noisy_latents.to(self.device_torch, dtype=dtype),
timesteps=timesteps, timesteps=timesteps,
@@ -2118,10 +2128,23 @@ class SDTrainer(BaseSDTrainProcess):
batch=batch, batch=batch,
**pred_kwargs **pred_kwargs
) )
batch.audio_pred_slot = None
multiplier = self.train_config.diff_output_preservation_multiplier if self.train_config.diff_output_preservation else self.train_config.blank_prompt_preservation_multiplier multiplier = self.train_config.diff_output_preservation_multiplier if self.train_config.diff_output_preservation else self.train_config.blank_prompt_preservation_multiplier
preservation_loss = torch.nn.functional.mse_loss(preservation_pred, prior_pred) * multiplier preservation_loss = torch.nn.functional.mse_loss(preservation_pred, prior_pred) * multiplier
self.additional_logs['loss/normal'] = loss.item() self.additional_logs['loss/normal'] = loss.item()
self.additional_logs['loss/preservation'] = preservation_loss.item() self.additional_logs['loss/preservation'] = preservation_loss.item()
# preserve the audio stream of joint audio models too.
# Both passes ran on the same noisy audio, so this holds
# the audio branch to its base model output.
if batch.audio_pred_preservation is not None and batch.audio_pred_prior is not None:
audio_preservation_loss = torch.nn.functional.mse_loss(
batch.audio_pred_preservation.float(),
batch.audio_pred_prior.float(),
) * multiplier * self.train_config.audio_loss_multiplier
self.additional_logs['loss/preservation_audio'] = audio_preservation_loss.item()
preservation_loss = preservation_loss + audio_preservation_loss
loss = loss + preservation_loss loss = loss + preservation_loss
# check if nan # check if nan

View File

@@ -225,9 +225,17 @@ class DataLoaderBatchDTO:
# prediction. Secondary passes (cfg / guidance loss / prior preds) # prediction. Secondary passes (cfg / guidance loss / prior preds)
# reuse it so their noisy audio matches the stored audio_target. # reuse it so their noisy audio matches the stored audio_target.
self.audio_noise: Union[torch.Tensor, None] = None self.audio_noise: Union[torch.Tensor, None] = None
# audio prediction from an unconditional/secondary pass. Kept # audio predictions from the non primary passes. Kept separate so
# separate so it cannot stomp the primary pred we backprop through. # they cannot stomp the primary pred we backprop through.
self.audio_pred_uncond: Union[torch.Tensor, None] = None self.audio_pred_uncond: Union[torch.Tensor, None] = None
self.audio_pred_prior: Union[torch.Tensor, None] = None
self.audio_pred_preservation: Union[torch.Tensor, None] = None
# which of the above the current secondary pass writes to. None (the
# default) means no secondary pass is in flight: any grad-enabled
# prediction is a primary one and writes audio_pred (and the
# noisy/sigma bookkeeping) directly. The trainer sets this around
# its prior / guidance-unconditional / preservation passes.
self.audio_pred_slot: Union[str, None] = None
# noisy audio rows and audio sigma of the primary pass, used to # noisy audio rows and audio sigma of the primary pass, used to
# rebuild the clean audio estimate for perceptual losses # rebuild the clean audio estimate for perceptual losses
self.audio_noisy: Union[torch.Tensor, None] = None self.audio_noisy: Union[torch.Tensor, None] = None
@@ -491,6 +499,15 @@ class DataLoaderBatchDTO:
): ):
return [x.caption_short for x in self.file_items] return [x.caption_short for x in self.file_items]
def set_secondary_audio_pred(self, pred):
"""Route an audio prediction from a non primary pass (prior,
unconditional/guidance, preservation) to its own slot so it cannot
stomp the primary prediction the loss backprops through. Passes that
did not declare a slot (e.g. a trainer's extra no_grad prediction)
are simply not stored."""
if self.audio_pred_slot is not None:
setattr(self, self.audio_pred_slot, pred)
def cleanup(self): def cleanup(self):
del self.latents del self.latents
del self.tensor del self.tensor
@@ -501,6 +518,8 @@ class DataLoaderBatchDTO:
del self.audio_pred del self.audio_pred
del self.audio_noise del self.audio_noise
del self.audio_pred_uncond del self.audio_pred_uncond
del self.audio_pred_prior
del self.audio_pred_preservation
del self.audio_noisy del self.audio_noisy
del self.audio_sigma del self.audio_sigma
del self.first_frame_latents del self.first_frame_latents