Automated PR - 2026-01-05

This commit is contained in:
sync-bot
2026-01-05 20:10:38 +00:00
parent fc3b319d34
commit 9ce438b353
153 changed files with 28100 additions and 0 deletions
@@ -0,0 +1,58 @@
"""Training strategies for different conditioning modes.
This package implements the Strategy Pattern to handle different training modes:
- Text-to-video training (standard generation, optionally with audio)
- Video-to-video training (IC-LoRA mode with reference videos)
Each strategy encapsulates the specific logic for preparing model inputs and computing loss.
"""
from ltx_trainer import logger
from ltx_trainer.training_strategies.base_strategy import (
DEFAULT_FPS,
VIDEO_SCALE_FACTORS,
ModelInputs,
TrainingStrategy,
TrainingStrategyConfigBase,
)
from ltx_trainer.training_strategies.text_to_video import TextToVideoConfig, TextToVideoStrategy
from ltx_trainer.training_strategies.video_to_video import VideoToVideoConfig, VideoToVideoStrategy
# Type alias for all strategy config types
TrainingStrategyConfig = TextToVideoConfig | VideoToVideoConfig
__all__ = [
"DEFAULT_FPS",
"VIDEO_SCALE_FACTORS",
"ModelInputs",
"TextToVideoConfig",
"TextToVideoStrategy",
"TrainingStrategy",
"TrainingStrategyConfig",
"TrainingStrategyConfigBase",
"VideoToVideoConfig",
"VideoToVideoStrategy",
"get_training_strategy",
]
def get_training_strategy(config: TrainingStrategyConfig) -> TrainingStrategy:
"""Factory function to create the appropriate training strategy.
The strategy is determined by the `name` field in the configuration.
Args:
config: Strategy-specific configuration with a `name` field
Returns:
The appropriate training strategy instance
Raises:
ValueError: If strategy name is not supported
"""
match config:
case TextToVideoConfig():
strategy = TextToVideoStrategy(config)
case VideoToVideoConfig():
strategy = VideoToVideoStrategy(config)
case _:
raise ValueError(f"Unknown training strategy config type: {type(config).__name__}")
audio_mode = "(audio enabled 🔈)" if getattr(config, "with_audio", False) else "(audio disabled 🔇)"
logger.debug(f"🎯 Using {strategy.__class__.__name__} training strategy {audio_mode}")
return strategy
@@ -0,0 +1,253 @@
"""Base class for training strategies.
This module defines the abstract base class that all training strategies must implement,
along with the base configuration class.
"""
import random
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, Literal
import torch
from pydantic import BaseModel, ConfigDict, Field
from torch import Tensor
from ltx_core.components.patchifiers import (
AudioPatchifier,
VideoLatentPatchifier,
get_pixel_coords,
)
from ltx_core.model.transformer.modality import Modality
from ltx_core.types import AudioLatentShape, SpatioTemporalScaleFactors, VideoLatentShape
from ltx_trainer.timestep_samplers import TimestepSampler
# Default frames per second for video missing in the FPS metadata
DEFAULT_FPS = 24
# VAE scale factors for LTX-2
VIDEO_SCALE_FACTORS = SpatioTemporalScaleFactors.default()
class TrainingStrategyConfigBase(BaseModel):
"""Base configuration class for training strategies.
All strategy-specific configuration classes should inherit from this.
"""
model_config = ConfigDict(extra="forbid")
name: Literal["text_to_video", "video_to_video"] = Field(
description="Unique name identifying the training strategy type"
)
@dataclass
class ModelInputs:
"""Container for model inputs using the Modality-based interface."""
video: Modality
audio: Modality | None
# Training targets (for loss computation)
video_targets: Tensor
audio_targets: Tensor | None
# Masks for loss computation
video_loss_mask: Tensor # Boolean mask: True = compute loss for this token
audio_loss_mask: Tensor | None
# Metadata needed for loss computation in some strategies
ref_seq_len: int | None = None # For IC-LoRA: length of reference sequence
class TrainingStrategy(ABC):
"""Abstract base class for training strategies.
Each strategy encapsulates the logic for a specific training mode,
handling input preparation and loss computation.
"""
def __init__(self, config: TrainingStrategyConfigBase):
"""Initialize strategy with configuration.
Args:
config: Strategy-specific configuration
"""
self.config = config
self._video_patchifier = VideoLatentPatchifier(patch_size=1)
self._audio_patchifier = AudioPatchifier(patch_size=1)
@property
def requires_audio(self) -> bool:
"""Whether this training strategy requires audio components.
Override this property in subclasses that support audio training.
The trainer uses this to determine whether to load audio VAE and vocoder.
Returns:
True if audio components should be loaded, False otherwise.
"""
return False
@abstractmethod
def get_data_sources(self) -> list[str] | dict[str, str]:
"""Get the required data sources for this training strategy.
Returns:
Either a list of data directory names (where output keys match directory names)
or a dictionary mapping data directory names to custom output keys for the dataset
"""
@abstractmethod
def prepare_training_inputs(
self,
batch: dict[str, Any],
timestep_sampler: TimestepSampler,
) -> ModelInputs:
"""Prepare training inputs from a raw data batch.
Args:
batch: Raw batch data from the dataset. Contains:
- "latents": Video latent data
- "conditions": Text embeddings with keys:
- "video_prompt_embeds": Already processed by embedding connectors
- "audio_prompt_embeds": Already processed by embedding connectors
- "prompt_attention_mask": Attention mask
- Additional keys depending on strategy (e.g., "ref_latents" for IC-LoRA)
timestep_sampler: Sampler for generating timesteps and noise
Returns:
ModelInputs containing Modality objects and training targets
"""
@abstractmethod
def compute_loss(
self,
video_pred: Tensor,
audio_pred: Tensor | None,
inputs: ModelInputs,
) -> Tensor:
"""Compute the training loss.
Args:
video_pred: Video prediction from the transformer model
audio_pred: Audio prediction from the transformer model (None for video-only)
inputs: The prepared model inputs containing targets and masks
Returns:
Scalar loss tensor
"""
def _get_video_positions(
self,
num_frames: int,
height: int,
width: int,
batch_size: int,
fps: float,
device: torch.device,
dtype: torch.dtype,
) -> Tensor:
"""Generate video position embeddings using ltx_core's native implementation.
Args:
num_frames: Number of latent frames
height: Latent height
width: Latent width
batch_size: Batch size
fps: Frames per second
device: Target device
dtype: Target dtype
Returns:
Position tensor of shape [B, 3, seq_len, 2]
"""
latent_coords = self._video_patchifier.get_patch_grid_bounds(
output_shape=VideoLatentShape(
frames=num_frames,
height=height,
width=width,
batch=batch_size,
channels=128, # Video latent channels
),
device=device,
)
# Convert latent coords to pixel coords with causal fix
pixel_coords = get_pixel_coords(
latent_coords=latent_coords,
scale_factors=VIDEO_SCALE_FACTORS,
causal_fix=True,
).to(dtype)
# Scale temporal dimension by 1/fps to get time in seconds
pixel_coords[:, 0, ...] = pixel_coords[:, 0, ...] / fps
return pixel_coords
def _get_audio_positions(
self,
num_time_steps: int,
batch_size: int,
device: torch.device,
dtype: torch.dtype,
) -> Tensor:
"""Generate audio position embeddings using ltx_core's native implementation.
Args:
num_time_steps: Number of audio time steps (T, not T*mel_bins)
batch_size: Batch size
device: Target device
dtype: Target dtype
Returns:
Position tensor of shape [B, 1, num_time_steps, 2]
Note:
Audio latents should be in patchified format [B, T, C*F] = [B, T, 128]
where T is the number of time steps, C=8 channels, F=16 mel bins.
This matches the format produced by AudioPatchifier.patchify().
"""
mel_bins = 16
latent_coords = self._audio_patchifier.get_patch_grid_bounds(
output_shape=AudioLatentShape(
frames=num_time_steps,
mel_bins=mel_bins,
batch=batch_size,
channels=8, # Audio latent channels
),
device=device,
)
return latent_coords.to(dtype)
@staticmethod
def _create_per_token_timesteps(conditioning_mask: Tensor, sampled_sigma: Tensor) -> Tensor:
"""Create per-token timesteps based on conditioning mask.
Args:
conditioning_mask: Boolean mask of shape (batch_size, sequence_length),
where True = conditioning token (timestep=0), False = target token (use sigma)
sampled_sigma: Sampled sigma values of shape (batch_size,) or (batch_size, 1, 1)
Returns:
Timesteps tensor of shape [batch_size, sequence_length]
"""
# Expand to match conditioning mask shape [B, seq_len]
expanded_sigma = sampled_sigma.view(-1, 1).expand_as(conditioning_mask)
# Conditioning tokens get 0, target tokens get the sampled sigma
return torch.where(conditioning_mask, torch.zeros_like(expanded_sigma), expanded_sigma)
@staticmethod
def _create_first_frame_conditioning_mask(
batch_size: int,
sequence_length: int,
height: int,
width: int,
device: torch.device,
first_frame_conditioning_p: float = 0.0,
) -> Tensor:
"""Create conditioning mask for first frame conditioning.
Args:
batch_size: Batch size
sequence_length: Total sequence length
height: Latent height
width: Latent width
device: Target device
first_frame_conditioning_p: Probability of conditioning on the first frame
Returns:
Boolean mask where True indicates first frame tokens (if conditioning is enabled)
"""
conditioning_mask = torch.zeros(batch_size, sequence_length, dtype=torch.bool, device=device)
if first_frame_conditioning_p > 0 and random.random() < first_frame_conditioning_p:
first_frame_end_idx = height * width
if first_frame_end_idx < sequence_length:
conditioning_mask[:, :first_frame_end_idx] = True
return conditioning_mask
@@ -0,0 +1,289 @@
"""Text-to-video training strategy.
This strategy implements standard text-to-video generation training where:
- Only target latents are used (no reference videos)
- Standard noise application and loss computation
- Supports first frame conditioning
- Optionally supports joint audio-video training
"""
from typing import Any, Literal
import torch
from pydantic import Field
from torch import Tensor
from ltx_core.model.transformer.modality import Modality
from ltx_trainer import logger
from ltx_trainer.timestep_samplers import TimestepSampler
from ltx_trainer.training_strategies.base_strategy import (
DEFAULT_FPS,
ModelInputs,
TrainingStrategy,
TrainingStrategyConfigBase,
)
class TextToVideoConfig(TrainingStrategyConfigBase):
"""Configuration for text-to-video training strategy."""
name: Literal["text_to_video"] = "text_to_video"
first_frame_conditioning_p: float = Field(
default=0.1,
description="Probability of conditioning on the first frame during training",
ge=0.0,
le=1.0,
)
with_audio: bool = Field(
default=False,
description="Whether to include audio in training (joint audio-video generation)",
)
audio_latents_dir: str = Field(
default="audio_latents",
description="Directory name for audio latents when with_audio is True",
)
class TextToVideoStrategy(TrainingStrategy):
"""Text-to-video training strategy.
This strategy implements regular video generation training where:
- Only target latents are used (no reference videos)
- Standard noise application and loss computation
- Supports first frame conditioning
- Optionally supports joint audio-video training when with_audio=True
"""
config: TextToVideoConfig
def __init__(self, config: TextToVideoConfig):
"""Initialize strategy with configuration.
Args:
config: Text-to-video configuration
"""
super().__init__(config)
@property
def requires_audio(self) -> bool:
"""Whether this training strategy requires audio components."""
return self.config.with_audio
def get_data_sources(self) -> list[str] | dict[str, str]:
"""
Text-to-video training requires latents and text conditions.
When with_audio is True, also requires audio latents.
"""
sources = {
"latents": "latents",
"conditions": "conditions",
}
if self.config.with_audio:
sources[self.config.audio_latents_dir] = "audio_latents"
return sources
def prepare_training_inputs(
self,
batch: dict[str, Any],
timestep_sampler: TimestepSampler,
) -> ModelInputs:
"""Prepare inputs for text-to-video training."""
# Get pre-encoded latents - dataset provides uniform non-patchified format [B, C, F, H, W]
latents = batch["latents"]
video_latents = latents["latents"]
# Get video dimensions (assume same for all batch elements)
num_frames = latents["num_frames"][0].item()
height = latents["height"][0].item()
width = latents["width"][0].item()
# Patchify latents: [B, C, F, H, W] -> [B, seq_len, C]
video_latents = self._video_patchifier.patchify(video_latents)
# Handle FPS with backward compatibility
fps = latents.get("fps", None)
if fps is not None and not torch.all(fps == fps[0]):
logger.warning(
f"Different FPS values found in the batch. Found: {fps.tolist()}, using the first one: {fps[0].item()}"
)
fps = fps[0].item() if fps is not None else DEFAULT_FPS
# Get text embeddings (already processed by embedding connectors in trainer)
conditions = batch["conditions"]
video_prompt_embeds = conditions["video_prompt_embeds"]
audio_prompt_embeds = conditions["audio_prompt_embeds"]
prompt_attention_mask = conditions["prompt_attention_mask"]
batch_size = video_latents.shape[0]
video_seq_len = video_latents.shape[1]
device = video_latents.device
dtype = video_latents.dtype
# Create conditioning mask (first frame conditioning)
video_conditioning_mask = self._create_first_frame_conditioning_mask(
batch_size=batch_size,
sequence_length=video_seq_len,
height=height,
width=width,
device=device,
first_frame_conditioning_p=self.config.first_frame_conditioning_p,
)
# Sample noise and sigmas
sigmas = timestep_sampler.sample_for(video_latents)
video_noise = torch.randn_like(video_latents)
# Apply noise: noisy = (1 - sigma) * clean + sigma * noise
sigmas_expanded = sigmas.view(-1, 1, 1)
noisy_video = (1 - sigmas_expanded) * video_latents + sigmas_expanded * video_noise
# For conditioning tokens, use clean latents
conditioning_mask_expanded = video_conditioning_mask.unsqueeze(-1)
noisy_video = torch.where(conditioning_mask_expanded, video_latents, noisy_video)
# Compute video targets (velocity prediction)
video_targets = video_noise - video_latents
# Create per-token timesteps
video_timesteps = self._create_per_token_timesteps(video_conditioning_mask, sigmas.squeeze())
# Generate video positions using ltx_core's native implementation
video_positions = self._get_video_positions(
num_frames=num_frames,
height=height,
width=width,
batch_size=batch_size,
fps=fps,
device=device,
dtype=dtype,
)
# Create video Modality
video_modality = Modality(
enabled=True,
latent=noisy_video,
timesteps=video_timesteps,
positions=video_positions,
context=video_prompt_embeds,
context_mask=prompt_attention_mask,
)
# Video loss mask: True for tokens we want to compute loss on (non-conditioning tokens)
video_loss_mask = ~video_conditioning_mask
# Handle audio if enabled
audio_modality = None
audio_targets = None
audio_loss_mask = None
if self.config.with_audio:
audio_modality, audio_targets, audio_loss_mask = self._prepare_audio_inputs(
batch=batch,
sigmas=sigmas,
audio_prompt_embeds=audio_prompt_embeds,
prompt_attention_mask=prompt_attention_mask,
batch_size=batch_size,
device=device,
dtype=dtype,
)
return ModelInputs(
video=video_modality,
audio=audio_modality,
video_targets=video_targets,
audio_targets=audio_targets,
video_loss_mask=video_loss_mask,
audio_loss_mask=audio_loss_mask,
)
def _prepare_audio_inputs(
self,
batch: dict[str, Any],
sigmas: Tensor,
audio_prompt_embeds: Tensor,
prompt_attention_mask: Tensor,
batch_size: int,
device: torch.device,
dtype: torch.dtype,
) -> tuple[Modality, Tensor, Tensor]:
"""Prepare audio inputs for joint audio-video training.
Args:
batch: Raw batch data containing audio_latents
sigmas: Sampled sigma values (same as video)
audio_prompt_embeds: Audio context embeddings
prompt_attention_mask: Attention mask for context
batch_size: Batch size
device: Target device
dtype: Target dtype
Returns:
Tuple of (audio_modality, audio_targets, audio_loss_mask)
"""
# Get audio latents - dataset provides uniform non-patchified format [B, C, T, F]
audio_data = batch["audio_latents"]
audio_latents = audio_data["latents"]
# Patchify audio latents: [B, C, T, F] -> [B, T, C*F]
audio_latents = self._audio_patchifier.patchify(audio_latents)
audio_seq_len = audio_latents.shape[1]
# Sample audio noise
audio_noise = torch.randn_like(audio_latents)
# Apply noise to audio (same sigma as video)
sigmas_expanded = sigmas.view(-1, 1, 1)
noisy_audio = (1 - sigmas_expanded) * audio_latents + sigmas_expanded * audio_noise
# Compute audio targets
audio_targets = audio_noise - audio_latents
# Audio timesteps: all tokens use the sampled sigma (no conditioning mask)
audio_timesteps = sigmas.view(-1, 1).expand(-1, audio_seq_len)
# Generate audio positions
audio_positions = self._get_audio_positions(
num_time_steps=audio_seq_len,
batch_size=batch_size,
device=device,
dtype=dtype,
)
# Create audio Modality
audio_modality = Modality(
enabled=True,
latent=noisy_audio,
timesteps=audio_timesteps,
positions=audio_positions,
context=audio_prompt_embeds,
context_mask=prompt_attention_mask,
)
# Audio loss mask: all tokens contribute to loss (no conditioning)
audio_loss_mask = torch.ones(batch_size, audio_seq_len, dtype=torch.bool, device=device)
return audio_modality, audio_targets, audio_loss_mask
def compute_loss(
self,
video_pred: Tensor,
audio_pred: Tensor | None,
inputs: ModelInputs,
) -> Tensor:
"""Compute masked MSE loss for video and optionally audio."""
# Video loss
video_loss = (video_pred - inputs.video_targets).pow(2)
video_loss_mask = inputs.video_loss_mask.unsqueeze(-1).float()
video_loss = video_loss.mul(video_loss_mask).div(video_loss_mask.mean())
video_loss = video_loss.mean()
# If no audio, return video loss only
if not self.config.with_audio or audio_pred is None or inputs.audio_targets is None:
return video_loss
# Audio loss (no conditioning mask)
audio_loss = (audio_pred - inputs.audio_targets).pow(2).mean()
# Combined loss
return video_loss + audio_loss
@@ -0,0 +1,223 @@
"""Video-to-video training strategy for IC-LoRA.
This strategy implements training with reference video conditioning where:
- Reference latents (clean) are concatenated with target latents (noised)
- Video coordinates handle both reference and target sequences
- Loss is computed only on the target portion
"""
from typing import Any, Literal
import torch
from pydantic import Field
from torch import Tensor
from ltx_core.model.transformer.modality import Modality
from ltx_trainer import logger
from ltx_trainer.timestep_samplers import TimestepSampler
from ltx_trainer.training_strategies.base_strategy import (
DEFAULT_FPS,
ModelInputs,
TrainingStrategy,
TrainingStrategyConfigBase,
)
class VideoToVideoConfig(TrainingStrategyConfigBase):
"""Configuration for video-to-video (IC-LoRA) training strategy."""
name: Literal["video_to_video"] = "video_to_video"
first_frame_conditioning_p: float = Field(
default=0.1,
description="Probability of conditioning on the first frame during training",
ge=0.0,
le=1.0,
)
reference_latents_dir: str = Field(
default="reference_latents",
description="Directory name for latents of reference videos",
)
class VideoToVideoStrategy(TrainingStrategy):
"""Video-to-video training strategy for IC-LoRA.
This strategy implements training with reference video conditioning where:
- Reference latents (clean) are concatenated with target latents (noised)
- Video coordinates handle both reference and target sequences
- Loss is computed only on the target portion
"""
config: VideoToVideoConfig
def __init__(self, config: VideoToVideoConfig):
"""Initialize strategy with configuration.
Args:
config: Video-to-video configuration
"""
super().__init__(config)
def get_data_sources(self) -> dict[str, str]:
"""IC-LoRA training requires latents, conditions, and reference latents."""
return {
"latents": "latents",
"conditions": "conditions",
self.config.reference_latents_dir: "ref_latents",
}
def prepare_training_inputs(
self,
batch: dict[str, Any],
timestep_sampler: TimestepSampler,
) -> ModelInputs:
"""Prepare inputs for IC-LoRA training with reference videos."""
# Get pre-encoded latents - dataset provides uniform non-patchified format [B, C, F, H, W]
latents = batch["latents"]
target_latents = latents["latents"]
ref_latents = batch["ref_latents"]["latents"]
# Get dimensions
num_frames = latents["num_frames"][0].item()
height = latents["height"][0].item()
width = latents["width"][0].item()
ref_latents_info = batch["ref_latents"]
ref_frames = ref_latents_info["num_frames"][0].item()
ref_height = ref_latents_info["height"][0].item()
ref_width = ref_latents_info["width"][0].item()
# Patchify latents: [B, C, F, H, W] -> [B, seq_len, C]
target_latents = self._video_patchifier.patchify(target_latents)
ref_latents = self._video_patchifier.patchify(ref_latents)
# Handle FPS
fps = latents.get("fps", None)
if fps is not None and not torch.all(fps == fps[0]):
logger.warning(
f"Different FPS values found in the batch. Found: {fps.tolist()}, using the first one: {fps[0].item()}"
)
fps = fps[0].item() if fps is not None else DEFAULT_FPS
# Get text embeddings (already processed by embedding connectors in trainer)
# Video-to-video uses only video embeddings
conditions = batch["conditions"]
prompt_embeds = conditions["video_prompt_embeds"]
prompt_attention_mask = conditions["prompt_attention_mask"]
batch_size = target_latents.shape[0]
ref_seq_len = ref_latents.shape[1]
target_seq_len = target_latents.shape[1]
device = target_latents.device
dtype = target_latents.dtype
# Create conditioning mask
# Reference tokens are always conditioning (timestep=0)
ref_conditioning_mask = torch.ones(batch_size, ref_seq_len, dtype=torch.bool, device=device)
# Target tokens: check for first frame conditioning
target_conditioning_mask = self._create_first_frame_conditioning_mask(
batch_size=batch_size,
sequence_length=target_seq_len,
height=height,
width=width,
device=device,
first_frame_conditioning_p=self.config.first_frame_conditioning_p,
)
# Combined conditioning mask
conditioning_mask = torch.cat([ref_conditioning_mask, target_conditioning_mask], dim=1)
# Sample noise and sigmas for target
sigmas = timestep_sampler.sample_for(target_latents)
noise = torch.randn_like(target_latents)
sigmas_expanded = sigmas.view(-1, 1, 1)
# Apply noise to target
noisy_target = (1 - sigmas_expanded) * target_latents + sigmas_expanded * noise
# For first frame conditioning in target, use clean latents
target_conditioning_mask_expanded = target_conditioning_mask.unsqueeze(-1)
noisy_target = torch.where(target_conditioning_mask_expanded, target_latents, noisy_target)
# Targets for loss computation
targets = noise - target_latents
# Concatenate reference (clean) and target (noisy)
combined_latents = torch.cat([ref_latents, noisy_target], dim=1)
# Create per-token timesteps
timesteps = self._create_per_token_timesteps(conditioning_mask, sigmas.squeeze())
# Generate positions for reference and target separately, then concatenate
ref_positions = self._get_video_positions(
num_frames=ref_frames,
height=ref_height,
width=ref_width,
batch_size=batch_size,
fps=fps,
device=device,
dtype=dtype,
)
target_positions = self._get_video_positions(
num_frames=num_frames,
height=height,
width=width,
batch_size=batch_size,
fps=fps,
device=device,
dtype=dtype,
)
# Concatenate positions along sequence dimension
positions = torch.cat([ref_positions, target_positions], dim=2)
# Create video Modality
video_modality = Modality(
enabled=True,
latent=combined_latents,
timesteps=timesteps,
positions=positions,
context=prompt_embeds,
context_mask=prompt_attention_mask,
)
# Loss mask: only compute loss on non-conditioning target tokens
# Reference tokens: all False (no loss)
# Target tokens: True where not conditioning
ref_loss_mask = torch.zeros(batch_size, ref_seq_len, dtype=torch.bool, device=device)
target_loss_mask = ~target_conditioning_mask
video_loss_mask = torch.cat([ref_loss_mask, target_loss_mask], dim=1)
return ModelInputs(
video=video_modality,
audio=None,
video_targets=targets,
audio_targets=None,
video_loss_mask=video_loss_mask,
audio_loss_mask=None,
ref_seq_len=ref_seq_len,
)
def compute_loss(
self,
video_pred: Tensor,
_audio_pred: Tensor | None,
inputs: ModelInputs,
) -> Tensor:
"""Compute masked loss only on target portion."""
# Extract target portion of prediction
ref_seq_len = inputs.ref_seq_len
target_pred = video_pred[:, ref_seq_len:, :]
# Get target portion of loss mask
target_loss_mask = inputs.video_loss_mask[:, ref_seq_len:]
# Compute loss
loss = (target_pred - inputs.video_targets).pow(2)
# Apply loss mask
loss_mask = target_loss_mask.unsqueeze(-1).float()
loss = loss.mul(loss_mask).div(loss_mask.mean())
return loss.mean()