Automated PR - 2026-01-15
This commit is contained in:
@@ -176,7 +176,7 @@ acceleration:
|
|||||||
|
|
||||||
# Load text encoder in 8-bit precision to save memory
|
# Load text encoder in 8-bit precision to save memory
|
||||||
# Useful when GPU memory is limited
|
# Useful when GPU memory is limited
|
||||||
load_text_encoder_in_8bit: false
|
load_text_encoder_in_8bit: true
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Data Configuration
|
# Data Configuration
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ sub-configurations:
|
|||||||
Check out our example configurations in the `configs` directory:
|
Check out our example configurations in the `configs` directory:
|
||||||
|
|
||||||
- 📄 [Audio-Video LoRA Training](../configs/ltx2_av_lora.yaml) - Joint audio-video generation training
|
- 📄 [Audio-Video LoRA Training](../configs/ltx2_av_lora.yaml) - Joint audio-video generation training
|
||||||
- 📄 [Audio-Video LoRA Training (Low VRAM)](../configs/ltx2_av_lora_low_vram.yaml) - Memory-optimized config for 32GB GPUs (uses 8-bit optimizer, INT8 quantization, and reduced LoRA rank)
|
- 📄 [Audio-Video LoRA Training (Low VRAM)](../configs/ltx2_av_lora_low_vram.yaml) - Memory-optimized config for 32GB
|
||||||
|
GPUs (uses 8-bit optimizer, INT8 quantization, and reduced LoRA rank)
|
||||||
- 📄 [IC-LoRA Training](../configs/ltx2_v2v_ic_lora.yaml) - Video-to-video transformation training
|
- 📄 [IC-LoRA Training](../configs/ltx2_v2v_ic_lora.yaml) - Video-to-video transformation training
|
||||||
|
|
||||||
## ⚙️ Configuration Sections
|
## ⚙️ Configuration Sections
|
||||||
@@ -300,10 +301,10 @@ checkpoints:
|
|||||||
|
|
||||||
**Key parameters:**
|
**Key parameters:**
|
||||||
|
|
||||||
| Parameter | Description |
|
| Parameter | Description |
|
||||||
|---------------|------------------------------------------------------------------------|
|
|---------------|-------------------------------------------------------------------------------|
|
||||||
| `interval` | Steps between intermediate checkpoint saves (set to `null` to disable) |
|
| `interval` | Steps between intermediate checkpoint saves (set to `null` to disable) |
|
||||||
| `keep_last_n` | Number of most recent checkpoints to keep (-1 = keep all) |
|
| `keep_last_n` | Number of most recent checkpoints to keep (-1 = keep all) |
|
||||||
| `precision` | Precision for saved checkpoint weights: `"bfloat16"` (default) or `"float32"` |
|
| `precision` | Precision for saved checkpoint weights: `"bfloat16"` (default) or `"float32"` |
|
||||||
|
|
||||||
### HubConfig
|
### HubConfig
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import torch
|
|||||||
import torchaudio
|
import torchaudio
|
||||||
import torchvision.utils
|
import torchvision.utils
|
||||||
import typer
|
import typer
|
||||||
|
from einops import rearrange
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.progress import (
|
from rich.progress import (
|
||||||
BarColumn,
|
BarColumn,
|
||||||
@@ -27,10 +28,16 @@ from rich.progress import (
|
|||||||
)
|
)
|
||||||
from transformers.utils.logging import disable_progress_bar
|
from transformers.utils.logging import disable_progress_bar
|
||||||
|
|
||||||
|
from ltx_core.model.video_vae import SpatialTilingConfig, TemporalTilingConfig, TilingConfig
|
||||||
from ltx_trainer import logger
|
from ltx_trainer import logger
|
||||||
from ltx_trainer.model_loader import load_audio_vae_decoder, load_video_vae_decoder, load_vocoder
|
from ltx_trainer.model_loader import load_audio_vae_decoder, load_video_vae_decoder, load_vocoder
|
||||||
from ltx_trainer.video_utils import save_video
|
from ltx_trainer.video_utils import save_video
|
||||||
|
|
||||||
|
DEFAULT_TILE_SIZE_PIXELS = 512 # Spatial tile size in pixels (must be ≥64 and divisible by 32)
|
||||||
|
DEFAULT_TILE_OVERLAP_PIXELS = 128 # Spatial tile overlap in pixels (must be divisible by 32)
|
||||||
|
DEFAULT_TILE_SIZE_FRAMES = 128 # Temporal tile size in frames (must be ≥16 and divisible by 8)
|
||||||
|
DEFAULT_TILE_OVERLAP_FRAMES = 24 # Temporal tile overlap in frames (must be divisible by 8)
|
||||||
|
|
||||||
disable_progress_bar()
|
disable_progress_bar()
|
||||||
console = Console()
|
console = Console()
|
||||||
app = typer.Typer(
|
app = typer.Typer(
|
||||||
@@ -60,19 +67,15 @@ class LatentsDecoder:
|
|||||||
self.vae = None
|
self.vae = None
|
||||||
self.audio_vae = None
|
self.audio_vae = None
|
||||||
self.vocoder = None
|
self.vocoder = None
|
||||||
self._load_model(model_path, vae_tiling, with_audio)
|
self.vae_tiling = vae_tiling
|
||||||
|
|
||||||
def _load_model(self, model_path: str, vae_tiling: bool, with_audio: bool = False) -> None:
|
self._load_model(model_path, with_audio)
|
||||||
|
|
||||||
|
def _load_model(self, model_path: str, with_audio: bool = False) -> None:
|
||||||
"""Initialize and load the VAE model(s)."""
|
"""Initialize and load the VAE model(s)."""
|
||||||
with console.status(f"[bold]Loading video VAE decoder from {model_path}...", spinner="dots"):
|
with console.status(f"[bold]Loading video VAE decoder from {model_path}...", spinner="dots"):
|
||||||
self.vae = load_video_vae_decoder(model_path, device=self.device, dtype=torch.bfloat16)
|
self.vae = load_video_vae_decoder(model_path, device=self.device, dtype=torch.bfloat16)
|
||||||
|
|
||||||
if vae_tiling:
|
|
||||||
logger.warning(
|
|
||||||
"VAE tiling is not yet implemented in this script. "
|
|
||||||
"Continuing without tiling - this may cause OOM errors for large resolutions."
|
|
||||||
)
|
|
||||||
|
|
||||||
if with_audio:
|
if with_audio:
|
||||||
with console.status(f"[bold]Loading audio VAE decoder from {model_path}...", spinner="dots"):
|
with console.status(f"[bold]Loading audio VAE decoder from {model_path}...", spinner="dots"):
|
||||||
self.audio_vae = load_audio_vae_decoder(model_path, device=self.device, dtype=torch.bfloat16)
|
self.audio_vae = load_audio_vae_decoder(model_path, device=self.device, dtype=torch.bfloat16)
|
||||||
@@ -125,61 +128,6 @@ class LatentsDecoder:
|
|||||||
|
|
||||||
logger.info(f"Decoding complete! Videos saved to {output_dir}")
|
logger.info(f"Decoding complete! Videos saved to {output_dir}")
|
||||||
|
|
||||||
def _process_file(self, latent_file: Path, output_dir: Path, seed: int | None) -> None:
|
|
||||||
"""Process a single latent file."""
|
|
||||||
# Load the latent data
|
|
||||||
data = torch.load(latent_file, map_location=self.device, weights_only=False)
|
|
||||||
|
|
||||||
# Get latents - handle both old patchified [seq_len, C] and new [C, F, H, W] formats
|
|
||||||
latents = data["latents"]
|
|
||||||
num_frames = data["num_frames"]
|
|
||||||
height = data["height"]
|
|
||||||
width = data["width"]
|
|
||||||
|
|
||||||
# Check if latents need reshaping (old patchified format)
|
|
||||||
if latents.dim() == 2:
|
|
||||||
# Old format: [seq_len, C] -> reshape to [C, F, H, W]
|
|
||||||
_seq_len, channels = latents.shape
|
|
||||||
latents = latents.reshape(num_frames, height, width, channels)
|
|
||||||
latents = latents.permute(3, 0, 1, 2) # [F, H, W, C] -> [C, F, H, W]
|
|
||||||
|
|
||||||
# Add batch dimension: [C, F, H, W] -> [1, C, F, H, W]
|
|
||||||
latents = latents.unsqueeze(0).to(device=self.device, dtype=torch.bfloat16)
|
|
||||||
|
|
||||||
# Create generator only if seed is provided
|
|
||||||
generator = None
|
|
||||||
if seed is not None:
|
|
||||||
generator = torch.Generator(device=self.device)
|
|
||||||
generator.manual_seed(seed)
|
|
||||||
|
|
||||||
# Decode the video (VAE decoder uses forward/call, not decode method)
|
|
||||||
video = self.vae(latents) # [B, C, F, H, W]
|
|
||||||
|
|
||||||
# Convert to [F, C, H, W] format and normalize to [0, 1]
|
|
||||||
video = video[0] # Remove batch dimension -> [C, F, H, W]
|
|
||||||
video = video.permute(1, 0, 2, 3) # [C, F, H, W] -> [F, C, H, W]
|
|
||||||
video = (video + 1) / 2 # Denormalize from [-1, 1] to [0, 1]
|
|
||||||
video = video.clamp(0, 1)
|
|
||||||
|
|
||||||
# Determine output format and save
|
|
||||||
is_image = video.shape[0] == 1
|
|
||||||
if is_image:
|
|
||||||
# Save as PNG for single frame
|
|
||||||
output_path = output_dir / f"{latent_file.stem}.png"
|
|
||||||
torchvision.utils.save_image(
|
|
||||||
video[0], # [C, H, W] in [0, 1]
|
|
||||||
str(output_path),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Save as MP4 for video using PyAV-based save_video
|
|
||||||
output_path = output_dir / f"{latent_file.stem}.mp4"
|
|
||||||
fps = data.get("fps", 24) # Use stored FPS or default to 24
|
|
||||||
save_video(
|
|
||||||
video_tensor=video, # [F, C, H, W] in [0, 1]
|
|
||||||
output_path=output_path,
|
|
||||||
fps=fps,
|
|
||||||
)
|
|
||||||
|
|
||||||
@torch.inference_mode()
|
@torch.inference_mode()
|
||||||
def decode_audio(self, latents_dir: Path, output_dir: Path) -> None:
|
def decode_audio(self, latents_dir: Path, output_dir: Path) -> None:
|
||||||
"""Decode all audio latent files in the directory recursively.
|
"""Decode all audio latent files in the directory recursively.
|
||||||
@@ -229,6 +177,87 @@ class LatentsDecoder:
|
|||||||
|
|
||||||
logger.info(f"Audio decoding complete! Audio files saved to {output_dir}")
|
logger.info(f"Audio decoding complete! Audio files saved to {output_dir}")
|
||||||
|
|
||||||
|
def _process_file(self, latent_file: Path, output_dir: Path, seed: int | None) -> None:
|
||||||
|
"""Process a single latent file."""
|
||||||
|
# Load the latent data
|
||||||
|
data = torch.load(latent_file, map_location=self.device, weights_only=False)
|
||||||
|
|
||||||
|
# Get latents - handle both old patchified [seq_len, C] and new [C, F, H, W] formats
|
||||||
|
latents = data["latents"]
|
||||||
|
num_frames = data["num_frames"]
|
||||||
|
height = data["height"]
|
||||||
|
width = data["width"]
|
||||||
|
|
||||||
|
# Check if latents need reshaping (old patchified format)
|
||||||
|
if latents.dim() == 2:
|
||||||
|
# Old format: [seq_len, C] -> reshape to [C, F, H, W]
|
||||||
|
latents = rearrange(latents, "(f h w) c -> c f h w", f=num_frames, h=height, w=width)
|
||||||
|
|
||||||
|
# Add batch dimension: [C, F, H, W] -> [1, C, F, H, W]
|
||||||
|
latents = latents.unsqueeze(0).to(device=self.device, dtype=torch.bfloat16)
|
||||||
|
|
||||||
|
# Create generator only if seed is provided
|
||||||
|
generator = None
|
||||||
|
if seed is not None:
|
||||||
|
generator = torch.Generator(device=self.device)
|
||||||
|
generator.manual_seed(seed)
|
||||||
|
|
||||||
|
# Decode the video
|
||||||
|
video = self._decode_video(latents, generator)
|
||||||
|
|
||||||
|
# Determine output format and save
|
||||||
|
is_image = video.shape[0] == 1
|
||||||
|
if is_image:
|
||||||
|
# Save as PNG for single frame
|
||||||
|
output_path = output_dir / f"{latent_file.stem}.png"
|
||||||
|
torchvision.utils.save_image(
|
||||||
|
video[0], # [C, H, W] in [0, 1]
|
||||||
|
str(output_path),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Save as MP4 for video using PyAV-based save_video
|
||||||
|
output_path = output_dir / f"{latent_file.stem}.mp4"
|
||||||
|
fps = data.get("fps", 24) # Use stored FPS or default to 24
|
||||||
|
save_video(
|
||||||
|
video_tensor=video, # [F, C, H, W] in [0, 1]
|
||||||
|
output_path=output_path,
|
||||||
|
fps=fps,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _decode_video(self, latents: torch.Tensor, generator: torch.Generator | None = None) -> torch.Tensor:
|
||||||
|
"""Decode latents to video frames."""
|
||||||
|
if self.vae_tiling:
|
||||||
|
# Use tiled decoding for reduced VRAM
|
||||||
|
tiling_config = TilingConfig(
|
||||||
|
spatial_config=SpatialTilingConfig(
|
||||||
|
tile_size_in_pixels=DEFAULT_TILE_SIZE_PIXELS,
|
||||||
|
tile_overlap_in_pixels=DEFAULT_TILE_OVERLAP_PIXELS,
|
||||||
|
),
|
||||||
|
temporal_config=TemporalTilingConfig(
|
||||||
|
tile_size_in_frames=DEFAULT_TILE_SIZE_FRAMES,
|
||||||
|
tile_overlap_in_frames=DEFAULT_TILE_OVERLAP_FRAMES,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
chunks = list(
|
||||||
|
self.vae.tiled_decode(
|
||||||
|
latents,
|
||||||
|
tiling_config=tiling_config,
|
||||||
|
generator=generator,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Concatenate along temporal dimension
|
||||||
|
video = torch.cat(chunks, dim=2) # [B, C, F, H, W]
|
||||||
|
else:
|
||||||
|
# Standard full decoding
|
||||||
|
video = self.vae(latents, generator=generator) # [B, C, F, H, W]
|
||||||
|
|
||||||
|
# Convert to [F, C, H, W] format and normalize to [0, 1]
|
||||||
|
video = rearrange(video, "1 c f h w -> f c h w")
|
||||||
|
video = (video + 1) / 2 # Denormalize from [-1, 1] to [0, 1]
|
||||||
|
video = video.clamp(0, 1)
|
||||||
|
|
||||||
|
return video
|
||||||
|
|
||||||
def _process_audio_file(self, latent_file: Path, output_dir: Path) -> None:
|
def _process_audio_file(self, latent_file: Path, output_dir: Path) -> None:
|
||||||
"""Process a single audio latent file."""
|
"""Process a single audio latent file."""
|
||||||
# Load the latent data
|
# Load the latent data
|
||||||
@@ -242,8 +271,7 @@ class LatentsDecoder:
|
|||||||
if latents.dim() == 2:
|
if latents.dim() == 2:
|
||||||
# Old format: [seq_len, channels] where seq_len = time * freq
|
# Old format: [seq_len, channels] where seq_len = time * freq
|
||||||
# Reshape to [C, T, F]
|
# Reshape to [C, T, F]
|
||||||
latents = latents.reshape(num_time_steps, freq_bins, -1) # [T, F, C]
|
latents = rearrange(latents, "(t f) c -> c t f", t=num_time_steps, f=freq_bins)
|
||||||
latents = latents.permute(2, 0, 1) # [T, F, C] -> [C, T, F]
|
|
||||||
|
|
||||||
# Add batch dimension: [C, T, F] -> [1, C, T, F]
|
# Add batch dimension: [C, T, F] -> [1, C, T, F]
|
||||||
latents = latents.unsqueeze(0)
|
latents = latents.unsqueeze(0)
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ class CaptionsDataset(Dataset):
|
|||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def compute_captions_embeddings(
|
def compute_captions_embeddings( # noqa: PLR0913
|
||||||
dataset_file: str | Path,
|
dataset_file: str | Path,
|
||||||
output_dir: str,
|
output_dir: str,
|
||||||
model_path: str,
|
model_path: str,
|
||||||
@@ -231,6 +231,7 @@ def compute_captions_embeddings(
|
|||||||
remove_llm_prefixes: bool = False,
|
remove_llm_prefixes: bool = False,
|
||||||
batch_size: int = 8,
|
batch_size: int = 8,
|
||||||
device: str = "cuda",
|
device: str = "cuda",
|
||||||
|
load_in_8bit: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Process captions and save text embeddings.
|
Process captions and save text embeddings.
|
||||||
@@ -245,6 +246,7 @@ def compute_captions_embeddings(
|
|||||||
remove_llm_prefixes: Whether to remove common LLM-generated prefixes
|
remove_llm_prefixes: Whether to remove common LLM-generated prefixes
|
||||||
batch_size: Batch size for processing
|
batch_size: Batch size for processing
|
||||||
device: Device to use for computation
|
device: Device to use for computation
|
||||||
|
load_in_8bit: Whether to load the Gemma text encoder in 8-bit precision
|
||||||
"""
|
"""
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
@@ -264,7 +266,13 @@ def compute_captions_embeddings(
|
|||||||
|
|
||||||
# Load text encoder
|
# Load text encoder
|
||||||
with console.status("[bold]Loading Gemma text encoder...", spinner="dots"):
|
with console.status("[bold]Loading Gemma text encoder...", spinner="dots"):
|
||||||
text_encoder = load_text_encoder(model_path, text_encoder_path, device=device, dtype=torch.bfloat16)
|
text_encoder = load_text_encoder(
|
||||||
|
model_path,
|
||||||
|
text_encoder_path,
|
||||||
|
device=device,
|
||||||
|
dtype=torch.bfloat16,
|
||||||
|
load_in_8bit=load_in_8bit,
|
||||||
|
)
|
||||||
|
|
||||||
logger.info("Text encoder loaded successfully")
|
logger.info("Text encoder loaded successfully")
|
||||||
|
|
||||||
@@ -326,7 +334,7 @@ def compute_captions_embeddings(
|
|||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def main(
|
def main( # noqa: PLR0913
|
||||||
dataset_file: str = typer.Argument(
|
dataset_file: str = typer.Argument(
|
||||||
...,
|
...,
|
||||||
help="Path to metadata file (CSV/JSON/JSONL) containing captions and media paths",
|
help="Path to metadata file (CSV/JSON/JSONL) containing captions and media paths",
|
||||||
@@ -368,6 +376,10 @@ def main(
|
|||||||
default=False,
|
default=False,
|
||||||
help="Remove common LLM-generated prefixes from captions",
|
help="Remove common LLM-generated prefixes from captions",
|
||||||
),
|
),
|
||||||
|
load_text_encoder_in_8bit: bool = typer.Option(
|
||||||
|
default=False,
|
||||||
|
help="Load the Gemma text encoder in 8-bit precision to save GPU memory (requires bitsandbytes)",
|
||||||
|
),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Process text captions and save embeddings for video generation training.
|
"""Process text captions and save embeddings for video generation training.
|
||||||
This script processes captions from metadata files and saves text embeddings
|
This script processes captions from metadata files and saves text embeddings
|
||||||
@@ -408,6 +420,7 @@ def main(
|
|||||||
remove_llm_prefixes=remove_llm_prefixes,
|
remove_llm_prefixes=remove_llm_prefixes,
|
||||||
batch_size=batch_size,
|
batch_size=batch_size,
|
||||||
device=device,
|
device=device,
|
||||||
|
load_in_8bit=load_text_encoder_in_8bit,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from process_videos import compute_latents, parse_resolution_buckets
|
|||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
from ltx_trainer import logger
|
from ltx_trainer import logger
|
||||||
|
from ltx_trainer.gpu_utils import free_gpu_memory_context
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
app = typer.Typer(
|
app = typer.Typer(
|
||||||
@@ -46,15 +47,9 @@ def preprocess_dataset( # noqa: PLR0913
|
|||||||
remove_llm_prefixes: bool = False,
|
remove_llm_prefixes: bool = False,
|
||||||
reference_column: str | None = None,
|
reference_column: str | None = None,
|
||||||
with_audio: bool = False,
|
with_audio: bool = False,
|
||||||
|
load_text_encoder_in_8bit: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Run the preprocessing pipeline with the given arguments."""
|
"""Run the preprocessing pipeline with the given arguments."""
|
||||||
# VAE tiling is not yet implemented
|
|
||||||
if vae_tiling:
|
|
||||||
logger.warning(
|
|
||||||
"VAE tiling is not yet implemented in this script. "
|
|
||||||
"Continuing without tiling - this may cause OOM errors for large resolutions."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validate dataset file
|
# Validate dataset file
|
||||||
_validate_dataset_file(dataset_file)
|
_validate_dataset_file(dataset_file)
|
||||||
|
|
||||||
@@ -66,19 +61,21 @@ def preprocess_dataset( # noqa: PLR0913
|
|||||||
if lora_trigger:
|
if lora_trigger:
|
||||||
logger.info(f'LoRA trigger word "{lora_trigger}" will be prepended to all captions')
|
logger.info(f'LoRA trigger word "{lora_trigger}" will be prepended to all captions')
|
||||||
|
|
||||||
# Process captions using the dedicated function
|
with free_gpu_memory_context():
|
||||||
compute_captions_embeddings(
|
# Process captions using the dedicated function
|
||||||
dataset_file=dataset_file,
|
compute_captions_embeddings(
|
||||||
output_dir=str(conditions_dir),
|
dataset_file=dataset_file,
|
||||||
model_path=model_path,
|
output_dir=str(conditions_dir),
|
||||||
text_encoder_path=text_encoder_path,
|
model_path=model_path,
|
||||||
caption_column=caption_column,
|
text_encoder_path=text_encoder_path,
|
||||||
media_column=video_column,
|
caption_column=caption_column,
|
||||||
lora_trigger=lora_trigger,
|
media_column=video_column,
|
||||||
remove_llm_prefixes=remove_llm_prefixes,
|
lora_trigger=lora_trigger,
|
||||||
batch_size=batch_size,
|
remove_llm_prefixes=remove_llm_prefixes,
|
||||||
device=device,
|
batch_size=batch_size,
|
||||||
)
|
device=device,
|
||||||
|
load_in_8bit=load_text_encoder_in_8bit,
|
||||||
|
)
|
||||||
|
|
||||||
# Process videos using the dedicated function
|
# Process videos using the dedicated function
|
||||||
audio_latents_dir = None
|
audio_latents_dir = None
|
||||||
@@ -86,36 +83,37 @@ def preprocess_dataset( # noqa: PLR0913
|
|||||||
logger.info("Audio preprocessing enabled - will extract and encode audio from videos")
|
logger.info("Audio preprocessing enabled - will extract and encode audio from videos")
|
||||||
audio_latents_dir = output_base / "audio_latents"
|
audio_latents_dir = output_base / "audio_latents"
|
||||||
|
|
||||||
compute_latents(
|
with free_gpu_memory_context():
|
||||||
dataset_file=dataset_file,
|
|
||||||
video_column=video_column,
|
|
||||||
resolution_buckets=resolution_buckets,
|
|
||||||
output_dir=str(latents_dir),
|
|
||||||
model_path=model_path,
|
|
||||||
batch_size=batch_size,
|
|
||||||
device=device,
|
|
||||||
vae_tiling=vae_tiling,
|
|
||||||
with_audio=with_audio,
|
|
||||||
audio_output_dir=str(audio_latents_dir) if audio_latents_dir else None,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Process reference videos if reference_column is provided
|
|
||||||
if reference_column:
|
|
||||||
logger.info("Processing reference videos for IC-LoRA training...")
|
|
||||||
reference_latents_dir = output_base / "reference_latents"
|
|
||||||
|
|
||||||
compute_latents(
|
compute_latents(
|
||||||
dataset_file=dataset_file,
|
dataset_file=dataset_file,
|
||||||
main_media_column=video_column,
|
video_column=video_column,
|
||||||
video_column=reference_column,
|
|
||||||
resolution_buckets=resolution_buckets,
|
resolution_buckets=resolution_buckets,
|
||||||
output_dir=str(reference_latents_dir),
|
output_dir=str(latents_dir),
|
||||||
model_path=model_path,
|
model_path=model_path,
|
||||||
batch_size=batch_size,
|
batch_size=batch_size,
|
||||||
device=device,
|
device=device,
|
||||||
vae_tiling=vae_tiling,
|
vae_tiling=vae_tiling,
|
||||||
|
with_audio=with_audio,
|
||||||
|
audio_output_dir=str(audio_latents_dir) if audio_latents_dir else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Process reference videos if reference_column is provided
|
||||||
|
if reference_column:
|
||||||
|
logger.info("Processing reference videos for IC-LoRA training...")
|
||||||
|
reference_latents_dir = output_base / "reference_latents"
|
||||||
|
|
||||||
|
compute_latents(
|
||||||
|
dataset_file=dataset_file,
|
||||||
|
main_media_column=video_column,
|
||||||
|
video_column=reference_column,
|
||||||
|
resolution_buckets=resolution_buckets,
|
||||||
|
output_dir=str(reference_latents_dir),
|
||||||
|
model_path=model_path,
|
||||||
|
batch_size=batch_size,
|
||||||
|
device=device,
|
||||||
|
vae_tiling=vae_tiling,
|
||||||
|
)
|
||||||
|
|
||||||
# Handle decoding if requested (for verification)
|
# Handle decoding if requested (for verification)
|
||||||
if decode:
|
if decode:
|
||||||
logger.info("Decoding latents for verification...")
|
logger.info("Decoding latents for verification...")
|
||||||
@@ -224,6 +222,10 @@ def main( # noqa: PLR0913
|
|||||||
default=False,
|
default=False,
|
||||||
help="Extract and encode audio from video files",
|
help="Extract and encode audio from video files",
|
||||||
),
|
),
|
||||||
|
load_text_encoder_in_8bit: bool = typer.Option(
|
||||||
|
default=False,
|
||||||
|
help="Load the Gemma text encoder in 8-bit precision to save GPU memory (requires bitsandbytes)",
|
||||||
|
),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Preprocess a video dataset by computing and saving latents and text embeddings.
|
"""Preprocess a video dataset by computing and saving latents and text embeddings.
|
||||||
The dataset must be a CSV, JSON, or JSONL file with columns for captions and video paths.
|
The dataset must be a CSV, JSON, or JSONL file with columns for captions and video paths.
|
||||||
@@ -269,6 +271,7 @@ def main( # noqa: PLR0913
|
|||||||
remove_llm_prefixes=remove_llm_prefixes,
|
remove_llm_prefixes=remove_llm_prefixes,
|
||||||
reference_column=reference_column,
|
reference_column=reference_column,
|
||||||
with_audio=with_audio,
|
with_audio=with_audio,
|
||||||
|
load_text_encoder_in_8bit=load_text_encoder_in_8bit,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ VAE_TEMPORAL_FACTOR = 8
|
|||||||
AUDIO_LATENT_CHANNELS = 8
|
AUDIO_LATENT_CHANNELS = 8
|
||||||
AUDIO_FREQUENCY_BINS = 16
|
AUDIO_FREQUENCY_BINS = 16
|
||||||
|
|
||||||
|
DEFAULT_TILE_SIZE = 512 # Spatial tile size in pixels (must be ≥64 and divisible by 32)
|
||||||
|
DEFAULT_TILE_OVERLAP = 128 # Spatial tile overlap in pixels (must be divisible by 32)
|
||||||
|
|
||||||
app = typer.Typer(
|
app = typer.Typer(
|
||||||
pretty_exceptions_enable=False,
|
pretty_exceptions_enable=False,
|
||||||
no_args_is_help=True,
|
no_args_is_help=True,
|
||||||
@@ -488,12 +491,6 @@ def compute_latents( # noqa: PLR0913, PLR0915
|
|||||||
with console.status(f"[bold]Loading video VAE encoder from [cyan]{model_path}[/]...", spinner="dots"):
|
with console.status(f"[bold]Loading video VAE encoder from [cyan]{model_path}[/]...", spinner="dots"):
|
||||||
vae = load_video_vae_encoder(model_path, device=torch_device, dtype=torch.bfloat16)
|
vae = load_video_vae_encoder(model_path, device=torch_device, dtype=torch.bfloat16)
|
||||||
|
|
||||||
if vae_tiling:
|
|
||||||
logger.warning(
|
|
||||||
"VAE tiling is not yet implemented in this script. "
|
|
||||||
"Continuing without tiling - this may cause OOM errors for large resolutions."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Load audio VAE encoder and audio processor if needed
|
# Load audio VAE encoder and audio processor if needed
|
||||||
audio_vae_encoder = None
|
audio_vae_encoder = None
|
||||||
audio_processor = None
|
audio_processor = None
|
||||||
@@ -543,7 +540,7 @@ def compute_latents( # noqa: PLR0913, PLR0915
|
|||||||
|
|
||||||
# Encode video
|
# Encode video
|
||||||
with torch.inference_mode():
|
with torch.inference_mode():
|
||||||
video_latent_data = encode_video(vae=vae, video=video)
|
video_latent_data = encode_video(vae=vae, video=video, use_tiling=vae_tiling)
|
||||||
|
|
||||||
# Save latents for each item in batch
|
# Save latents for each item in batch
|
||||||
for i in range(len(batch["relative_path"])):
|
for i in range(len(batch["relative_path"])):
|
||||||
@@ -611,6 +608,9 @@ def encode_video(
|
|||||||
vae: torch.nn.Module,
|
vae: torch.nn.Module,
|
||||||
video: torch.Tensor,
|
video: torch.Tensor,
|
||||||
dtype: torch.dtype | None = None,
|
dtype: torch.dtype | None = None,
|
||||||
|
use_tiling: bool = False,
|
||||||
|
tile_size: int = DEFAULT_TILE_SIZE,
|
||||||
|
tile_overlap: int = DEFAULT_TILE_OVERLAP,
|
||||||
) -> dict[str, torch.Tensor | int]:
|
) -> dict[str, torch.Tensor | int]:
|
||||||
"""Encode video into non-patchified latent representation.
|
"""Encode video into non-patchified latent representation.
|
||||||
Args:
|
Args:
|
||||||
@@ -618,6 +618,9 @@ def encode_video(
|
|||||||
video: Input tensor of shape [B, C, F, H, W] (batch, channels, frames, height, width)
|
video: Input tensor of shape [B, C, F, H, W] (batch, channels, frames, height, width)
|
||||||
This is the format expected by the VAE encoder.
|
This is the format expected by the VAE encoder.
|
||||||
dtype: Target dtype for output latents
|
dtype: Target dtype for output latents
|
||||||
|
use_tiling: Whether to use spatial tiling for memory efficiency
|
||||||
|
tile_size: Tile size in pixels (must be divisible by 32)
|
||||||
|
tile_overlap: Overlap between tiles in pixels (must be divisible by 32)
|
||||||
Returns:
|
Returns:
|
||||||
Dict containing non-patchified latents and shape information:
|
Dict containing non-patchified latents and shape information:
|
||||||
{
|
{
|
||||||
@@ -636,8 +639,17 @@ def encode_video(
|
|||||||
|
|
||||||
video = video.to(device=device, dtype=vae_dtype)
|
video = video.to(device=device, dtype=vae_dtype)
|
||||||
|
|
||||||
# Encode video - VAE expects [B, C, F, H, W], returns [B, C, F', H', W']
|
# Choose encoding method based on tiling flag
|
||||||
latents = vae(video)
|
if use_tiling:
|
||||||
|
latents = tiled_encode_video(
|
||||||
|
vae=vae,
|
||||||
|
video=video,
|
||||||
|
tile_size=tile_size,
|
||||||
|
tile_overlap=tile_overlap,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Encode video - VAE expects [B, C, F, H, W], returns [B, C, F', H', W']
|
||||||
|
latents = vae(video)
|
||||||
|
|
||||||
if dtype is not None:
|
if dtype is not None:
|
||||||
latents = latents.to(dtype=dtype)
|
latents = latents.to(dtype=dtype)
|
||||||
@@ -652,6 +664,161 @@ def encode_video(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def tiled_encode_video( # noqa: PLR0912, PLR0915
|
||||||
|
vae: torch.nn.Module,
|
||||||
|
video: torch.Tensor,
|
||||||
|
tile_size: int = DEFAULT_TILE_SIZE,
|
||||||
|
tile_overlap: int = DEFAULT_TILE_OVERLAP,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""Encode video using spatial tiling for memory efficiency.
|
||||||
|
Splits the video into overlapping spatial tiles, encodes each tile separately,
|
||||||
|
and blends the results using linear feathering in the overlap regions.
|
||||||
|
Args:
|
||||||
|
vae: Video VAE encoder model
|
||||||
|
video: Input tensor of shape [B, C, F, H, W]
|
||||||
|
tile_size: Tile size in pixels (must be divisible by 32)
|
||||||
|
tile_overlap: Overlap between tiles in pixels (must be divisible by 32)
|
||||||
|
Returns:
|
||||||
|
Encoded latent tensor [B, C_latent, F_latent, H_latent, W_latent]
|
||||||
|
"""
|
||||||
|
batch, _channels, frames, height, width = video.shape
|
||||||
|
device = video.device
|
||||||
|
dtype = video.dtype
|
||||||
|
|
||||||
|
# Validate tile parameters
|
||||||
|
if tile_size % VAE_SPATIAL_FACTOR != 0:
|
||||||
|
raise ValueError(f"tile_size must be divisible by {VAE_SPATIAL_FACTOR}, got {tile_size}")
|
||||||
|
if tile_overlap % VAE_SPATIAL_FACTOR != 0:
|
||||||
|
raise ValueError(f"tile_overlap must be divisible by {VAE_SPATIAL_FACTOR}, got {tile_overlap}")
|
||||||
|
if tile_overlap >= tile_size:
|
||||||
|
raise ValueError(f"tile_overlap ({tile_overlap}) must be less than tile_size ({tile_size})")
|
||||||
|
|
||||||
|
# If video fits in a single tile, use regular encoding
|
||||||
|
if height <= tile_size and width <= tile_size:
|
||||||
|
return vae(video)
|
||||||
|
|
||||||
|
# Calculate output dimensions
|
||||||
|
# VAE compresses: H -> H/32, W -> W/32, F -> 1 + (F-1)/8
|
||||||
|
output_height = height // VAE_SPATIAL_FACTOR
|
||||||
|
output_width = width // VAE_SPATIAL_FACTOR
|
||||||
|
output_frames = 1 + (frames - 1) // VAE_TEMPORAL_FACTOR
|
||||||
|
|
||||||
|
# Latent channels (128 for LTX-2)
|
||||||
|
# Get from a small test encode or assume 128
|
||||||
|
latent_channels = 128
|
||||||
|
|
||||||
|
# Initialize output and weight tensors
|
||||||
|
output = torch.zeros(
|
||||||
|
(batch, latent_channels, output_frames, output_height, output_width),
|
||||||
|
device=device,
|
||||||
|
dtype=dtype,
|
||||||
|
)
|
||||||
|
weights = torch.zeros(
|
||||||
|
(batch, 1, output_frames, output_height, output_width),
|
||||||
|
device=device,
|
||||||
|
dtype=dtype,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Calculate tile positions with overlap
|
||||||
|
# Step size is tile_size - tile_overlap
|
||||||
|
step_h = tile_size - tile_overlap
|
||||||
|
step_w = tile_size - tile_overlap
|
||||||
|
|
||||||
|
h_positions = list(range(0, max(1, height - tile_overlap), step_h))
|
||||||
|
w_positions = list(range(0, max(1, width - tile_overlap), step_w))
|
||||||
|
|
||||||
|
# Ensure last tile covers the edge
|
||||||
|
if h_positions[-1] + tile_size < height:
|
||||||
|
h_positions.append(height - tile_size)
|
||||||
|
if w_positions[-1] + tile_size < width:
|
||||||
|
w_positions.append(width - tile_size)
|
||||||
|
|
||||||
|
# Remove duplicates and sort
|
||||||
|
h_positions = sorted(set(h_positions))
|
||||||
|
w_positions = sorted(set(w_positions))
|
||||||
|
|
||||||
|
# Overlap in latent space
|
||||||
|
overlap_out_h = tile_overlap // VAE_SPATIAL_FACTOR
|
||||||
|
overlap_out_w = tile_overlap // VAE_SPATIAL_FACTOR
|
||||||
|
|
||||||
|
# Process each tile
|
||||||
|
for h_pos in h_positions:
|
||||||
|
for w_pos in w_positions:
|
||||||
|
# Calculate tile boundaries in input space
|
||||||
|
h_start = max(0, h_pos)
|
||||||
|
w_start = max(0, w_pos)
|
||||||
|
h_end = min(h_start + tile_size, height)
|
||||||
|
w_end = min(w_start + tile_size, width)
|
||||||
|
|
||||||
|
# Ensure tile dimensions are divisible by VAE_SPATIAL_FACTOR
|
||||||
|
tile_h = ((h_end - h_start) // VAE_SPATIAL_FACTOR) * VAE_SPATIAL_FACTOR
|
||||||
|
tile_w = ((w_end - w_start) // VAE_SPATIAL_FACTOR) * VAE_SPATIAL_FACTOR
|
||||||
|
|
||||||
|
if tile_h < VAE_SPATIAL_FACTOR or tile_w < VAE_SPATIAL_FACTOR:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Adjust end positions
|
||||||
|
h_end = h_start + tile_h
|
||||||
|
w_end = w_start + tile_w
|
||||||
|
|
||||||
|
# Extract tile
|
||||||
|
tile = video[:, :, :, h_start:h_end, w_start:w_end]
|
||||||
|
|
||||||
|
# Encode tile
|
||||||
|
encoded_tile = vae(tile)
|
||||||
|
|
||||||
|
# Get actual encoded dimensions
|
||||||
|
_, _, tile_out_frames, tile_out_height, tile_out_width = encoded_tile.shape
|
||||||
|
|
||||||
|
# Calculate output positions
|
||||||
|
out_h_start = h_start // VAE_SPATIAL_FACTOR
|
||||||
|
out_w_start = w_start // VAE_SPATIAL_FACTOR
|
||||||
|
out_h_end = min(out_h_start + tile_out_height, output_height)
|
||||||
|
out_w_end = min(out_w_start + tile_out_width, output_width)
|
||||||
|
|
||||||
|
# Trim encoded tile if necessary
|
||||||
|
actual_tile_h = out_h_end - out_h_start
|
||||||
|
actual_tile_w = out_w_end - out_w_start
|
||||||
|
encoded_tile = encoded_tile[:, :, :, :actual_tile_h, :actual_tile_w]
|
||||||
|
|
||||||
|
# Create blending mask with linear feathering at edges
|
||||||
|
mask = torch.ones(
|
||||||
|
(1, 1, tile_out_frames, actual_tile_h, actual_tile_w),
|
||||||
|
device=device,
|
||||||
|
dtype=dtype,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply feathering at edges (linear blend in overlap regions)
|
||||||
|
# Left edge
|
||||||
|
if h_pos > 0 and overlap_out_h > 0 and overlap_out_h < actual_tile_h:
|
||||||
|
fade_in = torch.linspace(0.0, 1.0, overlap_out_h + 2, device=device, dtype=dtype)[1:-1]
|
||||||
|
mask[:, :, :, :overlap_out_h, :] *= fade_in.view(1, 1, 1, -1, 1)
|
||||||
|
|
||||||
|
# Right edge (bottom in height dimension)
|
||||||
|
if h_end < height and overlap_out_h > 0 and overlap_out_h < actual_tile_h:
|
||||||
|
fade_out = torch.linspace(1.0, 0.0, overlap_out_h + 2, device=device, dtype=dtype)[1:-1]
|
||||||
|
mask[:, :, :, -overlap_out_h:, :] *= fade_out.view(1, 1, 1, -1, 1)
|
||||||
|
|
||||||
|
# Top edge (left in width dimension)
|
||||||
|
if w_pos > 0 and overlap_out_w > 0 and overlap_out_w < actual_tile_w:
|
||||||
|
fade_in = torch.linspace(0.0, 1.0, overlap_out_w + 2, device=device, dtype=dtype)[1:-1]
|
||||||
|
mask[:, :, :, :, :overlap_out_w] *= fade_in.view(1, 1, 1, 1, -1)
|
||||||
|
|
||||||
|
# Bottom edge (right in width dimension)
|
||||||
|
if w_end < width and overlap_out_w > 0 and overlap_out_w < actual_tile_w:
|
||||||
|
fade_out = torch.linspace(1.0, 0.0, overlap_out_w + 2, device=device, dtype=dtype)[1:-1]
|
||||||
|
mask[:, :, :, :, -overlap_out_w:] *= fade_out.view(1, 1, 1, 1, -1)
|
||||||
|
|
||||||
|
# Accumulate weighted results
|
||||||
|
output[:, :, :, out_h_start:out_h_end, out_w_start:out_w_end] += encoded_tile * mask
|
||||||
|
weights[:, :, :, out_h_start:out_h_end, out_w_start:out_w_end] += mask
|
||||||
|
|
||||||
|
# Normalize by weights (avoid division by zero)
|
||||||
|
output = output / (weights + 1e-8)
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
def encode_audio(
|
def encode_audio(
|
||||||
audio_vae_encoder: torch.nn.Module,
|
audio_vae_encoder: torch.nn.Module,
|
||||||
audio_processor: torch.nn.Module,
|
audio_processor: torch.nn.Module,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ This module provides captioning capabilities for videos with audio using:
|
|||||||
- Gemini Flash: Cloud-based API for audio-visual captioning
|
- Gemini Flash: Cloud-based API for audio-visual captioning
|
||||||
Requirements:
|
Requirements:
|
||||||
- Qwen2.5-Omni: transformers>=4.50, torch
|
- Qwen2.5-Omni: transformers>=4.50, torch
|
||||||
- Gemini Flash: google-generativeai (pip install google-generativeai)
|
- Gemini Flash: google-generativeai (uv pip install google-generativeai)
|
||||||
Set GEMINI_API_KEY or GOOGLE_API_KEY environment variable
|
Set GEMINI_API_KEY or GOOGLE_API_KEY environment variable
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
# ruff: noqa: PLC0415
|
||||||
|
|
||||||
|
"""
|
||||||
|
8-bit Gemma text encoder loading utilities.
|
||||||
|
This module provides functionality for loading the Gemma text encoder in 8-bit precision
|
||||||
|
using bitsandbytes, which significantly reduces GPU memory usage.
|
||||||
|
Example usage:
|
||||||
|
from ltx_trainer.gemma_8bit import load_8bit_gemma
|
||||||
|
text_encoder = load_8bit_gemma(
|
||||||
|
checkpoint_path="/path/to/ltx2.safetensors",
|
||||||
|
gemma_model_path="/path/to/gemma",
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from collections.abc import Generator
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from ltx_core.loader.sft_loader import SafetensorsModelStateDictLoader
|
||||||
|
from ltx_core.text_encoders.gemma.embeddings_connector import Embeddings1DConnectorConfigurator
|
||||||
|
from ltx_core.text_encoders.gemma.encoders.av_encoder import (
|
||||||
|
AV_GEMMA_TEXT_ENCODER_KEY_OPS,
|
||||||
|
AVGemmaTextEncoderModel,
|
||||||
|
)
|
||||||
|
from ltx_core.text_encoders.gemma.feature_extractor import GemmaFeaturesExtractorProjLinear
|
||||||
|
from ltx_core.text_encoders.gemma.tokenizer import LTXVGemmaTokenizer
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ltx_core.text_encoders.gemma import AVGemmaTextEncoderModel
|
||||||
|
|
||||||
|
|
||||||
|
def load_8bit_gemma(
|
||||||
|
checkpoint_path: str | Path,
|
||||||
|
gemma_model_path: str | Path,
|
||||||
|
dtype: torch.dtype = torch.bfloat16,
|
||||||
|
) -> "AVGemmaTextEncoderModel":
|
||||||
|
"""Load the Gemma text encoder in 8-bit precision using bitsandbytes.
|
||||||
|
This function bypasses ltx-core's standard loading path to enable 8-bit quantization
|
||||||
|
via the bitsandbytes library. The Gemma model is loaded with load_in_8bit=True and
|
||||||
|
torch_dtype=bfloat16, while the feature extractor and connector weights are loaded
|
||||||
|
from the LTX-2 checkpoint.
|
||||||
|
Args:
|
||||||
|
checkpoint_path: Path to the LTX-2 safetensors checkpoint file
|
||||||
|
gemma_model_path: Path to Gemma model directory
|
||||||
|
dtype: Data type for non-quantized model weights (feature extractor, connectors)
|
||||||
|
Returns:
|
||||||
|
Loaded AVGemmaTextEncoderModel with 8-bit quantized Gemma backbone
|
||||||
|
Raises:
|
||||||
|
ImportError: If bitsandbytes is not installed
|
||||||
|
FileNotFoundError: If required model files are not found
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from transformers import BitsAndBytesConfig, Gemma3ForConditionalGeneration
|
||||||
|
except ImportError as e:
|
||||||
|
raise ImportError(
|
||||||
|
"8-bit text encoder loading requires bitsandbytes. Install it with: uv pip install bitsandbytes"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
# Find paths within gemma_model_path
|
||||||
|
gemma_path = _find_gemma_subpath(gemma_model_path, "model*.safetensors")
|
||||||
|
tokenizer_path = _find_gemma_subpath(gemma_model_path, "tokenizer.model")
|
||||||
|
|
||||||
|
quantization_config = BitsAndBytesConfig(load_in_8bit=True)
|
||||||
|
with _suppress_accelerate_memory_warnings():
|
||||||
|
gemma_model = Gemma3ForConditionalGeneration.from_pretrained(
|
||||||
|
gemma_path,
|
||||||
|
quantization_config=quantization_config,
|
||||||
|
torch_dtype=torch.bfloat16,
|
||||||
|
device_map="auto",
|
||||||
|
local_files_only=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Load tokenizer
|
||||||
|
tokenizer = LTXVGemmaTokenizer(tokenizer_path, 1024)
|
||||||
|
|
||||||
|
# Load config and weights from the LTX-2 checkpoint
|
||||||
|
loader = SafetensorsModelStateDictLoader()
|
||||||
|
config = loader.metadata(str(checkpoint_path))
|
||||||
|
sd = loader.load(str(checkpoint_path), sd_ops=AV_GEMMA_TEXT_ENCODER_KEY_OPS)
|
||||||
|
|
||||||
|
# Helper to extract state dict for a given prefix
|
||||||
|
def extract_state_dict(prefix: str) -> dict[str, torch.Tensor]:
|
||||||
|
return {k.replace(prefix, ""): v for k, v in sd.sd.items() if k.startswith(prefix)}
|
||||||
|
|
||||||
|
# Create and load feature extractor
|
||||||
|
feature_extractor = GemmaFeaturesExtractorProjLinear()
|
||||||
|
feature_extractor.load_state_dict(extract_state_dict("feature_extractor_linear."))
|
||||||
|
feature_extractor = feature_extractor.to(device=gemma_model.device, dtype=dtype)
|
||||||
|
|
||||||
|
# Create and load video embeddings connector
|
||||||
|
embeddings_connector = Embeddings1DConnectorConfigurator.from_config(config)
|
||||||
|
embeddings_connector.load_state_dict(extract_state_dict("embeddings_connector."))
|
||||||
|
embeddings_connector = embeddings_connector.to(device=gemma_model.device, dtype=dtype)
|
||||||
|
|
||||||
|
# Create and load audio embeddings connector
|
||||||
|
audio_embeddings_connector = Embeddings1DConnectorConfigurator.from_config(config)
|
||||||
|
audio_embeddings_connector.load_state_dict(extract_state_dict("audio_embeddings_connector."))
|
||||||
|
audio_embeddings_connector = audio_embeddings_connector.to(device=gemma_model.device, dtype=dtype)
|
||||||
|
|
||||||
|
# Construct the text encoder
|
||||||
|
text_encoder = AVGemmaTextEncoderModel(
|
||||||
|
feature_extractor_linear=feature_extractor,
|
||||||
|
embeddings_connector=embeddings_connector,
|
||||||
|
audio_embeddings_connector=audio_embeddings_connector,
|
||||||
|
tokenizer=tokenizer,
|
||||||
|
model=gemma_model,
|
||||||
|
)
|
||||||
|
|
||||||
|
return text_encoder
|
||||||
|
|
||||||
|
|
||||||
|
def _find_gemma_subpath(root_path: str | Path, pattern: str) -> str:
|
||||||
|
"""Find a file matching a glob pattern and return its parent directory."""
|
||||||
|
matches = list(Path(root_path).rglob(pattern))
|
||||||
|
if not matches:
|
||||||
|
raise FileNotFoundError(f"No files matching pattern '{pattern}' found under {root_path}")
|
||||||
|
return str(matches[0].parent)
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def _suppress_accelerate_memory_warnings() -> Generator[None, None, None]:
|
||||||
|
"""Temporarily suppress INFO warnings from accelerate about memory allocation."""
|
||||||
|
accelerate_logger = logging.getLogger("accelerate.utils.modeling")
|
||||||
|
old_level = accelerate_logger.level
|
||||||
|
accelerate_logger.setLevel(logging.WARNING)
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
accelerate_logger.setLevel(old_level)
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
"""GPU memory management utilities for training and inference."""
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import gc
|
||||||
|
import subprocess
|
||||||
|
from typing import Callable, TypeVar
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from ltx_trainer import logger
|
||||||
|
|
||||||
|
F = TypeVar("F", bound=Callable)
|
||||||
|
|
||||||
|
|
||||||
|
def free_gpu_memory(log: bool = False) -> None:
|
||||||
|
"""Free GPU memory by running garbage collection and emptying CUDA cache.
|
||||||
|
Args:
|
||||||
|
log: If True, log memory stats after clearing
|
||||||
|
"""
|
||||||
|
gc.collect()
|
||||||
|
if torch.cuda.is_available():
|
||||||
|
torch.cuda.empty_cache()
|
||||||
|
if log:
|
||||||
|
allocated = torch.cuda.memory_allocated() / 1024**3
|
||||||
|
reserved = torch.cuda.memory_reserved() / 1024**3
|
||||||
|
logger.debug(f"GPU memory freed. Allocated: {allocated:.2f}GB, Reserved: {reserved:.2f}GB")
|
||||||
|
|
||||||
|
|
||||||
|
class free_gpu_memory_context: # noqa: N801
|
||||||
|
"""Context manager and decorator to free GPU memory before and/or after execution.
|
||||||
|
Can be used as a decorator:
|
||||||
|
@free_gpu_memory_context(after=True)
|
||||||
|
def my_function():
|
||||||
|
...
|
||||||
|
Or as a context manager:
|
||||||
|
with free_gpu_memory_context():
|
||||||
|
heavy_operation()
|
||||||
|
Args:
|
||||||
|
before: Free memory before execution (default: False)
|
||||||
|
after: Free memory after execution (default: True)
|
||||||
|
log: Log memory stats when freeing (default: False)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *, before: bool = False, after: bool = True, log: bool = False) -> None:
|
||||||
|
self.before = before
|
||||||
|
self.after = after
|
||||||
|
self.log = log
|
||||||
|
|
||||||
|
def __enter__(self) -> "free_gpu_memory_context":
|
||||||
|
if self.before:
|
||||||
|
free_gpu_memory(log=self.log)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type: type | None, exc_val: Exception | None, exc_tb: object) -> None:
|
||||||
|
if self.after:
|
||||||
|
free_gpu_memory(log=self.log)
|
||||||
|
|
||||||
|
def __call__(self, func: F) -> F:
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(*args, **kwargs) -> object:
|
||||||
|
with self:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def get_gpu_memory_gb(device: torch.device) -> float:
|
||||||
|
"""Get current GPU memory usage in GB using nvidia-smi.
|
||||||
|
Args:
|
||||||
|
device: torch.device to get memory usage for
|
||||||
|
Returns:
|
||||||
|
Current GPU memory usage in GB
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
device_id = device.index if device.index is not None else 0
|
||||||
|
result = subprocess.check_output(
|
||||||
|
[
|
||||||
|
"nvidia-smi",
|
||||||
|
"--query-gpu=memory.used",
|
||||||
|
"--format=csv,nounits,noheader",
|
||||||
|
"-i",
|
||||||
|
str(device_id),
|
||||||
|
],
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return float(result.strip()) / 1024 # Convert MB to GB
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError, ValueError) as e:
|
||||||
|
logger.error(f"Failed to get GPU memory from nvidia-smi: {e}")
|
||||||
|
# Fallback to torch
|
||||||
|
return torch.cuda.memory_allocated(device) / 1024**3
|
||||||
@@ -191,6 +191,7 @@ def load_text_encoder(
|
|||||||
gemma_model_path: str | Path,
|
gemma_model_path: str | Path,
|
||||||
device: Device = "cpu",
|
device: Device = "cpu",
|
||||||
dtype: torch.dtype = torch.bfloat16,
|
dtype: torch.dtype = torch.bfloat16,
|
||||||
|
load_in_8bit: bool = False,
|
||||||
) -> "AVGemmaTextEncoderModel":
|
) -> "AVGemmaTextEncoderModel":
|
||||||
"""Load the Gemma text encoder.
|
"""Load the Gemma text encoder.
|
||||||
Args:
|
Args:
|
||||||
@@ -198,9 +199,22 @@ def load_text_encoder(
|
|||||||
gemma_model_path: Path to Gemma model directory
|
gemma_model_path: Path to Gemma model directory
|
||||||
device: Device to load model on
|
device: Device to load model on
|
||||||
dtype: Data type for model weights
|
dtype: Data type for model weights
|
||||||
|
load_in_8bit: Whether to load the Gemma model in 8-bit precision using bitsandbytes.
|
||||||
|
When True, the model is loaded with device_map="auto" and the device argument
|
||||||
|
is ignored for the Gemma backbone (feature extractor still uses dtype).
|
||||||
Returns:
|
Returns:
|
||||||
Loaded AVGemmaTextEncoderModel
|
Loaded AVGemmaTextEncoderModel
|
||||||
"""
|
"""
|
||||||
|
if not Path(gemma_model_path).is_dir():
|
||||||
|
raise ValueError(f"Gemma model path is not a directory: {gemma_model_path}")
|
||||||
|
|
||||||
|
# Use 8-bit loading path if requested
|
||||||
|
if load_in_8bit:
|
||||||
|
from ltx_trainer.gemma_8bit import load_8bit_gemma
|
||||||
|
|
||||||
|
return load_8bit_gemma(checkpoint_path, gemma_model_path, dtype)
|
||||||
|
|
||||||
|
# Standard loading path
|
||||||
from ltx_core.loader.single_gpu_model_builder import SingleGPUModelBuilder
|
from ltx_core.loader.single_gpu_model_builder import SingleGPUModelBuilder
|
||||||
from ltx_core.text_encoders.gemma.encoders.av_encoder import (
|
from ltx_core.text_encoders.gemma.encoders.av_encoder import (
|
||||||
AV_GEMMA_TEXT_ENCODER_KEY_OPS,
|
AV_GEMMA_TEXT_ENCODER_KEY_OPS,
|
||||||
@@ -208,9 +222,6 @@ def load_text_encoder(
|
|||||||
)
|
)
|
||||||
from ltx_core.text_encoders.gemma.encoders.base_encoder import module_ops_from_gemma_root
|
from ltx_core.text_encoders.gemma.encoders.base_encoder import module_ops_from_gemma_root
|
||||||
|
|
||||||
if not Path(gemma_model_path).is_dir():
|
|
||||||
raise ValueError(f"Gemma model path is not a directory: {gemma_model_path}")
|
|
||||||
|
|
||||||
torch_device = _to_torch_device(device)
|
torch_device = _to_torch_device(device)
|
||||||
text_encoder = SingleGPUModelBuilder(
|
text_encoder = SingleGPUModelBuilder(
|
||||||
model_path=str(checkpoint_path),
|
model_path=str(checkpoint_path),
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
# Adapted from: https://github.com/bghira/SimpleTuner/blob/main/helpers/training/quantisation/__init__.py
|
# Adapted from: https://github.com/bghira/SimpleTuner
|
||||||
|
# With improvements from: https://github.com/ostris/ai-toolkit
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
from optimum.quanto import qtype
|
from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn, TextColumn
|
||||||
|
|
||||||
from ltx_trainer import logger
|
from ltx_trainer import logger
|
||||||
|
|
||||||
@@ -14,55 +15,161 @@ QuantizationOptions = Literal[
|
|||||||
"fp8uz-quanto",
|
"fp8uz-quanto",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Modules to exclude from quantization.
|
||||||
|
# These are glob patterns passed to quanto's `exclude` parameter.
|
||||||
|
# When quantizing the full model at once, these patterns match against full module paths.
|
||||||
|
# When quantizing block-by-block, we also use SKIP_ROOT_MODULES for top-level modules.
|
||||||
|
EXCLUDE_PATTERNS = [
|
||||||
|
# Input/output projection layers
|
||||||
|
"patchify_proj",
|
||||||
|
"audio_patchify_proj",
|
||||||
|
"proj_out",
|
||||||
|
"audio_proj_out",
|
||||||
|
# Timestep embedding layers - int4 tinygemm requires strict bfloat16 input
|
||||||
|
# and these receive float32 sinusoidal embeddings that are cast to bfloat16
|
||||||
|
"*adaln*",
|
||||||
|
"time_proj",
|
||||||
|
"timestep_embedder*",
|
||||||
|
# Caption/text projection layers
|
||||||
|
"caption_projection*",
|
||||||
|
"audio_caption_projection*",
|
||||||
|
# Normalization layers (usually excluded from quantization)
|
||||||
|
"*norm*",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Top-level modules to skip entirely during block-by-block quantization.
|
||||||
|
# These are exact matches against model.named_children() names.
|
||||||
|
# (Needed because quanto's exclude patterns don't work when calling quantize() directly on a module)
|
||||||
|
SKIP_ROOT_MODULES = {
|
||||||
|
"patchify_proj",
|
||||||
|
"audio_patchify_proj",
|
||||||
|
"proj_out",
|
||||||
|
"audio_proj_out",
|
||||||
|
"audio_caption_projection",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def quantize_model(
|
def quantize_model(
|
||||||
model: torch.nn.Module,
|
model: torch.nn.Module,
|
||||||
precision: QuantizationOptions,
|
precision: QuantizationOptions,
|
||||||
quantize_activations: bool = False,
|
quantize_activations: bool = False,
|
||||||
|
device: torch.device | str | None = None,
|
||||||
) -> torch.nn.Module:
|
) -> torch.nn.Module:
|
||||||
"""
|
"""
|
||||||
Quantize a model using the specified precision settings.
|
Quantize a model using optimum-quanto.
|
||||||
|
For large models with transformer_blocks, this function quantizes block-by-block
|
||||||
|
on GPU then moves back to CPU, which is much faster than quantizing on CPU and
|
||||||
|
uses less peak VRAM than loading the entire model to GPU at once.
|
||||||
Args:
|
Args:
|
||||||
model: The model to quantize.
|
model: The model to quantize.
|
||||||
precision: The precision level to quantize to (e.g. "int8-quanto", "fp8-quanto").
|
precision: The quantization precision (e.g. "int8-quanto", "fp8-quanto").
|
||||||
quantize_activations: Whether to quantize activations in addition to weights.
|
quantize_activations: Whether to quantize activations in addition to weights.
|
||||||
|
device: Device to use for quantization. If None, uses CUDA if available, else CPU.
|
||||||
Returns:
|
Returns:
|
||||||
The quantized model, or the original model if no quantization is performed.
|
The quantized model.
|
||||||
"""
|
"""
|
||||||
from optimum.quanto import freeze, quantize # noqa: PLC0415
|
from optimum.quanto import freeze, quantize # noqa: PLC0415
|
||||||
|
|
||||||
weight_quant = _quanto_type_map(precision)
|
if device is None:
|
||||||
extra_quanto_args = {
|
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||||
"exclude": [
|
elif isinstance(device, str):
|
||||||
# Input/output projection layers
|
device = torch.device(device)
|
||||||
"patchify_proj",
|
|
||||||
"audio_patchify_proj",
|
weight_quant = _get_quanto_dtype(precision)
|
||||||
"proj_out",
|
|
||||||
"audio_proj_out",
|
|
||||||
# Timestep embedding layers - int4 tinygemm requires strict bfloat16 input
|
|
||||||
# and these receive float32 sinusoidal embeddings that are cast to bfloat16
|
|
||||||
"*adaln*",
|
|
||||||
"time_proj",
|
|
||||||
"timestep_embedder*",
|
|
||||||
# Caption/text projection layers
|
|
||||||
"caption_projection*",
|
|
||||||
"audio_caption_projection*",
|
|
||||||
# Normalization layers (usually excluded from quantization)
|
|
||||||
"*norm*",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
if quantize_activations:
|
if quantize_activations:
|
||||||
logger.debug("Quantizing model weights and activations")
|
logger.debug("Quantizing model weights and activations")
|
||||||
extra_quanto_args["activations"] = weight_quant
|
activations_quant = weight_quant
|
||||||
else:
|
else:
|
||||||
logger.debug("Quantizing model weights only")
|
activations_quant = None
|
||||||
|
|
||||||
|
# Remember original device to restore after quantization
|
||||||
|
original_device = next(model.parameters()).device
|
||||||
|
|
||||||
|
# Check if model has transformer_blocks for block-by-block quantization
|
||||||
|
if hasattr(model, "transformer_blocks"):
|
||||||
|
logger.debug("Quantizing model using block-by-block approach for memory efficiency")
|
||||||
|
_quantize_blockwise(
|
||||||
|
model,
|
||||||
|
weight_quant=weight_quant,
|
||||||
|
activations_quant=activations_quant,
|
||||||
|
device=device,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Fallback: quantize entire model at once
|
||||||
|
model.to(device)
|
||||||
|
quantize(model, weights=weight_quant, activations=activations_quant, exclude=EXCLUDE_PATTERNS)
|
||||||
|
freeze(model)
|
||||||
|
|
||||||
|
# Restore model to original device
|
||||||
|
model.to(original_device)
|
||||||
|
|
||||||
quantize(model, weights=weight_quant, **extra_quanto_args)
|
|
||||||
freeze(model)
|
|
||||||
return model
|
return model
|
||||||
|
|
||||||
|
|
||||||
def _quanto_type_map(precision: QuantizationOptions) -> torch.dtype | qtype | None:
|
def _quantize_blockwise(
|
||||||
|
model: torch.nn.Module,
|
||||||
|
weight_quant: torch.dtype,
|
||||||
|
activations_quant: torch.dtype | None,
|
||||||
|
device: torch.device,
|
||||||
|
) -> None:
|
||||||
|
"""Quantize a model block-by-block using optimum-quanto.
|
||||||
|
This approach:
|
||||||
|
1. Moves each transformer block to GPU
|
||||||
|
2. Quantizes on GPU (fast!)
|
||||||
|
3. Freezes the quantized weights
|
||||||
|
4. Moves back to CPU
|
||||||
|
This is much faster than quantizing on CPU and uses less peak VRAM
|
||||||
|
than loading the entire model to GPU.
|
||||||
|
"""
|
||||||
|
from optimum.quanto import freeze, quantize # noqa: PLC0415
|
||||||
|
|
||||||
|
original_dtype = next(model.parameters()).dtype
|
||||||
|
transformer_blocks = list(model.transformer_blocks)
|
||||||
|
|
||||||
|
with Progress(
|
||||||
|
SpinnerColumn(),
|
||||||
|
TextColumn("[progress.description]{task.description}"),
|
||||||
|
BarColumn(),
|
||||||
|
TaskProgressColumn(),
|
||||||
|
transient=True,
|
||||||
|
) as progress:
|
||||||
|
task = progress.add_task("Quantizing transformer blocks", total=len(transformer_blocks))
|
||||||
|
|
||||||
|
for block in transformer_blocks:
|
||||||
|
# Move block to GPU
|
||||||
|
block.to(device, dtype=original_dtype, non_blocking=True)
|
||||||
|
|
||||||
|
# Quantize on GPU
|
||||||
|
quantize(block, weights=weight_quant, activations=activations_quant, exclude=EXCLUDE_PATTERNS)
|
||||||
|
freeze(block)
|
||||||
|
|
||||||
|
# Move back to CPU to free up VRAM for next block
|
||||||
|
block.to("cpu", non_blocking=True)
|
||||||
|
|
||||||
|
progress.advance(task)
|
||||||
|
|
||||||
|
# Quantize remaining non-transformer-block modules (e.g., embeddings, timestep projections)
|
||||||
|
# Skip modules that should not be quantized (patchify_proj, proj_out, etc.)
|
||||||
|
logger.debug("Quantizing remaining model components")
|
||||||
|
|
||||||
|
for name, module in model.named_children():
|
||||||
|
if name == "transformer_blocks":
|
||||||
|
continue # Already quantized
|
||||||
|
|
||||||
|
if name in SKIP_ROOT_MODULES:
|
||||||
|
logger.debug(f"Skipping quantization for module: {name}")
|
||||||
|
continue # Don't quantize these modules
|
||||||
|
|
||||||
|
# Move to device, quantize, freeze, move back
|
||||||
|
module.to(device, dtype=original_dtype, non_blocking=True)
|
||||||
|
quantize(module, weights=weight_quant, activations=activations_quant, exclude=EXCLUDE_PATTERNS)
|
||||||
|
freeze(module)
|
||||||
|
module.to("cpu", non_blocking=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_quanto_dtype(precision: QuantizationOptions) -> torch.dtype:
|
||||||
|
"""Map precision string to quanto dtype."""
|
||||||
from optimum.quanto import ( # noqa: PLC0415
|
from optimum.quanto import ( # noqa: PLC0415
|
||||||
qfloat8,
|
qfloat8,
|
||||||
qfloat8_e4m3fnuz,
|
qfloat8_e4m3fnuz,
|
||||||
@@ -79,14 +186,10 @@ def _quanto_type_map(precision: QuantizationOptions) -> torch.dtype | qtype | No
|
|||||||
return qint8
|
return qint8
|
||||||
elif precision in ("fp8-quanto", "fp8uz-quanto"):
|
elif precision in ("fp8-quanto", "fp8uz-quanto"):
|
||||||
if torch.backends.mps.is_available():
|
if torch.backends.mps.is_available():
|
||||||
logger.warning(
|
raise ValueError("FP8 quantization is not supported on MPS devices. Use int2, int4, or int8 instead.")
|
||||||
"MPS doesn't support dtype float8. "
|
|
||||||
"you must select another precision level such as int2, int8, or int8.",
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
if precision == "fp8-quanto":
|
if precision == "fp8-quanto":
|
||||||
return qfloat8
|
return qfloat8
|
||||||
elif precision == "fp8uz-quanto":
|
elif precision == "fp8uz-quanto":
|
||||||
return qfloat8_e4m3fnuz
|
return qfloat8_e4m3fnuz
|
||||||
|
|
||||||
raise ValueError(f"Invalid quantisation level: {precision}")
|
raise ValueError(f"Invalid quantization precision: {precision}")
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ from ltx_trainer import logger
|
|||||||
from ltx_trainer.config import LtxTrainerConfig
|
from ltx_trainer.config import LtxTrainerConfig
|
||||||
from ltx_trainer.config_display import print_config
|
from ltx_trainer.config_display import print_config
|
||||||
from ltx_trainer.datasets import PrecomputedDataset
|
from ltx_trainer.datasets import PrecomputedDataset
|
||||||
|
from ltx_trainer.gpu_utils import free_gpu_memory, free_gpu_memory_context, get_gpu_memory_gb
|
||||||
from ltx_trainer.hf_hub_utils import push_to_hub
|
from ltx_trainer.hf_hub_utils import push_to_hub
|
||||||
from ltx_trainer.model_loader import load_model as load_ltx_model
|
from ltx_trainer.model_loader import load_model as load_ltx_model
|
||||||
from ltx_trainer.model_loader import load_text_encoder
|
from ltx_trainer.model_loader import load_text_encoder
|
||||||
@@ -38,7 +39,7 @@ from ltx_trainer.progress import TrainingProgress
|
|||||||
from ltx_trainer.quantization import quantize_model
|
from ltx_trainer.quantization import quantize_model
|
||||||
from ltx_trainer.timestep_samplers import SAMPLERS
|
from ltx_trainer.timestep_samplers import SAMPLERS
|
||||||
from ltx_trainer.training_strategies import get_training_strategy
|
from ltx_trainer.training_strategies import get_training_strategy
|
||||||
from ltx_trainer.utils import get_gpu_memory_gb, open_image_as_srgb, save_image
|
from ltx_trainer.utils import open_image_as_srgb, save_image
|
||||||
from ltx_trainer.validation_sampler import CachedPromptEmbeddings, GenerationConfig, ValidationSampler
|
from ltx_trainer.validation_sampler import CachedPromptEmbeddings, GenerationConfig, ValidationSampler
|
||||||
from ltx_trainer.video_utils import read_video, save_video
|
from ltx_trainer.video_utils import read_video, save_video
|
||||||
|
|
||||||
@@ -330,6 +331,7 @@ class LtxvTrainer:
|
|||||||
|
|
||||||
return loss
|
return loss
|
||||||
|
|
||||||
|
@free_gpu_memory_context(after=True)
|
||||||
def _load_text_encoder_and_cache_embeddings(self) -> list[CachedPromptEmbeddings] | None:
|
def _load_text_encoder_and_cache_embeddings(self) -> list[CachedPromptEmbeddings] | None:
|
||||||
"""Load text encoder, computes and returns validation embeddings."""
|
"""Load text encoder, computes and returns validation embeddings."""
|
||||||
|
|
||||||
@@ -342,17 +344,13 @@ class LtxvTrainer:
|
|||||||
|
|
||||||
# Load text encoder on GPU
|
# Load text encoder on GPU
|
||||||
logger.debug("Loading text encoder...")
|
logger.debug("Loading text encoder...")
|
||||||
if self._config.acceleration.load_text_encoder_in_8bit:
|
|
||||||
logger.warning(
|
|
||||||
"⚠️ load_text_encoder_in_8bit is set to True but 8-bit text encoder loading "
|
|
||||||
"is not currently implemented. The text encoder will be loaded in bfloat16 precision."
|
|
||||||
)
|
|
||||||
|
|
||||||
self._text_encoder = load_text_encoder(
|
self._text_encoder = load_text_encoder(
|
||||||
checkpoint_path=self._config.model.model_path,
|
checkpoint_path=self._config.model.model_path,
|
||||||
gemma_model_path=self._config.model.text_encoder_path,
|
gemma_model_path=self._config.model.text_encoder_path,
|
||||||
device="cuda",
|
device="cuda",
|
||||||
dtype=torch.bfloat16,
|
dtype=torch.bfloat16,
|
||||||
|
load_in_8bit=self._config.acceleration.load_text_encoder_in_8bit,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Cache validation embeddings if prompts are configured
|
# Cache validation embeddings if prompts are configured
|
||||||
@@ -378,7 +376,6 @@ class LtxvTrainer:
|
|||||||
self._text_encoder.model = None
|
self._text_encoder.model = None
|
||||||
self._text_encoder.tokenizer = None
|
self._text_encoder.tokenizer = None
|
||||||
self._text_encoder.feature_extractor_linear = None
|
self._text_encoder.feature_extractor_linear = None
|
||||||
torch.cuda.empty_cache()
|
|
||||||
|
|
||||||
logger.debug("Validation prompt embeddings cached. Gemma model unloaded")
|
logger.debug("Validation prompt embeddings cached. Gemma model unloaded")
|
||||||
return cached_embeddings
|
return cached_embeddings
|
||||||
@@ -724,6 +721,7 @@ class LtxvTrainer:
|
|||||||
|
|
||||||
# Note: Use @torch.no_grad() instead of @torch.inference_mode() to avoid FSDP inplace update errors after validation
|
# Note: Use @torch.no_grad() instead of @torch.inference_mode() to avoid FSDP inplace update errors after validation
|
||||||
@torch.no_grad()
|
@torch.no_grad()
|
||||||
|
@free_gpu_memory_context(after=True)
|
||||||
def _sample_videos(self, progress: TrainingProgress) -> list[Path] | None:
|
def _sample_videos(self, progress: TrainingProgress) -> list[Path] | None:
|
||||||
"""Run validation by generating videos from validation prompts."""
|
"""Run validation by generating videos from validation prompts."""
|
||||||
use_images = self._config.validation.images is not None
|
use_images = self._config.validation.images is not None
|
||||||
@@ -731,10 +729,9 @@ class LtxvTrainer:
|
|||||||
generate_audio = self._config.validation.generate_audio
|
generate_audio = self._config.validation.generate_audio
|
||||||
inference_steps = self._config.validation.inference_steps
|
inference_steps = self._config.validation.inference_steps
|
||||||
|
|
||||||
# Free up GPU memory before validation sampling.
|
# Zero gradients and free GPU memory to reclaim memory before validation sampling
|
||||||
# Zero gradients and empty the cache to reclaim memory.
|
|
||||||
self._optimizer.zero_grad(set_to_none=True)
|
self._optimizer.zero_grad(set_to_none=True)
|
||||||
torch.cuda.empty_cache()
|
free_gpu_memory()
|
||||||
|
|
||||||
# Start sampling progress tracking
|
# Start sampling progress tracking
|
||||||
sampling_ctx = progress.start_sampling(
|
sampling_ctx = progress.start_sampling(
|
||||||
@@ -831,9 +828,6 @@ class LtxvTrainer:
|
|||||||
# Clean up progress tasks
|
# Clean up progress tasks
|
||||||
sampling_ctx.cleanup()
|
sampling_ctx.cleanup()
|
||||||
|
|
||||||
# Clear GPU cache after validation
|
|
||||||
torch.cuda.empty_cache()
|
|
||||||
|
|
||||||
rel_outputs_path = output_dir.relative_to(self._config.output_dir)
|
rel_outputs_path = output_dir.relative_to(self._config.output_dir)
|
||||||
logger.info(f"🎥 Validation samples for step {self._global_step} saved in {rel_outputs_path}")
|
logger.info(f"🎥 Validation samples for step {self._global_step} saved in {rel_outputs_path}")
|
||||||
return video_paths
|
return video_paths
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import io
|
import io
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -7,35 +6,6 @@ import torch
|
|||||||
from PIL import ExifTags, Image, ImageCms, ImageOps
|
from PIL import ExifTags, Image, ImageCms, ImageOps
|
||||||
from PIL.Image import Image as PilImage
|
from PIL.Image import Image as PilImage
|
||||||
|
|
||||||
from ltx_trainer import logger
|
|
||||||
|
|
||||||
|
|
||||||
def get_gpu_memory_gb(device: torch.device) -> float:
|
|
||||||
"""
|
|
||||||
Get current GPU memory usage in GB using nvidia-smi
|
|
||||||
Args:
|
|
||||||
device: torch.device to get memory usage for
|
|
||||||
Returns:
|
|
||||||
Current GPU memory usage in GB
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
device_id = device.index if device.index is not None else 0
|
|
||||||
result = subprocess.check_output(
|
|
||||||
[
|
|
||||||
"nvidia-smi",
|
|
||||||
"--query-gpu=memory.used",
|
|
||||||
"--format=csv,nounits,noheader",
|
|
||||||
"-i",
|
|
||||||
str(device_id),
|
|
||||||
],
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
return float(result.strip()) / 1024 # Convert MB to GB
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError, ValueError) as e:
|
|
||||||
logger.error(f"Failed to get GPU memory from nvidia-smi: {e}")
|
|
||||||
# Fallback to torch
|
|
||||||
return torch.cuda.memory_allocated(device) / 1024**3
|
|
||||||
|
|
||||||
|
|
||||||
def open_image_as_srgb(image_path: str | Path | io.BytesIO) -> PilImage:
|
def open_image_as_srgb(image_path: str | Path | io.BytesIO) -> PilImage:
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user