Migrate to a new DTO for latents to carry more information that a normal tensor such as audio.

This commit is contained in:
Jaret Burkett
2026-08-30 10:30:21 -06:00
parent 2a69c1e7de
commit 764b5064fb
8 changed files with 369 additions and 215 deletions

View File

@@ -16,6 +16,7 @@ from toolkit.clip_vision_adapter import ClipVisionAdapter
from toolkit.config_modules import GenerateImageConfig
from toolkit.data_loader import get_dataloader_datasets
from toolkit.data_transfer_object.data_loader import DataLoaderBatchDTO, FileItemDTO
from toolkit.dto import DTO
from toolkit.guidance import get_targeted_guidance_loss, get_guidance_loss, GuidanceType
from toolkit.image_utils import show_tensors, show_latents
from toolkit.ip_adapter import IPAdapter
@@ -541,6 +542,13 @@ class SDTrainer(BaseSDTrainProcess):
target_mask_multiplier = None
dtype = get_torch_dtype(self.train_config.dtype)
# joint audio models return a DTO: video pred as the tensor, the audio
# pred and its target riding as extras on each pass's own prediction.
# Grab them before any math rebinds noise_pred to a plain tensor.
audio_pred = noise_pred.get('audio') if isinstance(noise_pred, DTO) else None
audio_target = noise_pred.get('audio_target') if isinstance(noise_pred, DTO) else None
audio_sigma = noise_pred.get('audio_sigma') if isinstance(noise_pred, DTO) else None
has_mask = batch.mask_tensor is not None
with torch.no_grad():
@@ -625,6 +633,9 @@ class SDTrainer(BaseSDTrainProcess):
# matching adapter prediction
target = prior_pred
if getattr(self.sd, 'dopsd_self_ref', False):
if isinstance(prior_pred, DTO) and prior_pred.get('audio') is not None:
# the teacher's audio prediction is the audio target too
audio_target = prior_pred.get('audio').detach()
# D-OPSD bleed: also train against the normal (non-teacher) target
if hasattr(self.sd, 'get_loss_target'):
dopsd_normal_target = self.sd.get_loss_target(
@@ -749,9 +760,6 @@ class SDTrainer(BaseSDTrainProcess):
unconditional_embeds = concat_prompt_embeds(
[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(
noisy_latents=noisy_latents,
timesteps=timesteps,
@@ -759,7 +767,8 @@ class SDTrainer(BaseSDTrainProcess):
unconditional_embeds=None,
batch=batch,
)
batch.audio_pred_slot = None
# joint audio models: this pass's DTO carries its own audio pred
audio_uncond = unconditional_target.get('audio') if isinstance(unconditional_target, DTO) else None
is_video = len(target.shape) == 5
if self.train_config.do_guidance_loss_cfg_zero:
@@ -797,17 +806,17 @@ class SDTrainer(BaseSDTrainProcess):
unconditional_target = unconditional_target * alpha
target = unconditional_target + guidance_scale * (target - unconditional_target)
# joint audio models (ltx2, minimax_h3, flux3) carry their audio
# target/pred on the batch. Extrapolate the audio target the
# same way so the audio stream trains contrastively as well.
audio_uncond = getattr(batch, 'audio_pred_uncond', None)
if batch.audio_target is not None and audio_uncond is not None:
audio_target = batch.audio_target.float()
# joint audio models carry their audio pred/target on the pred
# DTOs. Extrapolate the audio target the same way so the audio
# stream trains contrastively as well.
if audio_target is not None and audio_uncond is not None:
a_dtype = audio_target.dtype
a_target = audio_target.float()
audio_uncond = audio_uncond.float()
audio_dims = [1] * (audio_target.dim() - 1)
audio_dims = [1] * (a_target.dim() - 1)
if self.train_config.do_guidance_loss_cfg_zero:
batch_size = audio_target.shape[0]
a_pos_flat = audio_target.view(batch_size, -1)
batch_size = a_target.shape[0]
a_pos_flat = a_target.view(batch_size, -1)
a_neg_flat = audio_uncond.view(batch_size, -1)
a_dot = torch.sum(a_pos_flat * a_neg_flat, dim=1, keepdim=True)
a_squared_norm = torch.sum(a_neg_flat ** 2, dim=1, keepdim=True) + 1e-8
@@ -816,22 +825,22 @@ class SDTrainer(BaseSDTrainProcess):
audio_guidance_scale = self._guidance_loss_target_batch
if isinstance(audio_guidance_scale, list):
audio_guidance_scale = torch.tensor(audio_guidance_scale).to(
audio_target.device, dtype=audio_target.dtype
a_target.device, dtype=a_target.dtype
).view(-1, *audio_dims)
if self.train_config.guidance_loss_schedule == 'sigma':
# audio streams can run on their own remapped sigma
audio_sigma = getattr(batch, 'audio_sigma', None)
if audio_sigma is None:
audio_sigma = timesteps / 1000.0
audio_sigma = audio_sigma.to(
audio_target.device, dtype=audio_target.dtype
a_sigma = audio_sigma
if a_sigma is None:
a_sigma = timesteps / 1000.0
a_sigma = a_sigma.to(
a_target.device, dtype=a_target.dtype
).view(-1, *audio_dims)
audio_guidance_scale = 1.0 + (audio_guidance_scale - 1.0) * audio_sigma
audio_guidance_scale = 1.0 + (audio_guidance_scale - 1.0) * a_sigma
batch.audio_target = (
audio_uncond + audio_guidance_scale * (audio_target - audio_uncond)
).to(batch.audio_target.dtype).detach()
audio_target = (
audio_uncond + audio_guidance_scale * (a_target - audio_uncond)
).to(a_dtype).detach()
if self.train_config.do_differential_guidance:
with torch.no_grad():
@@ -865,7 +874,6 @@ class SDTrainer(BaseSDTrainProcess):
# we also denoise as the unaugmented tensor is not a noisy diffirental
with torch.no_grad():
unaugmented_latents = self.sd.encode_images(batch.unaugmented_tensor).to(self.device_torch, dtype=dtype)
unaugmented_latents = unaugmented_latents * self.train_config.latent_multiplier
target = unaugmented_latents.detach()
# Get the target for loss depending on the prediction type
@@ -1040,8 +1048,8 @@ class SDTrainer(BaseSDTrainProcess):
loss = loss.mean()
# check for audio loss
if batch.audio_pred is not None and batch.audio_target is not None:
audio_loss = torch.nn.functional.mse_loss(batch.audio_pred.float(), batch.audio_target.float(), reduction="mean")
if audio_pred is not None and audio_target is not None:
audio_loss = torch.nn.functional.mse_loss(audio_pred.float(), audio_target.float(), reduction="mean")
audio_loss = audio_loss * self.train_config.audio_loss_multiplier
self.additional_logs['loss/img'] = loss.item()
self.additional_logs['loss/audio'] = audio_loss.item()
@@ -2035,10 +2043,6 @@ class SDTrainer(BaseSDTrainProcess):
)
batch.dopsd_teacher_pass = True
# 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(
noisy_latents=noisy_latents,
conditional_embeds=prior_embeds_to_use,
@@ -2051,13 +2055,10 @@ class SDTrainer(BaseSDTrainProcess):
unconditional_embeds=unconditional_embeds,
conditioned_prompts=conditioned_prompts
)
batch.audio_pred_slot = None
if is_dopsd:
batch.dopsd_teacher_pass = False
if batch.audio_pred_prior is not None:
# the teacher's audio prediction is the audio target too
batch.audio_target = batch.audio_pred_prior.detach()
if prior_pred is not None:
# a DTO prior pred keeps its audio extras through detach
prior_pred = prior_pred.detach()
# do the custom adapter after the prior prediction
@@ -2235,7 +2236,6 @@ class SDTrainer(BaseSDTrainProcess):
preservation_embeds = concat_prompt_embeds(
[blank_embeds] * noisy_latents.shape[0]
)
batch.audio_pred_slot = 'audio_pred_preservation'
preservation_pred = self.predict_noise(
noisy_latents=noisy_latents.to(self.device_torch, dtype=dtype),
timesteps=timesteps,
@@ -2244,7 +2244,6 @@ class SDTrainer(BaseSDTrainProcess):
batch=batch,
**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
preservation_loss = torch.nn.functional.mse_loss(preservation_pred, prior_pred) * multiplier
self.additional_logs['loss/normal'] = loss.item()
@@ -2253,10 +2252,12 @@ class SDTrainer(BaseSDTrainProcess):
# 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_pres = preservation_pred.get('audio') if isinstance(preservation_pred, DTO) else None
audio_prior = prior_pred.get('audio') if isinstance(prior_pred, DTO) else None
if audio_pres is not None and audio_prior is not None:
audio_preservation_loss = torch.nn.functional.mse_loss(
batch.audio_pred_preservation.float(),
batch.audio_pred_prior.float(),
audio_pres.float(),
audio_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