Automated PR - 2026-04-23
This commit is contained in:
@@ -31,7 +31,7 @@ from ltx_pipelines.utils.helpers import (
|
||||
get_device,
|
||||
)
|
||||
from ltx_pipelines.utils.media_io import decode_audio_from_file, encode_video
|
||||
from ltx_pipelines.utils.types import ModalitySpec
|
||||
from ltx_pipelines.utils.types import ModalitySpec, OffloadMode
|
||||
|
||||
|
||||
class A2VidPipelineTwoStage:
|
||||
@@ -53,12 +53,15 @@ class A2VidPipelineTwoStage:
|
||||
quantization: QuantizationPolicy | None = None,
|
||||
registry: Registry | None = None,
|
||||
torch_compile: bool = False,
|
||||
offload_mode: OffloadMode = OffloadMode.NONE,
|
||||
):
|
||||
self.device = device or get_device()
|
||||
self.dtype = torch.bfloat16
|
||||
self._scheduler = LTX2Scheduler()
|
||||
|
||||
self.prompt_encoder = PromptEncoder(checkpoint_path, gemma_root, self.dtype, self.device, registry=registry)
|
||||
self.prompt_encoder = PromptEncoder(
|
||||
checkpoint_path, gemma_root, self.dtype, self.device, registry=registry, offload_mode=offload_mode
|
||||
)
|
||||
self.image_conditioner = ImageConditioner(checkpoint_path, self.dtype, self.device, registry=registry)
|
||||
self.audio_conditioner = AudioConditioner(checkpoint_path, self.dtype, self.device, registry=registry)
|
||||
self.stage_1 = DiffusionStage(
|
||||
@@ -69,6 +72,7 @@ class A2VidPipelineTwoStage:
|
||||
quantization=quantization,
|
||||
registry=registry,
|
||||
torch_compile=torch_compile,
|
||||
offload_mode=offload_mode,
|
||||
)
|
||||
stage_2_loras = (*tuple(loras), *tuple(distilled_lora))
|
||||
self.stage_2 = DiffusionStage(
|
||||
@@ -79,6 +83,7 @@ class A2VidPipelineTwoStage:
|
||||
quantization=quantization,
|
||||
registry=registry,
|
||||
torch_compile=torch_compile,
|
||||
offload_mode=offload_mode,
|
||||
)
|
||||
self.upsampler = VideoUpsampler(
|
||||
checkpoint_path, spatial_upsampler_path, self.dtype, self.device, registry=registry
|
||||
@@ -102,7 +107,6 @@ class A2VidPipelineTwoStage:
|
||||
audio_max_duration: float | None = None,
|
||||
tiling_config: TilingConfig | None = None,
|
||||
enhance_prompt: bool = False,
|
||||
streaming_prefetch_count: int | None = None,
|
||||
max_batch_size: int = 1,
|
||||
stage_1_sigmas: torch.Tensor | None = None,
|
||||
stage_2_sigmas: torch.Tensor = STAGE_2_DISTILLED_SIGMAS,
|
||||
@@ -117,7 +121,6 @@ class A2VidPipelineTwoStage:
|
||||
[prompt, negative_prompt],
|
||||
enhance_first_prompt=enhance_prompt,
|
||||
enhance_prompt_image=images[0][0] if len(images) > 0 else None,
|
||||
streaming_prefetch_count=streaming_prefetch_count,
|
||||
)
|
||||
v_context_p, a_context_p = ctx_p.video_encoding, ctx_p.audio_encoding
|
||||
v_context_n, _ = ctx_n.video_encoding, ctx_n.audio_encoding
|
||||
@@ -183,7 +186,6 @@ class A2VidPipelineTwoStage:
|
||||
noise_scale=0.0,
|
||||
initial_latent=encoded_audio_latent,
|
||||
),
|
||||
streaming_prefetch_count=streaming_prefetch_count,
|
||||
max_batch_size=max_batch_size,
|
||||
)
|
||||
|
||||
@@ -223,7 +225,6 @@ class A2VidPipelineTwoStage:
|
||||
noise_scale=0.0,
|
||||
initial_latent=encoded_audio_latent,
|
||||
),
|
||||
streaming_prefetch_count=streaming_prefetch_count,
|
||||
)
|
||||
|
||||
decoded_video = self.video_decoder(video_state.latent, tiling_config, generator)
|
||||
@@ -266,6 +267,7 @@ def main() -> None:
|
||||
loras=tuple(args.lora) if args.lora else (),
|
||||
quantization=args.quantization,
|
||||
torch_compile=args.compile,
|
||||
offload_mode=args.offload_mode,
|
||||
)
|
||||
tiling_config = TilingConfig.default()
|
||||
video_chunks_number = get_video_chunks_number(args.num_frames, tiling_config)
|
||||
@@ -294,7 +296,6 @@ def main() -> None:
|
||||
audio_max_duration=args.audio_max_duration
|
||||
if args.audio_max_duration is not None
|
||||
else args.num_frames / args.frame_rate,
|
||||
streaming_prefetch_count=args.streaming_prefetch_count,
|
||||
max_batch_size=args.max_batch_size,
|
||||
)
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ from ltx_pipelines.utils.helpers import (
|
||||
get_device,
|
||||
)
|
||||
from ltx_pipelines.utils.media_io import encode_video
|
||||
from ltx_pipelines.utils.types import ModalitySpec
|
||||
from ltx_pipelines.utils.types import ModalitySpec, OffloadMode
|
||||
|
||||
|
||||
class DistilledPipeline:
|
||||
@@ -54,12 +54,18 @@ class DistilledPipeline:
|
||||
quantization: QuantizationPolicy | None = None,
|
||||
registry: Registry | None = None,
|
||||
torch_compile: bool = False,
|
||||
offload_mode: OffloadMode = OffloadMode.NONE,
|
||||
):
|
||||
self.device = device or get_device()
|
||||
self.dtype = torch.bfloat16
|
||||
|
||||
self.prompt_encoder = PromptEncoder(
|
||||
distilled_checkpoint_path, gemma_root, self.dtype, self.device, registry=registry
|
||||
distilled_checkpoint_path,
|
||||
gemma_root,
|
||||
self.dtype,
|
||||
self.device,
|
||||
registry=registry,
|
||||
offload_mode=offload_mode,
|
||||
)
|
||||
self.image_conditioner = ImageConditioner(distilled_checkpoint_path, self.dtype, self.device, registry=registry)
|
||||
self.stage = DiffusionStage(
|
||||
@@ -70,6 +76,7 @@ class DistilledPipeline:
|
||||
quantization=quantization,
|
||||
registry=registry,
|
||||
torch_compile=torch_compile,
|
||||
offload_mode=offload_mode,
|
||||
)
|
||||
self.upsampler = VideoUpsampler(
|
||||
distilled_checkpoint_path, spatial_upsampler_path, self.dtype, self.device, registry=registry
|
||||
@@ -88,7 +95,6 @@ class DistilledPipeline:
|
||||
images: list[ImageConditioningInput],
|
||||
tiling_config: TilingConfig | None = None,
|
||||
enhance_prompt: bool = False,
|
||||
streaming_prefetch_count: int | None = None,
|
||||
stage_1_sigmas: torch.Tensor = DISTILLED_SIGMAS,
|
||||
stage_2_sigmas: torch.Tensor = STAGE_2_DISTILLED_SIGMAS,
|
||||
) -> tuple[Iterator[torch.Tensor], Audio]:
|
||||
@@ -102,7 +108,6 @@ class DistilledPipeline:
|
||||
[prompt],
|
||||
enhance_first_prompt=enhance_prompt,
|
||||
enhance_prompt_image=images[0][0] if len(images) > 0 else None,
|
||||
streaming_prefetch_count=streaming_prefetch_count,
|
||||
)
|
||||
video_context, audio_context = ctx_p.video_encoding, ctx_p.audio_encoding
|
||||
|
||||
@@ -130,7 +135,6 @@ class DistilledPipeline:
|
||||
fps=frame_rate,
|
||||
video=ModalitySpec(context=video_context, conditionings=stage_1_conditionings),
|
||||
audio=ModalitySpec(context=audio_context),
|
||||
streaming_prefetch_count=streaming_prefetch_count,
|
||||
)
|
||||
|
||||
# Stage 2: Upsample and refine the video at higher resolution with distilled LORA.
|
||||
@@ -167,7 +171,6 @@ class DistilledPipeline:
|
||||
noise_scale=stage_2_sigmas[0].item(),
|
||||
initial_latent=audio_state.latent,
|
||||
),
|
||||
streaming_prefetch_count=streaming_prefetch_count,
|
||||
)
|
||||
|
||||
decoded_video = self.video_decoder(video_state.latent, tiling_config, generator)
|
||||
@@ -189,6 +192,7 @@ def main() -> None:
|
||||
loras=tuple(args.lora) if args.lora else (),
|
||||
quantization=args.quantization,
|
||||
torch_compile=args.compile,
|
||||
offload_mode=args.offload_mode,
|
||||
)
|
||||
tiling_config = TilingConfig.default()
|
||||
video_chunks_number = get_video_chunks_number(args.num_frames, tiling_config)
|
||||
@@ -202,7 +206,6 @@ def main() -> None:
|
||||
images=args.images,
|
||||
tiling_config=tiling_config,
|
||||
enhance_prompt=args.enhance_prompt,
|
||||
streaming_prefetch_count=args.streaming_prefetch_count,
|
||||
)
|
||||
|
||||
encode_video(
|
||||
|
||||
@@ -0,0 +1,886 @@
|
||||
"""HDR IC-LoRA pipeline: two-stage video generation with HDR output.
|
||||
Extends the standard IC-LoRA pipeline with HDR decode via LogC3 inverse
|
||||
transform. ``__call__`` returns a **linear HDR float** tensor
|
||||
``[f, h, w, c]``; tonemapping and EXR saving are the caller's
|
||||
responsibility.
|
||||
Text embeddings must be pre-computed externally (e.g. using
|
||||
``PromptEncoder`` from ``ltx_pipelines.utils.blocks`` with a Gemma text
|
||||
encoder) and saved as a ``.safetensors`` file with ``video_context``
|
||||
and ``audio_context`` tensors (via ``safetensors.torch.save_file``).
|
||||
The path is passed via ``text_embeddings_path``.
|
||||
Run as a script for batch inference::
|
||||
python -m ltx_pipelines.hdr_ic_lora \\
|
||||
--input ./videos/ \\
|
||||
--output-dir ./hdr-output \\
|
||||
--distilled-checkpoint-path /models/ltx-2.3-22b-distilled.safetensors \\
|
||||
--spatial-upsampler-path /models/ltx-2.3-spatial-upscaler-x2-1.0.safetensors \\
|
||||
--hdr-lora /path/to/hdr_lora.safetensors \\
|
||||
--text-embeddings /path/to/hdr_scene_emb.safetensors \\
|
||||
--num-frames 161
|
||||
Supports resolutions up to 4K (3840x2160 @ 121 frames on 80 GB,
|
||||
49 frames on 48 GB). The caller is responsible for choosing a resolution
|
||||
and frame count that fits in GPU memory. See ``--help`` for a reference
|
||||
table, or use ``ltx_pipelines.utils.vram_budget.max_frames_for_resolution``
|
||||
to query your specific configuration.
|
||||
"""
|
||||
|
||||
import dataclasses
|
||||
import logging
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
from einops import rearrange
|
||||
from safetensors import safe_open
|
||||
|
||||
from ltx_core.components.noisers import GaussianNoiser
|
||||
from ltx_core.components.patchifiers import VideoLatentPatchifier
|
||||
from ltx_core.conditioning import (
|
||||
ConditioningItem,
|
||||
VideoConditionByReferenceLatent,
|
||||
)
|
||||
from ltx_core.hdr import apply_hdr_decode_postprocess
|
||||
from ltx_core.loader import LoraPathStrengthAndSDOps
|
||||
from ltx_core.loader.registry import Registry
|
||||
from ltx_core.loader.sd_ops import LTXV_LORA_COMFY_RENAMING_MAP
|
||||
from ltx_core.modality_tiling import VideoModalityTilingHelper
|
||||
from ltx_core.model.video_vae import TilingConfig, VideoEncoder
|
||||
from ltx_core.quantization import QuantizationPolicy
|
||||
from ltx_core.tiling import DimensionTilingConfig, TileCountConfig
|
||||
from ltx_core.tools import VideoLatentTools
|
||||
from ltx_core.types import VideoLatentShape
|
||||
from ltx_pipelines.utils.blocks import (
|
||||
DiffusionStage,
|
||||
ImageConditioner,
|
||||
VideoDecoder,
|
||||
VideoUpsampler,
|
||||
)
|
||||
from ltx_pipelines.utils.constants import DISTILLED_SIGMA_VALUES, STAGE_2_DISTILLED_SIGMA_VALUES
|
||||
from ltx_pipelines.utils.denoisers import SimpleDenoiser
|
||||
from ltx_pipelines.utils.helpers import get_device, modality_from_latent_state
|
||||
from ltx_pipelines.utils.media_io import ResizeMode, align_resolution, load_video_conditioning_hdr
|
||||
from ltx_pipelines.utils.types import ModalitySpec, OffloadMode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
DEFAULT_NUM_FRAMES = 161
|
||||
MIN_RESOLUTION = 64
|
||||
ALIGNMENT_DIVISOR = 64
|
||||
|
||||
# Conditioning videos whose spatial resolution (H x W) exceeds this value are
|
||||
# encoded with the tiled encoder. The default (512 x 768) is suitable for
|
||||
# H100-80GB. On lower-VRAM GPUs pass tiled_vae_encode_pixel_threshold=256*256
|
||||
# to the pipeline constructor.
|
||||
TILED_VAE_ENCODE_PIXEL_THRESHOLD = 512 * 768
|
||||
|
||||
_DEFAULT_QUANTIZATION = QuantizationPolicy.fp8_cast()
|
||||
|
||||
# Default stage-2 configuration: one refinement phase with modest 2-way tiling
|
||||
# in every dimension and a short 2-step distilled sigma schedule.
|
||||
_S2 = STAGE_2_DISTILLED_SIGMA_VALUES
|
||||
|
||||
_TILED_2F2H2W_OV8_6 = TileCountConfig(
|
||||
frames=DimensionTilingConfig(2, 8),
|
||||
height=DimensionTilingConfig(2, 6),
|
||||
width=DimensionTilingConfig(2, 6),
|
||||
)
|
||||
|
||||
STAGE2_TILINGS = [_TILED_2F2H2W_OV8_6]
|
||||
STAGE2_SIGMAS = [[_S2[0], _S2[1], 0.0]]
|
||||
STAGE2_USE_IC_LORA = [True]
|
||||
|
||||
|
||||
def _clamp_dim_tiling(cfg: DimensionTilingConfig, dim_size: int, axis: str) -> DimensionTilingConfig:
|
||||
"""Clamp a single dim's tile count and overlap to the latent's extent.
|
||||
``split_by_count`` requires ``overlap < tile_size``; with
|
||||
``tile_size = (dim_size + overlap*(n-1)) // n`` this reduces to
|
||||
``overlap <= dim_size - n``. When the configured overlap exceeds this
|
||||
bound it is clamped; if the latent is too small to hold ``n`` tiles
|
||||
at all, tiling falls back to a single tile on this axis.
|
||||
"""
|
||||
n = cfg.num_tiles
|
||||
if n <= 1:
|
||||
return cfg
|
||||
if dim_size < n:
|
||||
logger.warning(
|
||||
"%s tiling: dim_size=%d < num_tiles=%d; falling back to 1 tile on this axis.",
|
||||
axis,
|
||||
dim_size,
|
||||
n,
|
||||
)
|
||||
return DimensionTilingConfig(1, 0)
|
||||
max_overlap = dim_size - n
|
||||
if cfg.overlap <= max_overlap:
|
||||
return cfg
|
||||
logger.warning(
|
||||
"%s tiling: overlap=%d exceeds latent bound (%d); clamping to %d.",
|
||||
axis,
|
||||
cfg.overlap,
|
||||
max_overlap,
|
||||
max_overlap,
|
||||
)
|
||||
return DimensionTilingConfig(n, max_overlap)
|
||||
|
||||
|
||||
def _clamp_tile_to_latent(tiling: TileCountConfig, latent_shape: tuple[int, int, int]) -> TileCountConfig:
|
||||
"""Clamp frame, height, and width tilings to the latent's extents.
|
||||
``latent_shape`` is ``(F, H, W)`` in latent units.
|
||||
"""
|
||||
f, h, w = latent_shape
|
||||
return replace(
|
||||
tiling,
|
||||
frames=_clamp_dim_tiling(tiling.frames, f, "Frame"),
|
||||
height=_clamp_dim_tiling(tiling.height, h, "Height"),
|
||||
width=_clamp_dim_tiling(tiling.width, w, "Width"),
|
||||
)
|
||||
|
||||
|
||||
# Default tiling config (spatial tile 1280 px, overlap 256 px; temporal 32
|
||||
# frames, overlap 16). On GPUs with < 80 GB VRAM you may need to shrink
|
||||
# the spatial tile size (e.g. 768) to avoid OOM during VAE decode.
|
||||
DEFAULT_SPATIAL_TILE = 1280
|
||||
DEFAULT_SPATIAL_OVERLAP = 256
|
||||
DEFAULT_TEMPORAL_TILE = 32
|
||||
DEFAULT_TEMPORAL_OVERLAP = 16
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# HDR LoRA config
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class HdrLoraConfig:
|
||||
"""Explicit HDR LoRA parameters.
|
||||
Read from LoRA safetensors metadata by :func:`read_hdr_lora_config`, or
|
||||
constructed manually for testing.
|
||||
"""
|
||||
|
||||
hdr_transform: str = "logc3"
|
||||
reference_downscale_factor: int = 1
|
||||
|
||||
|
||||
def read_hdr_lora_config(lora_path: str) -> HdrLoraConfig | None:
|
||||
"""Read HDR config from LoRA safetensors metadata.
|
||||
Returns ``None`` when the LoRA has no HDR metadata.
|
||||
"""
|
||||
try:
|
||||
with safe_open(lora_path, framework="pt") as f:
|
||||
metadata = f.metadata() or {}
|
||||
except (OSError, ValueError) as e:
|
||||
logger.warning("Failed to read metadata from LoRA file '%s': %s", lora_path, e)
|
||||
return None
|
||||
|
||||
hdr_transform = metadata.get("hdr_transform", "")
|
||||
has_hdr = bool(hdr_transform or metadata.get("use_hdr_transform"))
|
||||
if not has_hdr:
|
||||
return None
|
||||
|
||||
transform = hdr_transform if hdr_transform and hdr_transform != "true" else "logc3"
|
||||
scale = int(metadata.get("reference_downscale_factor", 1))
|
||||
return HdrLoraConfig(hdr_transform=transform, reference_downscale_factor=scale)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pipeline
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class HDRICLoraPipeline:
|
||||
"""Two-stage IC-LoRA pipeline with HDR support.
|
||||
Same two-stage architecture as ICLoraPipeline (half-res generation + 2x
|
||||
upscale refinement), with HDR decode via LogC3 inverse.
|
||||
``__call__`` returns a **linear HDR float** tensor ``[f, h, w, c]``.
|
||||
Tonemapping and EXR saving are the caller's responsibility.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
distilled_checkpoint_path: str,
|
||||
spatial_upsampler_path: str,
|
||||
hdr_lora: str | Path,
|
||||
text_embeddings_path: str | Path,
|
||||
device: torch.device | None = None,
|
||||
quantization: QuantizationPolicy = _DEFAULT_QUANTIZATION,
|
||||
registry: Registry | None = None,
|
||||
hdr_lora_config: HdrLoraConfig | None = None,
|
||||
tiled_vae_encode_pixel_threshold: int = TILED_VAE_ENCODE_PIXEL_THRESHOLD,
|
||||
offload_mode: OffloadMode = OffloadMode.NONE,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
distilled_checkpoint_path: Path to the distilled model checkpoint.
|
||||
spatial_upsampler_path: Path to the spatial upsampler checkpoint.
|
||||
hdr_lora: Path to the HDR IC-LoRA ``.safetensors`` file.
|
||||
text_embeddings_path: Path to pre-computed text embeddings
|
||||
(``.safetensors`` file with ``video_context`` and
|
||||
``audio_context`` tensors).
|
||||
device: Target device. Auto-detected when ``None``.
|
||||
quantization: Quantization policy. Defaults to ``fp8_cast``.
|
||||
registry: Optional model registry for caching loaded components.
|
||||
hdr_lora_config: Explicit HDR LoRA config override. When ``None``,
|
||||
auto-detected from LoRA safetensors metadata.
|
||||
tiled_vae_encode_pixel_threshold: Conditioning videos whose spatial
|
||||
area (H x W) exceeds this value are encoded with the tiled
|
||||
encoder. Default ``512 * 768`` is suitable for 80 GB GPUs.
|
||||
Use ``256 * 256`` on GPUs with less VRAM.
|
||||
offload_mode: Weight offloading strategy for diffusion stages.
|
||||
"""
|
||||
self.device = device or get_device()
|
||||
self._tiled_vae_encode_threshold = tiled_vae_encode_pixel_threshold
|
||||
if offload_mode != OffloadMode.NONE and quantization is not None:
|
||||
logger.info("Offload mode enabled — disabling quantization (not supported with layer streaming).")
|
||||
quantization = None
|
||||
self.dtype = torch.bfloat16
|
||||
|
||||
lora_path = str(Path(hdr_lora).resolve())
|
||||
loras = (LoraPathStrengthAndSDOps(lora_path, 1.0, LTXV_LORA_COMFY_RENAMING_MAP),)
|
||||
|
||||
# Load pre-computed text embeddings from safetensors.
|
||||
emb_path = Path(text_embeddings_path)
|
||||
logger.info("Loading text embeddings from %s", emb_path)
|
||||
with safe_open(emb_path, framework="pt", device=str(self.device)) as f:
|
||||
self.text_embeddings: tuple[torch.Tensor, torch.Tensor] = (
|
||||
f.get_tensor("video_context"),
|
||||
f.get_tensor("audio_context"),
|
||||
)
|
||||
|
||||
self.image_conditioner = ImageConditioner(distilled_checkpoint_path, self.dtype, self.device, registry=registry)
|
||||
self.stage_1 = DiffusionStage(
|
||||
distilled_checkpoint_path,
|
||||
self.dtype,
|
||||
self.device,
|
||||
loras=loras,
|
||||
quantization=quantization,
|
||||
registry=registry,
|
||||
offload_mode=offload_mode,
|
||||
)
|
||||
self.stage_2 = DiffusionStage(
|
||||
distilled_checkpoint_path,
|
||||
self.dtype,
|
||||
self.device,
|
||||
loras=loras,
|
||||
quantization=quantization,
|
||||
registry=registry,
|
||||
offload_mode=offload_mode,
|
||||
)
|
||||
self.upsampler = VideoUpsampler(
|
||||
distilled_checkpoint_path, spatial_upsampler_path, self.dtype, self.device, registry=registry
|
||||
)
|
||||
self.video_decoder = VideoDecoder(distilled_checkpoint_path, self.dtype, self.device, registry=registry)
|
||||
|
||||
# HDR config: explicit override, or auto-detect from LoRA metadata.
|
||||
if hdr_lora_config is not None:
|
||||
self._hdr_config: HdrLoraConfig | None = hdr_lora_config
|
||||
else:
|
||||
self._hdr_config = read_hdr_lora_config(lora_path)
|
||||
|
||||
if self._hdr_config is not None:
|
||||
logger.info("[HDR IC-LoRA] HDR mode enabled (%s decode)", self._hdr_config.hdr_transform)
|
||||
|
||||
@property
|
||||
def hdr_transform(self) -> str:
|
||||
"""Active HDR transform name (defaults to 'logc3')."""
|
||||
return self._hdr_config.hdr_transform if self._hdr_config is not None else "logc3"
|
||||
|
||||
@property
|
||||
def reference_downscale_factor(self) -> int:
|
||||
"""Reference video downscale factor from HDR LoRA config."""
|
||||
return self._hdr_config.reference_downscale_factor if self._hdr_config is not None else 1
|
||||
|
||||
def __call__( # noqa: PLR0913
|
||||
self,
|
||||
seed: int,
|
||||
height: int,
|
||||
width: int,
|
||||
num_frames: int,
|
||||
frame_rate: float,
|
||||
video_conditioning: list[tuple[str, float]],
|
||||
tiling_config: TilingConfig | None = None,
|
||||
high_quality_hdr: bool = False,
|
||||
stage2_tilings: list[TileCountConfig] | None = None,
|
||||
stage2_sigmas: list[list[float]] | None = None,
|
||||
stage2_use_ic_lora: list[bool] | None = None,
|
||||
) -> torch.Tensor:
|
||||
"""Generate video with IC-LoRA conditioning and HDR output.
|
||||
Returns a linear HDR float tensor ``[f, h, w, c]``.
|
||||
Args:
|
||||
seed: Random seed for reproducibility.
|
||||
height: Desired output video height in pixels. Aligned internally
|
||||
to the nearest multiple of 64 (rounded up). Decoded output is
|
||||
cropped back to this size.
|
||||
width: Desired output video width in pixels. Same alignment rules
|
||||
as *height*.
|
||||
num_frames: Number of frames to generate.
|
||||
frame_rate: Output video frame rate.
|
||||
video_conditioning: List of (path, strength) tuples for IC-LoRA video conditioning.
|
||||
high_quality_hdr: High-quality HDR mode. Duplicates each conditioning
|
||||
frame and generates at 2x frame count, then keeps every other
|
||||
output frame. Reduces temporal artifacts at the cost of ~2x
|
||||
generation time.
|
||||
Returns:
|
||||
Linear HDR float tensor ``[f, h, w, c]``.
|
||||
"""
|
||||
# In high-quality HDR mode, generate 2*N - 1 frames internally
|
||||
# (satisfies (n-1)%8==0 when N itself does), then keep every other frame.
|
||||
if high_quality_hdr:
|
||||
gen_num_frames = 2 * num_frames - 1
|
||||
logger.info("[HDR IC-LoRA] High-quality HDR: %d -> %d internal frames", num_frames, gen_num_frames)
|
||||
else:
|
||||
gen_num_frames = num_frames
|
||||
gen_w, gen_h, crop_w, crop_h = align_resolution(
|
||||
width, height, ResizeMode.REFLECT_PAD, divisor=ALIGNMENT_DIVISOR
|
||||
)
|
||||
if gen_h < MIN_RESOLUTION or gen_w < MIN_RESOLUTION:
|
||||
raise ValueError(
|
||||
f"Resolution ({width}x{height}) is too small after alignment "
|
||||
f"(got {gen_w}x{gen_h}, need at least {MIN_RESOLUTION}x{MIN_RESOLUTION})."
|
||||
)
|
||||
needs_crop = crop_w != gen_w or crop_h != gen_h
|
||||
if needs_crop:
|
||||
logger.info(
|
||||
"[HDR IC-LoRA] Aligned %dx%d -> %dx%d, will crop to %dx%d",
|
||||
width,
|
||||
height,
|
||||
gen_w,
|
||||
gen_h,
|
||||
crop_w,
|
||||
crop_h,
|
||||
)
|
||||
|
||||
generator = torch.Generator(device=self.device).manual_seed(seed)
|
||||
noiser = GaussianNoiser(generator=generator)
|
||||
|
||||
video_context, _ = self.text_embeddings
|
||||
|
||||
# Stage 1: Initial low resolution video generation.
|
||||
s1_w, s1_h = gen_w // 2, gen_h // 2
|
||||
|
||||
stage_1_conditionings = self.image_conditioner(
|
||||
lambda enc: self._create_conditionings(
|
||||
video_conditioning=video_conditioning,
|
||||
height=s1_h,
|
||||
width=s1_w,
|
||||
video_encoder=enc,
|
||||
num_frames=gen_num_frames,
|
||||
tiling_config=tiling_config,
|
||||
high_quality_hdr=high_quality_hdr,
|
||||
)
|
||||
)
|
||||
|
||||
stage_1_sigmas = torch.Tensor(DISTILLED_SIGMA_VALUES).to(self.device)
|
||||
|
||||
# HDR is video-only: skip the audio stream to avoid denoising 5B audio params.
|
||||
video_state, _ = self.stage_1(
|
||||
denoiser=SimpleDenoiser(video_context, None),
|
||||
sigmas=stage_1_sigmas,
|
||||
noiser=noiser,
|
||||
width=s1_w,
|
||||
height=s1_h,
|
||||
frames=gen_num_frames,
|
||||
fps=frame_rate,
|
||||
video=ModalitySpec(
|
||||
context=video_context,
|
||||
conditionings=stage_1_conditionings,
|
||||
),
|
||||
)
|
||||
|
||||
if stage2_tilings is None:
|
||||
stage2_tilings = list(STAGE2_TILINGS)
|
||||
if stage2_sigmas is None:
|
||||
stage2_sigmas = [list(s) for s in STAGE2_SIGMAS]
|
||||
if stage2_use_ic_lora is None:
|
||||
stage2_use_ic_lora = list(STAGE2_USE_IC_LORA)
|
||||
if not (len(stage2_tilings) == len(stage2_sigmas) == len(stage2_use_ic_lora)):
|
||||
raise ValueError("stage2_tilings, stage2_sigmas, and stage2_use_ic_lora must have equal length")
|
||||
|
||||
# Stage 2: Upsample and refine at full resolution.
|
||||
upscaled_video_latent = self.upsampler(video_state.latent[:1])
|
||||
|
||||
stage_2_conditionings = self.image_conditioner(
|
||||
lambda enc: self._create_conditionings(
|
||||
video_conditioning=video_conditioning,
|
||||
height=gen_h,
|
||||
width=gen_w,
|
||||
video_encoder=enc,
|
||||
num_frames=gen_num_frames,
|
||||
tiling_config=tiling_config,
|
||||
high_quality_hdr=high_quality_hdr,
|
||||
)
|
||||
)
|
||||
with self.stage_2.model_context() as transformer:
|
||||
phase_latent = upscaled_video_latent
|
||||
for phase_idx, (tiling, sigmas_list, use_ic) in enumerate(
|
||||
zip(stage2_tilings, stage2_sigmas, stage2_use_ic_lora, strict=True)
|
||||
):
|
||||
diffusion_tiling = _clamp_tile_to_latent(tiling, tuple(phase_latent.shape[2:5]))
|
||||
conditionings = stage_2_conditionings if use_ic else []
|
||||
sigma_t = torch.tensor(sigmas_list, dtype=torch.float32, device=self.device)
|
||||
logger.info(
|
||||
"[Stage 2 / phase %d] sigmas=%s ic_lora=%s tiling_h=%s tiling_w=%s",
|
||||
phase_idx,
|
||||
sigmas_list,
|
||||
use_ic,
|
||||
diffusion_tiling.height,
|
||||
diffusion_tiling.width,
|
||||
)
|
||||
phase_latent = self._run_stage2_phase(
|
||||
transformer=transformer,
|
||||
latent=phase_latent,
|
||||
conditionings=conditionings,
|
||||
tiling=diffusion_tiling,
|
||||
sigmas=sigma_t,
|
||||
v_ctx=video_context,
|
||||
frame_rate=frame_rate,
|
||||
seed=seed,
|
||||
)
|
||||
|
||||
final_video_latent = phase_latent
|
||||
|
||||
crop_size = (crop_w, crop_h) if needs_crop else None
|
||||
return self._decode_video(
|
||||
final_video_latent,
|
||||
tiling_config,
|
||||
generator,
|
||||
crop_size,
|
||||
high_quality_hdr=high_quality_hdr,
|
||||
)
|
||||
|
||||
def _run_stage2_phase(
|
||||
self,
|
||||
transformer: object,
|
||||
latent: torch.Tensor,
|
||||
conditionings: list[ConditioningItem],
|
||||
tiling: TileCountConfig,
|
||||
sigmas: torch.Tensor,
|
||||
v_ctx: torch.Tensor,
|
||||
frame_rate: float,
|
||||
seed: int,
|
||||
) -> torch.Tensor:
|
||||
"""Run one stage-2 denoising phase with optional IC-LoRA conditioning.
|
||||
Each tile calls ``stage_2.run()`` with a tile-sized ``ModalitySpec`` for
|
||||
video only (audio is omitted entirely for HDR). IC-LoRA conditionings
|
||||
are sliced spatially to match each tile's extent.
|
||||
"""
|
||||
batch, n_channels, n_frames, n_height, n_width = latent.shape
|
||||
full_shape = VideoLatentShape(batch=batch, channels=n_channels, frames=n_frames, height=n_height, width=n_width)
|
||||
full_tools = VideoLatentTools(VideoLatentPatchifier(patch_size=1), full_shape, frame_rate)
|
||||
helper = VideoModalityTilingHelper(tiling, full_tools)
|
||||
|
||||
ref_initial = full_tools.create_initial_state(device=self.device, dtype=self.dtype)
|
||||
ref_modality = modality_from_latent_state(ref_initial, v_ctx, sigmas[0])
|
||||
n_gen = full_tools.target_shape.token_count()
|
||||
blend_output = torch.zeros(batch, n_gen, n_channels, device=self.device, dtype=self.dtype)
|
||||
patchifier = VideoLatentPatchifier(patch_size=1)
|
||||
df = self.reference_downscale_factor
|
||||
|
||||
for tile_idx, tile in enumerate(helper.tiles):
|
||||
_, ctx = helper.tile_modality(ref_modality, tile, normalize_positions=True)
|
||||
frame_s, height_s, width_s = tile.in_coords
|
||||
tile_h = height_s.stop - height_s.start
|
||||
tile_w = width_s.stop - width_s.start
|
||||
tile_f = frame_s.stop - frame_s.start
|
||||
|
||||
tile_conditionings = [
|
||||
VideoConditionByReferenceLatent(
|
||||
latent=cond.latent[
|
||||
:,
|
||||
:,
|
||||
frame_s,
|
||||
slice(height_s.start // df, height_s.stop // df),
|
||||
slice(width_s.start // df, width_s.stop // df),
|
||||
].to(device=self.device, dtype=self.dtype),
|
||||
downscale_factor=cond.downscale_factor,
|
||||
strength=cond.strength,
|
||||
)
|
||||
for cond in conditionings
|
||||
]
|
||||
|
||||
tile_video_state, _ = self.stage_2.run(
|
||||
transformer=transformer,
|
||||
denoiser=SimpleDenoiser(v_ctx, None),
|
||||
sigmas=sigmas,
|
||||
noiser=GaussianNoiser(generator=torch.Generator(device=self.device).manual_seed(seed + tile_idx)),
|
||||
width=tile_w * 32,
|
||||
height=tile_h * 32,
|
||||
frames=(tile_f - 1) * 8 + 1,
|
||||
fps=frame_rate,
|
||||
video=ModalitySpec(
|
||||
context=v_ctx,
|
||||
conditionings=tile_conditionings,
|
||||
noise_scale=sigmas[0].item(),
|
||||
initial_latent=latent[:, :, frame_s, height_s, width_s].to(device=self.device, dtype=self.dtype),
|
||||
),
|
||||
)
|
||||
|
||||
tile_tokens = patchifier.patchify(tile_video_state.latent)
|
||||
blend_output = helper.blend(tile_tokens, tile, ctx, blend_output)
|
||||
|
||||
return full_tools.unpatchify(replace(ref_initial, latent=blend_output)).latent
|
||||
|
||||
def _decode_video(
|
||||
self,
|
||||
latent: torch.Tensor,
|
||||
tiling_config: TilingConfig | None,
|
||||
generator: torch.Generator,
|
||||
crop_size: tuple[int, int] | None = None,
|
||||
*,
|
||||
high_quality_hdr: bool = False,
|
||||
) -> torch.Tensor:
|
||||
"""Decode latent to HDR video, optionally cropping to target size.
|
||||
Args:
|
||||
crop_size: ``(width, height)`` to crop decoded frames to, or
|
||||
``None`` to skip cropping.
|
||||
high_quality_hdr: When True, keep only every other frame (undoes the
|
||||
2x generation applied during high-quality HDR mode).
|
||||
Returns:
|
||||
Linear HDR float tensor ``[f, h, w, c]``.
|
||||
"""
|
||||
# Cast to float32 so tiled-decode accumulation buffers and blending
|
||||
# masks run in full precision, avoiding bfloat16 seam artifacts.
|
||||
# Request float32 [0, 1] output — apply_hdr_decode_postprocess expects it.
|
||||
latent = latent.float()
|
||||
decoded = torch.cat(
|
||||
list(self.video_decoder(latent, tiling_config, generator, output_dtype=torch.float32)),
|
||||
dim=0,
|
||||
)
|
||||
decoded = rearrange(decoded, "f h w c -> 1 c f h w")
|
||||
hdr = apply_hdr_decode_postprocess(decoded, transform=self.hdr_transform)
|
||||
del decoded
|
||||
out = rearrange(hdr[0], "c f h w -> f h w c")
|
||||
if crop_size is not None:
|
||||
out = out[:, : crop_size[1], : crop_size[0], :]
|
||||
if high_quality_hdr:
|
||||
out = out[::2]
|
||||
return out
|
||||
|
||||
def _create_conditionings(
|
||||
self,
|
||||
video_conditioning: list[tuple[str, float]],
|
||||
height: int,
|
||||
width: int,
|
||||
num_frames: int,
|
||||
video_encoder: VideoEncoder,
|
||||
tiling_config: TilingConfig | None = None,
|
||||
high_quality_hdr: bool = False,
|
||||
) -> list[ConditioningItem]:
|
||||
"""Create conditioning items for video generation."""
|
||||
conditionings: list[ConditioningItem] = []
|
||||
|
||||
scale = self.reference_downscale_factor
|
||||
if scale != 1 and (height % scale != 0 or width % scale != 0):
|
||||
raise ValueError(
|
||||
f"Output dimensions ({height}x{width}) must be divisible by reference_downscale_factor ({scale})"
|
||||
)
|
||||
ref_height = height // scale
|
||||
ref_width = width // scale
|
||||
|
||||
# In high-quality HDR mode, load half the frames then duplicate each one.
|
||||
load_frame_cap = (num_frames + 1) // 2 if high_quality_hdr else num_frames
|
||||
|
||||
for video_path, strength in video_conditioning:
|
||||
video = torch.cat(
|
||||
list(
|
||||
load_video_conditioning_hdr(
|
||||
video_path=video_path,
|
||||
height=ref_height,
|
||||
width=ref_width,
|
||||
frame_cap=load_frame_cap,
|
||||
dtype=self.dtype,
|
||||
device=self.device,
|
||||
hdr_transform=self.hdr_transform,
|
||||
resize_mode=ResizeMode.REFLECT_PAD,
|
||||
)
|
||||
),
|
||||
dim=2,
|
||||
)
|
||||
if high_quality_hdr:
|
||||
video = video.repeat_interleave(2, dim=2)[:, :, :num_frames, :, :]
|
||||
if tiling_config is not None and ref_height * ref_width > self._tiled_vae_encode_threshold:
|
||||
encoded_video = video_encoder.tiled_encode(video, tiling_config)
|
||||
else:
|
||||
encoded_video = video_encoder(video)
|
||||
|
||||
cond = VideoConditionByReferenceLatent(
|
||||
latent=encoded_video,
|
||||
downscale_factor=scale,
|
||||
strength=strength,
|
||||
)
|
||||
conditionings.append(cond)
|
||||
|
||||
if video_conditioning:
|
||||
logger.info("[HDR IC-LoRA] Added %d video conditioning(s)", len(video_conditioning))
|
||||
|
||||
return conditionings
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_tiling_config(
|
||||
spatial_tile: int = DEFAULT_SPATIAL_TILE,
|
||||
spatial_overlap: int = DEFAULT_SPATIAL_OVERLAP,
|
||||
temporal_tile: int = DEFAULT_TEMPORAL_TILE,
|
||||
temporal_overlap: int = DEFAULT_TEMPORAL_OVERLAP,
|
||||
) -> TilingConfig:
|
||||
"""Build a TilingConfig from explicit sizes.
|
||||
The defaults (1280 px spatial tile, 256 px overlap; 32 temporal frames,
|
||||
16 overlap) are suitable for H100-80 GB. On GPUs with less VRAM,
|
||||
reduce the spatial tile size (e.g. ``spatial_tile=768``).
|
||||
"""
|
||||
from ltx_core.model.video_vae.tiling import SpatialTilingConfig, TemporalTilingConfig # noqa: PLC0415
|
||||
|
||||
return TilingConfig(
|
||||
spatial_config=SpatialTilingConfig(tile_size_in_pixels=spatial_tile, tile_overlap_in_pixels=spatial_overlap),
|
||||
temporal_config=TemporalTilingConfig(
|
||||
tile_size_in_frames=temporal_tile,
|
||||
tile_overlap_in_frames=temporal_overlap,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
_VIDEO_SUFFIXES = {".mp4", ".mov"}
|
||||
|
||||
|
||||
def _collect_videos(input_path: Path) -> list[Path]:
|
||||
"""Return a list of .mp4/.mov files from *input_path* (file or directory)."""
|
||||
if input_path.is_file():
|
||||
return [input_path]
|
||||
if input_path.is_dir():
|
||||
return sorted(p for p in input_path.iterdir() if p.is_file() and p.suffix.lower() in _VIDEO_SUFFIXES)
|
||||
logger.error("Input %s is not a file or directory", input_path)
|
||||
return []
|
||||
|
||||
|
||||
def _process_single_video( # noqa: PLR0913
|
||||
pipeline: HDRICLoraPipeline,
|
||||
video_path: Path,
|
||||
vid_w: int,
|
||||
vid_h: int,
|
||||
num_frames: int,
|
||||
frame_rate: float,
|
||||
output_dir: Path,
|
||||
tiling_config: TilingConfig,
|
||||
seed: int,
|
||||
skip_mp4: bool,
|
||||
exr_half: bool,
|
||||
exr_executor: "ThreadPoolExecutor", # noqa: F821
|
||||
exr_futures: list,
|
||||
high_quality_hdr: bool = False,
|
||||
) -> None:
|
||||
"""Run inference on a single video: generate EXR frames + optional H.264 .mp4 preview."""
|
||||
import gc # noqa: PLC0415
|
||||
import time # noqa: PLC0415
|
||||
|
||||
from ltx_pipelines.utils.media_io import encode_exr_sequence_to_mp4, save_exr_tensor # noqa: PLC0415
|
||||
|
||||
output_mp4 = output_dir / f"{video_path.stem}.mp4"
|
||||
exr_dir = output_dir / f"{video_path.stem}_exr"
|
||||
|
||||
t0 = time.time()
|
||||
hdr_video = pipeline(
|
||||
seed=seed,
|
||||
height=vid_h,
|
||||
width=vid_w,
|
||||
num_frames=num_frames,
|
||||
frame_rate=frame_rate,
|
||||
video_conditioning=[(str(video_path), 1.0)],
|
||||
tiling_config=tiling_config,
|
||||
high_quality_hdr=high_quality_hdr,
|
||||
)
|
||||
|
||||
exr_dir.mkdir(parents=True, exist_ok=True)
|
||||
for j in range(hdr_video.shape[0]):
|
||||
frame_cpu = hdr_video[j].cpu().clone()
|
||||
path = exr_dir / f"frame_{j:05d}.exr"
|
||||
exr_futures.append(exr_executor.submit(save_exr_tensor, frame_cpu, str(path), exr_half))
|
||||
|
||||
del hdr_video
|
||||
gc.collect()
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
if not skip_mp4:
|
||||
# Wait for EXR saves to finish before encoding.
|
||||
for fut in exr_futures:
|
||||
fut.result()
|
||||
logger.info("Encoding H.264 sRGB preview: %s", video_path.name)
|
||||
encode_exr_sequence_to_mp4(exr_dir, output_mp4, frame_rate)
|
||||
|
||||
elapsed = time.time() - t0
|
||||
logger.info("Decode + encode: %.1fs | %s", elapsed, output_mp4)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI entry point
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _build_arg_parser() -> "argparse.ArgumentParser": # noqa: F821
|
||||
"""Build the argument parser for HDR IC-LoRA batch inference."""
|
||||
import argparse # noqa: PLC0415
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="HDR IC-LoRA inference: EXR frames + tonemapped ProRes .mov.",
|
||||
epilog="""\
|
||||
Resolution & frame constraints
|
||||
------------------------------
|
||||
* Width and height must each be divisible by 32.
|
||||
* Frame count must satisfy (frames - 1) %% 8 == 0.
|
||||
Valid counts: 1, 9, 17, 25, ..., 121, 129, 137, 145, 153, 161.
|
||||
|
||||
Max frames by resolution (fp8_cast, bfloat16 VAE, tiled decode)
|
||||
---------------------------------------------------------------
|
||||
Resolution 80 GB (H100) 48 GB (A6000)
|
||||
------------------------------------------------
|
||||
720p 1280x720 161+ frames 161+ frames
|
||||
1080p 1920x1080 161+ frames 161+ frames
|
||||
2K 2048x1080 161+ frames 161+ frames
|
||||
1440p 2560x1440 161+ frames 137 frames
|
||||
4K 3840x2160 121 frames 49 frames
|
||||
4K 4096x2160 105 frames 49 frames
|
||||
|
||||
Estimates from ltx_pipelines.utils.vram_budget. Run
|
||||
python -c "from ltx_pipelines.utils.vram_budget import \\
|
||||
max_frames_for_resolution as mf; print(mf(W, H, vram_gb=GB))"
|
||||
to check your specific resolution and GPU.
|
||||
|
||||
* The tiled-encode threshold (%(tiled_threshold)s px) and the default
|
||||
tiling config (%(stile)s px spatial tile) are tuned for 80 GB.
|
||||
On lower-VRAM GPUs pass --spatial-tile 768 (or smaller).
|
||||
"""
|
||||
% {"tiled_threshold": TILED_VAE_ENCODE_PIXEL_THRESHOLD, "stile": DEFAULT_SPATIAL_TILE},
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument("--input", required=True, help="Single .mp4 or directory of .mp4 videos.")
|
||||
parser.add_argument("--output-dir", required=True, help="Directory for .mov and EXR folders.")
|
||||
parser.add_argument("--hdr-lora", required=True, help="HDR IC-LoRA .safetensors file.")
|
||||
parser.add_argument("--text-embeddings", required=True, help="Pre-computed text embeddings (.safetensors file).")
|
||||
parser.add_argument("--distilled-checkpoint-path", required=True, help="Distilled model checkpoint (.safetensors).")
|
||||
parser.add_argument("--spatial-upsampler-path", required=True, help="Spatial upsampler (.safetensors).")
|
||||
parser.add_argument(
|
||||
"--num-frames",
|
||||
type=int,
|
||||
default=DEFAULT_NUM_FRAMES,
|
||||
help=f"Number of output frames. Must satisfy (n-1) %% 8 == 0 (default: {DEFAULT_NUM_FRAMES}).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--spatial-tile",
|
||||
type=int,
|
||||
default=DEFAULT_SPATIAL_TILE,
|
||||
help=f"Spatial tile size in pixels for tiled VAE decode (default: {DEFAULT_SPATIAL_TILE}). "
|
||||
"Reduce on lower-VRAM GPUs (e.g. 768 for 48 GB).",
|
||||
)
|
||||
parser.add_argument("--skip-mp4", action="store_true", help="Skip H.264 MP4 encoding, only produce EXR.")
|
||||
parser.add_argument("--exr-half", action="store_true", help="Save EXR as float16.")
|
||||
parser.add_argument("--seed", type=int, default=10, help="Random seed (default: 10).")
|
||||
parser.add_argument(
|
||||
"--offload",
|
||||
dest="offload_mode",
|
||||
type=OffloadMode,
|
||||
default=OffloadMode.NONE,
|
||||
choices=list(OffloadMode),
|
||||
help=(
|
||||
"Weight offloading strategy. "
|
||||
"'none' keeps all weights on GPU (default). "
|
||||
"'cpu' pins weights in CPU RAM, streams to GPU per layer. "
|
||||
"'disk' reads weights from disk on demand (lowest memory). "
|
||||
"Example: --offload cpu"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--high-quality",
|
||||
action="store_true",
|
||||
help="High-quality HDR mode. Generates at 2x frame count internally "
|
||||
"and keeps every other frame for smoother output. ~2x slower.",
|
||||
)
|
||||
return parser
|
||||
@torch.inference_mode()
|
||||
def main() -> None:
|
||||
"""Batch HDR IC-LoRA inference: per-frame EXR + tonemapped ProRes .mov."""
|
||||
import time # noqa: PLC0415
|
||||
from concurrent.futures import ThreadPoolExecutor # noqa: PLC0415
|
||||
|
||||
from ltx_pipelines.utils.media_io import get_videostream_metadata # noqa: PLC0415
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
args = _build_arg_parser().parse_args()
|
||||
high_quality = args.high_quality
|
||||
num_frames = args.num_frames
|
||||
|
||||
tiling_config = _make_tiling_config(spatial_tile=args.spatial_tile)
|
||||
|
||||
input_path = Path(args.input)
|
||||
output_dir = Path(args.output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
videos = _collect_videos(input_path)
|
||||
if not videos:
|
||||
logger.error("No valid videos to process.")
|
||||
return
|
||||
logger.info("Found %d video(s), generating %d frames each", len(videos), num_frames)
|
||||
|
||||
logger.info("Loading pipeline...")
|
||||
pipeline = HDRICLoraPipeline(
|
||||
distilled_checkpoint_path=args.distilled_checkpoint_path,
|
||||
spatial_upsampler_path=args.spatial_upsampler_path,
|
||||
hdr_lora=args.hdr_lora,
|
||||
text_embeddings_path=args.text_embeddings,
|
||||
offload_mode=args.offload_mode,
|
||||
)
|
||||
logger.info("Pipeline loaded.")
|
||||
|
||||
exr_executor = ThreadPoolExecutor(max_workers=4)
|
||||
exr_futures: list = []
|
||||
|
||||
total_t0 = time.time()
|
||||
successes = 0
|
||||
|
||||
for i, video_path in enumerate(videos, 1):
|
||||
meta = get_videostream_metadata(str(video_path))
|
||||
vid_w, vid_h = meta.width, meta.height
|
||||
logger.info("%s", "=" * 60)
|
||||
logger.info("[%d/%d] %s (%dx%d, %df)", i, len(videos), video_path.name, vid_w, vid_h, num_frames)
|
||||
|
||||
_process_single_video(
|
||||
pipeline=pipeline,
|
||||
video_path=video_path,
|
||||
vid_w=vid_w,
|
||||
vid_h=vid_h,
|
||||
num_frames=num_frames,
|
||||
frame_rate=meta.fps,
|
||||
output_dir=output_dir,
|
||||
tiling_config=tiling_config,
|
||||
seed=args.seed,
|
||||
skip_mp4=args.skip_mp4,
|
||||
exr_half=args.exr_half,
|
||||
exr_executor=exr_executor,
|
||||
exr_futures=exr_futures,
|
||||
high_quality_hdr=high_quality,
|
||||
)
|
||||
successes += 1
|
||||
|
||||
infer_elapsed = time.time() - total_t0
|
||||
logger.info("%s", "=" * 60)
|
||||
logger.info("All inference done in %.0fs (%d/%d OK)", infer_elapsed, successes, len(videos))
|
||||
|
||||
if exr_futures:
|
||||
t0 = time.time()
|
||||
logger.info("Waiting for %d EXR saves...", len(exr_futures))
|
||||
for fut in exr_futures:
|
||||
fut.result()
|
||||
exr_wait = time.time() - t0
|
||||
if exr_wait > 0.1:
|
||||
logger.info("EXR save wait: %.1fs", exr_wait)
|
||||
|
||||
logger.info("Total wall time: %.0fs", time.time() - total_t0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -39,7 +39,7 @@ from ltx_pipelines.utils.constants import (
|
||||
from ltx_pipelines.utils.denoisers import SimpleDenoiser
|
||||
from ltx_pipelines.utils.helpers import assert_resolution, combined_image_conditionings, get_device
|
||||
from ltx_pipelines.utils.media_io import decode_video_by_frame, encode_video, video_preprocess
|
||||
from ltx_pipelines.utils.types import ModalitySpec
|
||||
from ltx_pipelines.utils.types import ModalitySpec, OffloadMode
|
||||
|
||||
|
||||
class ICLoraPipeline:
|
||||
@@ -63,12 +63,18 @@ class ICLoraPipeline:
|
||||
quantization: QuantizationPolicy | None = None,
|
||||
registry: Registry | None = None,
|
||||
torch_compile: bool = False,
|
||||
offload_mode: OffloadMode = OffloadMode.NONE,
|
||||
):
|
||||
self.device = device or get_device()
|
||||
self.dtype = torch.bfloat16
|
||||
|
||||
self.prompt_encoder = PromptEncoder(
|
||||
distilled_checkpoint_path, gemma_root, self.dtype, self.device, registry=registry
|
||||
distilled_checkpoint_path,
|
||||
gemma_root,
|
||||
self.dtype,
|
||||
self.device,
|
||||
registry=registry,
|
||||
offload_mode=offload_mode,
|
||||
)
|
||||
self.image_conditioner = ImageConditioner(distilled_checkpoint_path, self.dtype, self.device, registry=registry)
|
||||
self.stage_1 = DiffusionStage(
|
||||
@@ -79,6 +85,7 @@ class ICLoraPipeline:
|
||||
quantization=quantization,
|
||||
registry=registry,
|
||||
torch_compile=torch_compile,
|
||||
offload_mode=offload_mode,
|
||||
)
|
||||
self.stage_2 = DiffusionStage(
|
||||
distilled_checkpoint_path,
|
||||
@@ -88,6 +95,7 @@ class ICLoraPipeline:
|
||||
quantization=quantization,
|
||||
registry=registry,
|
||||
torch_compile=torch_compile,
|
||||
offload_mode=offload_mode,
|
||||
)
|
||||
self.upsampler = VideoUpsampler(
|
||||
distilled_checkpoint_path, spatial_upsampler_path, self.dtype, self.device, registry=registry
|
||||
@@ -125,7 +133,6 @@ class ICLoraPipeline:
|
||||
conditioning_attention_strength: float = 1.0,
|
||||
skip_stage_2: bool = False,
|
||||
conditioning_attention_mask: torch.Tensor | None = None,
|
||||
streaming_prefetch_count: int | None = None,
|
||||
stage_1_sigmas: torch.Tensor = DISTILLED_SIGMAS,
|
||||
stage_2_sigmas: torch.Tensor = STAGE_2_DISTILLED_SIGMAS,
|
||||
) -> tuple[Iterator[torch.Tensor], Audio]:
|
||||
@@ -175,7 +182,6 @@ class ICLoraPipeline:
|
||||
enhance_first_prompt=enhance_prompt,
|
||||
enhance_prompt_image=images[0][0] if len(images) > 0 else None,
|
||||
enhance_prompt_seed=seed,
|
||||
streaming_prefetch_count=streaming_prefetch_count,
|
||||
)
|
||||
video_context, audio_context = ctx_p.video_encoding, ctx_p.audio_encoding
|
||||
|
||||
@@ -219,7 +225,6 @@ class ICLoraPipeline:
|
||||
audio=ModalitySpec(
|
||||
context=audio_context,
|
||||
),
|
||||
streaming_prefetch_count=streaming_prefetch_count,
|
||||
)
|
||||
|
||||
if skip_stage_2:
|
||||
@@ -264,7 +269,6 @@ class ICLoraPipeline:
|
||||
noise_scale=stage_2_sigmas[0].item(),
|
||||
initial_latent=audio_state.latent,
|
||||
),
|
||||
streaming_prefetch_count=streaming_prefetch_count,
|
||||
)
|
||||
|
||||
decoded_video = self.video_decoder(video_state.latent, tiling_config, generator)
|
||||
@@ -463,6 +467,7 @@ def main() -> None:
|
||||
loras=tuple(args.lora) if args.lora else (),
|
||||
quantization=args.quantization,
|
||||
torch_compile=args.compile,
|
||||
offload_mode=args.offload_mode,
|
||||
)
|
||||
tiling_config = TilingConfig.default()
|
||||
video_chunks_number = get_video_chunks_number(args.num_frames, tiling_config)
|
||||
@@ -479,7 +484,6 @@ def main() -> None:
|
||||
conditioning_attention_strength=conditioning_attention_strength,
|
||||
skip_stage_2=args.skip_stage_2,
|
||||
conditioning_attention_mask=conditioning_attention_mask,
|
||||
streaming_prefetch_count=args.streaming_prefetch_count,
|
||||
)
|
||||
|
||||
encode_video(
|
||||
|
||||
@@ -15,7 +15,11 @@ from ltx_core.loader.registry import Registry
|
||||
from ltx_core.model.video_vae import TilingConfig, get_video_chunks_number
|
||||
from ltx_core.quantization import QuantizationPolicy
|
||||
from ltx_core.types import Audio, VideoPixelShape
|
||||
from ltx_pipelines.utils.args import ImageConditioningInput, default_2_stage_arg_parser, detect_checkpoint_path
|
||||
from ltx_pipelines.utils.args import (
|
||||
ImageConditioningInput,
|
||||
default_2_stage_arg_parser,
|
||||
detect_checkpoint_path,
|
||||
)
|
||||
from ltx_pipelines.utils.blocks import (
|
||||
AudioDecoder,
|
||||
DiffusionStage,
|
||||
@@ -35,7 +39,7 @@ from ltx_pipelines.utils.helpers import (
|
||||
image_conditionings_by_adding_guiding_latent,
|
||||
)
|
||||
from ltx_pipelines.utils.media_io import encode_video
|
||||
from ltx_pipelines.utils.types import ModalitySpec
|
||||
from ltx_pipelines.utils.types import ModalitySpec, OffloadMode
|
||||
|
||||
|
||||
class KeyframeInterpolationPipeline:
|
||||
@@ -59,12 +63,15 @@ class KeyframeInterpolationPipeline:
|
||||
quantization: QuantizationPolicy | None = None,
|
||||
registry: Registry | None = None,
|
||||
torch_compile: bool = False,
|
||||
offload_mode: OffloadMode = OffloadMode.NONE,
|
||||
):
|
||||
self.device = device or get_device()
|
||||
self.dtype = torch.bfloat16
|
||||
self._scheduler = LTX2Scheduler()
|
||||
|
||||
self.prompt_encoder = PromptEncoder(checkpoint_path, gemma_root, self.dtype, self.device, registry=registry)
|
||||
self.prompt_encoder = PromptEncoder(
|
||||
checkpoint_path, gemma_root, self.dtype, self.device, registry=registry, offload_mode=offload_mode
|
||||
)
|
||||
self.image_conditioner = ImageConditioner(checkpoint_path, self.dtype, self.device, registry=registry)
|
||||
self.stage_1 = DiffusionStage(
|
||||
checkpoint_path,
|
||||
@@ -74,6 +81,7 @@ class KeyframeInterpolationPipeline:
|
||||
quantization=quantization,
|
||||
registry=registry,
|
||||
torch_compile=torch_compile,
|
||||
offload_mode=offload_mode,
|
||||
)
|
||||
stage_2_loras = (*tuple(loras), *tuple(distilled_lora))
|
||||
self.stage_2 = DiffusionStage(
|
||||
@@ -84,6 +92,7 @@ class KeyframeInterpolationPipeline:
|
||||
quantization=quantization,
|
||||
registry=registry,
|
||||
torch_compile=torch_compile,
|
||||
offload_mode=offload_mode,
|
||||
)
|
||||
self.upsampler = VideoUpsampler(
|
||||
checkpoint_path, spatial_upsampler_path, self.dtype, self.device, registry=registry
|
||||
@@ -106,7 +115,6 @@ class KeyframeInterpolationPipeline:
|
||||
images: list[ImageConditioningInput],
|
||||
tiling_config: TilingConfig | None = None,
|
||||
enhance_prompt: bool = False,
|
||||
streaming_prefetch_count: int | None = None,
|
||||
max_batch_size: int = 1,
|
||||
stage_1_sigmas: torch.Tensor | None = None,
|
||||
stage_2_sigmas: torch.Tensor = STAGE_2_DISTILLED_SIGMAS,
|
||||
@@ -122,7 +130,6 @@ class KeyframeInterpolationPipeline:
|
||||
enhance_first_prompt=enhance_prompt,
|
||||
enhance_prompt_image=images[0][0] if len(images) > 0 else None,
|
||||
enhance_prompt_seed=seed,
|
||||
streaming_prefetch_count=streaming_prefetch_count,
|
||||
)
|
||||
v_context_p, a_context_p = ctx_p.video_encoding, ctx_p.audio_encoding
|
||||
v_context_n, a_context_n = ctx_n.video_encoding, ctx_n.audio_encoding
|
||||
@@ -179,7 +186,6 @@ class KeyframeInterpolationPipeline:
|
||||
audio=ModalitySpec(
|
||||
context=a_context_p,
|
||||
),
|
||||
streaming_prefetch_count=streaming_prefetch_count,
|
||||
max_batch_size=max_batch_size,
|
||||
)
|
||||
|
||||
@@ -218,7 +224,6 @@ class KeyframeInterpolationPipeline:
|
||||
noise_scale=stage_2_sigmas[0].item(),
|
||||
initial_latent=audio_state.latent,
|
||||
),
|
||||
streaming_prefetch_count=streaming_prefetch_count,
|
||||
)
|
||||
|
||||
decoded_video = self.video_decoder(video_state.latent, tiling_config, generator)
|
||||
@@ -241,6 +246,7 @@ def main() -> None:
|
||||
loras=tuple(args.lora) if args.lora else (),
|
||||
quantization=args.quantization,
|
||||
torch_compile=args.compile,
|
||||
offload_mode=args.offload_mode,
|
||||
)
|
||||
tiling_config = TilingConfig.default()
|
||||
video_chunks_number = get_video_chunks_number(args.num_frames, tiling_config)
|
||||
@@ -271,7 +277,6 @@ def main() -> None:
|
||||
),
|
||||
images=args.images,
|
||||
tiling_config=tiling_config,
|
||||
streaming_prefetch_count=args.streaming_prefetch_count,
|
||||
max_batch_size=args.max_batch_size,
|
||||
)
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ from ltx_pipelines.utils.media_io import (
|
||||
encode_video,
|
||||
get_videostream_metadata,
|
||||
)
|
||||
from ltx_pipelines.utils.types import ModalitySpec
|
||||
from ltx_pipelines.utils.types import ModalitySpec, OffloadMode
|
||||
|
||||
|
||||
class RetakePipeline:
|
||||
@@ -74,6 +74,7 @@ class RetakePipeline:
|
||||
registry: Registry | None = None,
|
||||
distilled: bool = True,
|
||||
torch_compile: bool = False,
|
||||
offload_mode: OffloadMode = OffloadMode.NONE,
|
||||
):
|
||||
self.device = device or get_device()
|
||||
self.dtype = torch.bfloat16
|
||||
@@ -86,6 +87,7 @@ class RetakePipeline:
|
||||
dtype=self.dtype,
|
||||
device=self.device,
|
||||
registry=registry,
|
||||
offload_mode=offload_mode,
|
||||
)
|
||||
self.image_conditioner = ImageConditioner(
|
||||
checkpoint_path=checkpoint_path,
|
||||
@@ -107,6 +109,7 @@ class RetakePipeline:
|
||||
quantization=quantization,
|
||||
registry=registry,
|
||||
torch_compile=torch_compile,
|
||||
offload_mode=offload_mode,
|
||||
)
|
||||
self.video_decoder = VideoDecoder(
|
||||
checkpoint_path=checkpoint_path,
|
||||
@@ -141,7 +144,6 @@ class RetakePipeline:
|
||||
regenerate_audio: bool = True,
|
||||
enhance_prompt: bool = False,
|
||||
tiling_config: TilingConfig | None = None,
|
||||
streaming_prefetch_count: int | None = None,
|
||||
max_batch_size: int = 1,
|
||||
sigmas: torch.Tensor | None = None,
|
||||
) -> tuple[Iterator[torch.Tensor], torch.Tensor]:
|
||||
@@ -210,7 +212,6 @@ class RetakePipeline:
|
||||
prompts_to_encode,
|
||||
enhance_first_prompt=enhance_prompt,
|
||||
enhance_prompt_seed=seed,
|
||||
streaming_prefetch_count=streaming_prefetch_count,
|
||||
)
|
||||
|
||||
v_context_p, a_context_p = contexts[0].video_encoding, contexts[0].audio_encoding
|
||||
@@ -269,7 +270,6 @@ class RetakePipeline:
|
||||
fps=output_shape.fps,
|
||||
video=video_modality_spec,
|
||||
audio=audio_modality_spec,
|
||||
streaming_prefetch_count=streaming_prefetch_count,
|
||||
max_batch_size=max_batch_size,
|
||||
)
|
||||
|
||||
@@ -309,6 +309,7 @@ def main() -> None:
|
||||
quantization=args.quantization,
|
||||
distilled=args.distilled,
|
||||
torch_compile=args.compile,
|
||||
offload_mode=args.offload_mode,
|
||||
)
|
||||
params = detect_params(args.distilled_checkpoint_path)
|
||||
tiling_config = TilingConfig.default()
|
||||
@@ -321,7 +322,6 @@ def main() -> None:
|
||||
video_guider_params=params.video_guider_params,
|
||||
audio_guider_params=params.audio_guider_params,
|
||||
tiling_config=tiling_config,
|
||||
streaming_prefetch_count=args.streaming_prefetch_count,
|
||||
max_batch_size=args.max_batch_size,
|
||||
)
|
||||
video_chunks_number = get_video_chunks_number(src.frames, tiling_config)
|
||||
|
||||
@@ -20,7 +20,11 @@ from ltx_pipelines.utils import (
|
||||
combined_image_conditionings,
|
||||
get_device,
|
||||
)
|
||||
from ltx_pipelines.utils.args import ImageConditioningInput, default_1_stage_arg_parser, detect_checkpoint_path
|
||||
from ltx_pipelines.utils.args import (
|
||||
ImageConditioningInput,
|
||||
default_1_stage_arg_parser,
|
||||
detect_checkpoint_path,
|
||||
)
|
||||
from ltx_pipelines.utils.blocks import (
|
||||
AudioDecoder,
|
||||
DiffusionStage,
|
||||
@@ -31,7 +35,7 @@ from ltx_pipelines.utils.blocks import (
|
||||
from ltx_pipelines.utils.constants import detect_params
|
||||
from ltx_pipelines.utils.denoisers import FactoryGuidedDenoiser
|
||||
from ltx_pipelines.utils.media_io import encode_video
|
||||
from ltx_pipelines.utils.types import ModalitySpec
|
||||
from ltx_pipelines.utils.types import ModalitySpec, OffloadMode
|
||||
|
||||
|
||||
class TI2VidOneStagePipeline:
|
||||
@@ -52,6 +56,7 @@ class TI2VidOneStagePipeline:
|
||||
quantization: QuantizationPolicy | None = None,
|
||||
registry: Registry | None = None,
|
||||
torch_compile: bool = False,
|
||||
offload_mode: OffloadMode = OffloadMode.NONE,
|
||||
):
|
||||
self.dtype = torch.bfloat16
|
||||
self.device = device or get_device()
|
||||
@@ -62,6 +67,7 @@ class TI2VidOneStagePipeline:
|
||||
dtype=self.dtype,
|
||||
device=self.device,
|
||||
registry=registry,
|
||||
offload_mode=offload_mode,
|
||||
)
|
||||
self.image_conditioner = ImageConditioner(
|
||||
checkpoint_path=checkpoint_path,
|
||||
@@ -77,6 +83,7 @@ class TI2VidOneStagePipeline:
|
||||
quantization=quantization,
|
||||
registry=registry,
|
||||
torch_compile=torch_compile,
|
||||
offload_mode=offload_mode,
|
||||
)
|
||||
self.video_decoder = VideoDecoder(
|
||||
checkpoint_path=checkpoint_path,
|
||||
@@ -105,7 +112,6 @@ class TI2VidOneStagePipeline:
|
||||
audio_guider_params: MultiModalGuiderParams | MultiModalGuiderFactory,
|
||||
images: list[ImageConditioningInput],
|
||||
enhance_prompt: bool = False,
|
||||
streaming_prefetch_count: int | None = None,
|
||||
tiling_config: TilingConfig | None = None,
|
||||
max_batch_size: int = 1,
|
||||
sigmas: torch.Tensor | None = None,
|
||||
@@ -121,7 +127,6 @@ class TI2VidOneStagePipeline:
|
||||
enhance_first_prompt=enhance_prompt,
|
||||
enhance_prompt_image=images[0][0] if len(images) > 0 else None,
|
||||
enhance_prompt_seed=seed,
|
||||
streaming_prefetch_count=streaming_prefetch_count,
|
||||
)
|
||||
v_context_p, a_context_p = ctx_p.video_encoding, ctx_p.audio_encoding
|
||||
v_context_n, a_context_n = ctx_n.video_encoding, ctx_n.audio_encoding
|
||||
@@ -170,7 +175,6 @@ class TI2VidOneStagePipeline:
|
||||
audio=ModalitySpec(
|
||||
context=a_context_p,
|
||||
),
|
||||
streaming_prefetch_count=streaming_prefetch_count,
|
||||
max_batch_size=max_batch_size,
|
||||
)
|
||||
|
||||
@@ -192,6 +196,7 @@ def main() -> None:
|
||||
loras=tuple(args.lora) if args.lora else (),
|
||||
quantization=args.quantization,
|
||||
torch_compile=args.compile,
|
||||
offload_mode=args.offload_mode,
|
||||
)
|
||||
video, audio = pipeline(
|
||||
prompt=args.prompt,
|
||||
@@ -219,7 +224,6 @@ def main() -> None:
|
||||
stg_blocks=args.audio_stg_blocks,
|
||||
),
|
||||
images=args.images,
|
||||
streaming_prefetch_count=args.streaming_prefetch_count,
|
||||
max_batch_size=args.max_batch_size,
|
||||
)
|
||||
|
||||
|
||||
@@ -15,7 +15,11 @@ from ltx_core.loader.registry import Registry
|
||||
from ltx_core.model.video_vae import TilingConfig, get_video_chunks_number
|
||||
from ltx_core.quantization import QuantizationPolicy
|
||||
from ltx_core.types import Audio, VideoPixelShape
|
||||
from ltx_pipelines.utils.args import ImageConditioningInput, default_2_stage_arg_parser, detect_checkpoint_path
|
||||
from ltx_pipelines.utils.args import (
|
||||
ImageConditioningInput,
|
||||
default_2_stage_arg_parser,
|
||||
detect_checkpoint_path,
|
||||
)
|
||||
from ltx_pipelines.utils.blocks import (
|
||||
AudioDecoder,
|
||||
DiffusionStage,
|
||||
@@ -35,7 +39,7 @@ from ltx_pipelines.utils.helpers import (
|
||||
get_device,
|
||||
)
|
||||
from ltx_pipelines.utils.media_io import encode_video
|
||||
from ltx_pipelines.utils.types import ModalitySpec
|
||||
from ltx_pipelines.utils.types import ModalitySpec, OffloadMode
|
||||
|
||||
|
||||
class TI2VidTwoStagesPipeline:
|
||||
@@ -58,12 +62,15 @@ class TI2VidTwoStagesPipeline:
|
||||
quantization: QuantizationPolicy | None = None,
|
||||
registry: Registry | None = None,
|
||||
torch_compile: bool = False,
|
||||
offload_mode: OffloadMode = OffloadMode.NONE,
|
||||
):
|
||||
self.device = device or get_device()
|
||||
self.dtype = torch.bfloat16
|
||||
self._scheduler = LTX2Scheduler()
|
||||
|
||||
self.prompt_encoder = PromptEncoder(checkpoint_path, gemma_root, self.dtype, self.device, registry=registry)
|
||||
self.prompt_encoder = PromptEncoder(
|
||||
checkpoint_path, gemma_root, self.dtype, self.device, registry=registry, offload_mode=offload_mode
|
||||
)
|
||||
self.image_conditioner = ImageConditioner(checkpoint_path, self.dtype, self.device, registry=registry)
|
||||
self.upsampler = VideoUpsampler(
|
||||
checkpoint_path, spatial_upsampler_path, self.dtype, self.device, registry=registry
|
||||
@@ -79,6 +86,7 @@ class TI2VidTwoStagesPipeline:
|
||||
quantization=quantization,
|
||||
registry=registry,
|
||||
torch_compile=torch_compile,
|
||||
offload_mode=offload_mode,
|
||||
)
|
||||
self.stage_2 = DiffusionStage(
|
||||
checkpoint_path,
|
||||
@@ -88,6 +96,7 @@ class TI2VidTwoStagesPipeline:
|
||||
quantization=quantization,
|
||||
registry=registry,
|
||||
torch_compile=torch_compile,
|
||||
offload_mode=offload_mode,
|
||||
)
|
||||
|
||||
def __call__( # noqa: PLR0913
|
||||
@@ -105,7 +114,6 @@ class TI2VidTwoStagesPipeline:
|
||||
images: list[ImageConditioningInput],
|
||||
tiling_config: TilingConfig | None = None,
|
||||
enhance_prompt: bool = False,
|
||||
streaming_prefetch_count: int | None = None,
|
||||
max_batch_size: int = 1,
|
||||
stage_1_sigmas: torch.Tensor | None = None,
|
||||
stage_2_sigmas: torch.Tensor = STAGE_2_DISTILLED_SIGMAS,
|
||||
@@ -121,7 +129,6 @@ class TI2VidTwoStagesPipeline:
|
||||
enhance_first_prompt=enhance_prompt,
|
||||
enhance_prompt_image=images[0][0] if len(images) > 0 else None,
|
||||
enhance_prompt_seed=seed,
|
||||
streaming_prefetch_count=streaming_prefetch_count,
|
||||
)
|
||||
v_context_p, a_context_p = ctx_p.video_encoding, ctx_p.audio_encoding
|
||||
v_context_n, a_context_n = ctx_n.video_encoding, ctx_n.audio_encoding
|
||||
@@ -170,7 +177,6 @@ class TI2VidTwoStagesPipeline:
|
||||
fps=frame_rate,
|
||||
video=ModalitySpec(context=v_context_p, conditionings=stage_1_conditionings),
|
||||
audio=ModalitySpec(context=a_context_p),
|
||||
streaming_prefetch_count=streaming_prefetch_count,
|
||||
max_batch_size=max_batch_size,
|
||||
)
|
||||
|
||||
@@ -208,7 +214,6 @@ class TI2VidTwoStagesPipeline:
|
||||
noise_scale=stage_2_sigmas[0].item(),
|
||||
initial_latent=audio_state.latent,
|
||||
),
|
||||
streaming_prefetch_count=streaming_prefetch_count,
|
||||
)
|
||||
|
||||
decoded_video = self.video_decoder(video_state.latent, tiling_config, generator)
|
||||
@@ -231,6 +236,7 @@ def main() -> None:
|
||||
loras=tuple(args.lora) if args.lora else (),
|
||||
quantization=args.quantization,
|
||||
torch_compile=args.compile,
|
||||
offload_mode=args.offload_mode,
|
||||
)
|
||||
tiling_config = TilingConfig.default()
|
||||
video_chunks_number = get_video_chunks_number(args.num_frames, tiling_config)
|
||||
@@ -261,7 +267,6 @@ def main() -> None:
|
||||
),
|
||||
images=args.images,
|
||||
tiling_config=tiling_config,
|
||||
streaming_prefetch_count=args.streaming_prefetch_count,
|
||||
max_batch_size=args.max_batch_size,
|
||||
)
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ from ltx_pipelines.utils.helpers import (
|
||||
)
|
||||
from ltx_pipelines.utils.media_io import encode_video
|
||||
from ltx_pipelines.utils.samplers import res2s_audio_video_denoising_loop
|
||||
from ltx_pipelines.utils.types import ModalitySpec
|
||||
from ltx_pipelines.utils.types import ModalitySpec, OffloadMode
|
||||
|
||||
|
||||
class TI2VidTwoStagesHQPipeline:
|
||||
@@ -61,6 +61,7 @@ class TI2VidTwoStagesHQPipeline:
|
||||
quantization: QuantizationPolicy | None = None,
|
||||
registry: Registry | None = None,
|
||||
torch_compile: bool = False,
|
||||
offload_mode: OffloadMode = OffloadMode.NONE,
|
||||
):
|
||||
self.device = device or get_device()
|
||||
self.dtype = torch.bfloat16
|
||||
@@ -77,7 +78,9 @@ class TI2VidTwoStagesHQPipeline:
|
||||
sd_ops=distilled_lora[0].sd_ops,
|
||||
)
|
||||
|
||||
self.prompt_encoder = PromptEncoder(checkpoint_path, gemma_root, self.dtype, self.device, registry=registry)
|
||||
self.prompt_encoder = PromptEncoder(
|
||||
checkpoint_path, gemma_root, self.dtype, self.device, registry=registry, offload_mode=offload_mode
|
||||
)
|
||||
self.image_conditioner = ImageConditioner(checkpoint_path, self.dtype, self.device, registry=registry)
|
||||
self.upsampler = VideoUpsampler(
|
||||
checkpoint_path, spatial_upsampler_path, self.dtype, self.device, registry=registry
|
||||
@@ -93,6 +96,7 @@ class TI2VidTwoStagesHQPipeline:
|
||||
quantization=quantization,
|
||||
registry=registry,
|
||||
torch_compile=torch_compile,
|
||||
offload_mode=offload_mode,
|
||||
)
|
||||
self.stage_2 = DiffusionStage(
|
||||
checkpoint_path,
|
||||
@@ -102,6 +106,7 @@ class TI2VidTwoStagesHQPipeline:
|
||||
quantization=quantization,
|
||||
registry=registry,
|
||||
torch_compile=torch_compile,
|
||||
offload_mode=offload_mode,
|
||||
)
|
||||
|
||||
@torch.inference_mode()
|
||||
@@ -120,7 +125,6 @@ class TI2VidTwoStagesHQPipeline:
|
||||
images: list[ImageConditioningInput],
|
||||
tiling_config: TilingConfig | None = None,
|
||||
enhance_prompt: bool = False,
|
||||
streaming_prefetch_count: int | None = None,
|
||||
max_batch_size: int = 1,
|
||||
stage_1_sigmas: torch.Tensor | None = None,
|
||||
stage_2_sigmas: torch.Tensor = STAGE_2_DISTILLED_SIGMAS,
|
||||
@@ -136,7 +140,6 @@ class TI2VidTwoStagesHQPipeline:
|
||||
enhance_first_prompt=enhance_prompt,
|
||||
enhance_prompt_image=images[0][0] if len(images) > 0 else None,
|
||||
enhance_prompt_seed=seed,
|
||||
streaming_prefetch_count=streaming_prefetch_count,
|
||||
)
|
||||
v_context_p, a_context_p = ctx_p.video_encoding, ctx_p.audio_encoding
|
||||
v_context_n, a_context_n = ctx_n.video_encoding, ctx_n.audio_encoding
|
||||
@@ -190,7 +193,6 @@ class TI2VidTwoStagesHQPipeline:
|
||||
video=ModalitySpec(context=v_context_p, conditionings=stage_1_conditionings),
|
||||
audio=ModalitySpec(context=a_context_p),
|
||||
loop=res2s_audio_video_denoising_loop,
|
||||
streaming_prefetch_count=streaming_prefetch_count,
|
||||
max_batch_size=max_batch_size,
|
||||
)
|
||||
|
||||
@@ -231,7 +233,6 @@ class TI2VidTwoStagesHQPipeline:
|
||||
initial_latent=audio_state.latent,
|
||||
),
|
||||
loop=res2s_audio_video_denoising_loop,
|
||||
streaming_prefetch_count=streaming_prefetch_count,
|
||||
)
|
||||
|
||||
decoded_video = self.video_decoder(video_state.latent, tiling_config, generator)
|
||||
@@ -254,6 +255,7 @@ def main() -> None:
|
||||
loras=tuple(args.lora) if args.lora else (),
|
||||
quantization=args.quantization,
|
||||
torch_compile=args.compile,
|
||||
offload_mode=args.offload_mode,
|
||||
)
|
||||
tiling_config = TilingConfig.default()
|
||||
video_chunks_number = get_video_chunks_number(args.num_frames, tiling_config)
|
||||
@@ -284,7 +286,6 @@ def main() -> None:
|
||||
),
|
||||
images=args.images,
|
||||
tiling_config=tiling_config,
|
||||
streaming_prefetch_count=args.streaming_prefetch_count,
|
||||
max_batch_size=args.max_batch_size,
|
||||
)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from ltx_pipelines.utils.constants import (
|
||||
LTX_2_3_PARAMS,
|
||||
PipelineParams,
|
||||
)
|
||||
from ltx_pipelines.utils.types import OffloadMode
|
||||
|
||||
|
||||
class ImageConditioningInput(NamedTuple):
|
||||
@@ -231,16 +232,19 @@ def basic_arg_parser(
|
||||
except ValueError as e:
|
||||
raise argparse.ArgumentTypeError(f"must be an integer, got {value}") from e
|
||||
|
||||
# Layer streaming
|
||||
# Weight offloading
|
||||
parser.add_argument(
|
||||
"--streaming-prefetch-count",
|
||||
type=_positive_int,
|
||||
default=None,
|
||||
metavar="N",
|
||||
"--offload",
|
||||
dest="offload_mode",
|
||||
type=OffloadMode,
|
||||
default=OffloadMode.NONE,
|
||||
choices=list(OffloadMode),
|
||||
help=(
|
||||
"Enable layer streaming prefetching N layers ahead. "
|
||||
"At most 1 + N layers reside on GPU at once. "
|
||||
"Must be >= 1. Example: --streaming-prefetch-count 2"
|
||||
"Weight offloading strategy. "
|
||||
"'none' keeps all weights on GPU (default). "
|
||||
"'cpu' pins weights in CPU RAM, streams to GPU per layer. "
|
||||
"'disk' reads weights from disk on demand (lowest memory). "
|
||||
"Example: --offload cpu"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -15,11 +15,11 @@ from typing import Callable, TypeVar
|
||||
import torch
|
||||
|
||||
from ltx_core.batch_split import BatchSplitAdapter
|
||||
from ltx_core.block_streaming import DISK_CPU_SLOTS, StreamingModelBuilder
|
||||
from ltx_core.components.diffusion_steps import EulerDiffusionStep
|
||||
from ltx_core.components.noisers import Noiser
|
||||
from ltx_core.components.patchifiers import AudioPatchifier, VideoLatentPatchifier
|
||||
from ltx_core.components.protocols import DiffusionStepProtocol
|
||||
from ltx_core.layer_streaming import LayerStreamingWrapper
|
||||
from ltx_core.loader import SDOps
|
||||
from ltx_core.loader.primitives import LoraPathStrengthAndSDOps
|
||||
from ltx_core.loader.registry import DummyRegistry, Registry
|
||||
@@ -70,7 +70,7 @@ from ltx_pipelines.utils.helpers import (
|
||||
generate_enhanced_prompt,
|
||||
)
|
||||
from ltx_pipelines.utils.samplers import euler_denoising_loop
|
||||
from ltx_pipelines.utils.types import Denoiser, ModalitySpec
|
||||
from ltx_pipelines.utils.types import Denoiser, ModalitySpec, OffloadMode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -85,34 +85,24 @@ _M = TypeVar("_M", bound=torch.nn.Module)
|
||||
|
||||
@contextmanager
|
||||
def _streaming_model(
|
||||
model: _M,
|
||||
layers_attr: str,
|
||||
builder: StreamingModelBuilder,
|
||||
offload_mode: OffloadMode,
|
||||
target_device: torch.device,
|
||||
prefetch_count: int,
|
||||
) -> Iterator[_M]:
|
||||
"""Wrap *model* with :class:`LayerStreamingWrapper`, yield it, then tear down."""
|
||||
wrapped = LayerStreamingWrapper(
|
||||
model,
|
||||
layers_attr=layers_attr,
|
||||
dtype: torch.dtype,
|
||||
) -> Iterator:
|
||||
"""Build a streaming wrapper, yield it, then tear down and free memory."""
|
||||
cpu_slots_count = DISK_CPU_SLOTS if offload_mode == OffloadMode.DISK else None
|
||||
wrapped = builder.build(
|
||||
target_device=target_device,
|
||||
prefetch_count=prefetch_count,
|
||||
dtype=dtype,
|
||||
cpu_slots_count=cpu_slots_count,
|
||||
)
|
||||
try:
|
||||
yield wrapped # type: ignore[misc]
|
||||
yield wrapped
|
||||
finally:
|
||||
wrapped.teardown()
|
||||
wrapped.to("meta")
|
||||
cleanup_memory()
|
||||
# Flush the host (pinned) memory cache so that freed pinned pages are
|
||||
# returned to the OS. Without this, sequential streaming models
|
||||
# (e.g. text encoder then transformer) exhaust host memory because the
|
||||
# CachingHostAllocator keeps freed blocks cached indefinitely.
|
||||
torch.cuda.synchronize(device=target_device)
|
||||
try:
|
||||
if hasattr(torch._C, "_host_emptyCache"):
|
||||
torch._C._host_emptyCache()
|
||||
except Exception:
|
||||
logger.warning("Host empty cache cleanup failed; ignoring.", exc_info=True)
|
||||
|
||||
|
||||
def _build_state(
|
||||
@@ -163,11 +153,30 @@ class DiffusionStage:
|
||||
quantization: QuantizationPolicy | None = None,
|
||||
registry: Registry | None = None,
|
||||
torch_compile: bool = False,
|
||||
offload_mode: OffloadMode = OffloadMode.NONE,
|
||||
) -> None:
|
||||
if offload_mode != OffloadMode.NONE:
|
||||
if torch_compile:
|
||||
raise ValueError("torch.compile is not supported with layer streaming")
|
||||
if quantization is not None:
|
||||
raise ValueError("quantization is not supported with layer streaming")
|
||||
self._streaming_builder = StreamingModelBuilder(
|
||||
model_class_configurator=LTXModelConfigurator,
|
||||
model_path=checkpoint_path,
|
||||
model_sd_ops=LTXV_MODEL_COMFY_RENAMING_MAP,
|
||||
loras=tuple(loras),
|
||||
registry=registry or DummyRegistry(),
|
||||
blocks_attr="velocity_model.transformer_blocks",
|
||||
blocks_prefix="transformer_blocks",
|
||||
state_dict_prefix="velocity_model.",
|
||||
model_wrapper=lambda m: X0Model(m).eval(),
|
||||
)
|
||||
|
||||
self._dtype = dtype
|
||||
self._device = device
|
||||
self._quantization = quantization
|
||||
self._torch_compile = torch_compile
|
||||
self._offload_mode = offload_mode
|
||||
self._transformer_builder = Builder(
|
||||
model_path=checkpoint_path,
|
||||
model_class_configurator=LTXModelConfigurator,
|
||||
@@ -205,22 +214,21 @@ class DiffusionStage:
|
||||
builder = self._transformer_builder.with_module_ops(module_ops).with_sd_ops(sd_ops).with_loras(loras)
|
||||
return X0Model(builder.build(device=target, **kwargs)).to(target).eval()
|
||||
|
||||
def _transformer_ctx(
|
||||
self,
|
||||
streaming_prefetch_count: int | None,
|
||||
**kwargs: object,
|
||||
) -> AbstractContextManager:
|
||||
if streaming_prefetch_count is not None:
|
||||
return _streaming_model(
|
||||
self._build_transformer(device=torch.device("cpu"), **kwargs),
|
||||
layers_attr="velocity_model.transformer_blocks",
|
||||
target_device=self._device,
|
||||
prefetch_count=streaming_prefetch_count,
|
||||
)
|
||||
def _transformer_ctx(self, **kwargs: object) -> AbstractContextManager:
|
||||
if self._offload_mode != OffloadMode.NONE:
|
||||
return _streaming_model(self._streaming_builder, self._offload_mode, self._device, self._dtype)
|
||||
return gpu_model(self._build_transformer(**kwargs))
|
||||
|
||||
def __call__( # noqa: PLR0913
|
||||
def model_context(self, **kwargs: object) -> AbstractContextManager:
|
||||
"""Build the transformer, yield it, then free its memory on exit.
|
||||
Keyword arguments are forwarded to the underlying builder (e.g.
|
||||
``video_tools`` required by ``TiledDataParallelBuilder``).
|
||||
"""
|
||||
return self._transformer_ctx(**kwargs)
|
||||
|
||||
def run( # noqa: PLR0913
|
||||
self,
|
||||
transformer: object,
|
||||
denoiser: Denoiser,
|
||||
sigmas: torch.Tensor,
|
||||
noiser: Noiser,
|
||||
@@ -232,27 +240,14 @@ class DiffusionStage:
|
||||
audio: ModalitySpec | None = None,
|
||||
stepper: DiffusionStepProtocol | None = None,
|
||||
loop: Callable[..., tuple[LatentState | None, LatentState | None]] | None = None,
|
||||
streaming_prefetch_count: int | None = None,
|
||||
max_batch_size: int = 1,
|
||||
) -> tuple[LatentState | None, LatentState | None]:
|
||||
"""Build transformer → run denoising loop → free transformer.
|
||||
Args:
|
||||
width: Output width in pixels.
|
||||
height: Output height in pixels.
|
||||
frames: Number of output frames.
|
||||
fps: Frame rate.
|
||||
loop: Denoising loop function. Must accept
|
||||
``(sigmas, video_state, audio_state, stepper, transformer, denoiser)``
|
||||
as the first six positional arguments. When ``None``, resolves to
|
||||
:func:`euler_denoising_loop` at call time.
|
||||
streaming_prefetch_count: When set, build the transformer on CPU and
|
||||
wrap with :class:`LayerStreamingWrapper` for memory-efficient
|
||||
inference, prefetching this many layers ahead.
|
||||
max_batch_size: Maximum batch size per transformer forward pass.
|
||||
Guided denoisers make up to 4 transformer calls per step.
|
||||
When set to a value > 1, the transformer batches multiple
|
||||
calls together, reducing layer-streaming PCIe transfers.
|
||||
Default ``1`` preserves sequential behavior.
|
||||
"""Run denoising with a pre-built transformer.
|
||||
Same semantics as ``__call__`` but accepts a pre-built transformer so
|
||||
the model can be shared across multiple calls (e.g. tiled inference
|
||||
inside a single ``model_context()`` block). Audio supports
|
||||
``ModalitySpec(frozen=True)`` to keep the latent unchanged throughout
|
||||
denoising while still providing cross-modal context to the transformer.
|
||||
Returns ``(video_state | None, audio_state | None)`` with cleared
|
||||
conditionings and unpatchified latents for present modalities.
|
||||
"""
|
||||
@@ -261,7 +256,6 @@ class DiffusionStage:
|
||||
|
||||
if loop is None:
|
||||
loop = euler_denoising_loop
|
||||
|
||||
if stepper is None:
|
||||
stepper = EulerDiffusionStep()
|
||||
|
||||
@@ -281,28 +275,70 @@ class DiffusionStage:
|
||||
audio_tools = AudioLatentTools(AudioPatchifier(patch_size=1), a_shape)
|
||||
audio_state = _build_state(audio, audio_tools, noiser, self._dtype, self._device)
|
||||
|
||||
with self._transformer_ctx(streaming_prefetch_count, video_tools=video_tools) as base_transformer:
|
||||
transformer = BatchSplitAdapter(base_transformer, max_batch_size=max_batch_size)
|
||||
video_state, audio_state = loop(
|
||||
sigmas=sigmas,
|
||||
video_state=video_state,
|
||||
audio_state=audio_state,
|
||||
stepper=stepper,
|
||||
transformer=transformer,
|
||||
denoiser=denoiser,
|
||||
)
|
||||
wrapped = BatchSplitAdapter(transformer, max_batch_size=max_batch_size) # type: ignore[arg-type]
|
||||
video_state, audio_state = loop(
|
||||
sigmas=sigmas,
|
||||
video_state=video_state,
|
||||
audio_state=audio_state,
|
||||
stepper=stepper,
|
||||
transformer=wrapped,
|
||||
denoiser=denoiser,
|
||||
)
|
||||
|
||||
# Post-process: clear conditionings and unpatchify
|
||||
if video_state is not None and video_tools is not None:
|
||||
video_state = video_tools.clear_conditioning(video_state)
|
||||
video_state = video_tools.unpatchify(video_state)
|
||||
|
||||
if audio_state is not None and audio_tools is not None:
|
||||
audio_state = audio_tools.clear_conditioning(audio_state)
|
||||
audio_state = audio_tools.unpatchify(audio_state)
|
||||
|
||||
return video_state, audio_state
|
||||
|
||||
def __call__( # noqa: PLR0913
|
||||
self,
|
||||
denoiser: Denoiser,
|
||||
sigmas: torch.Tensor,
|
||||
noiser: Noiser,
|
||||
width: int,
|
||||
height: int,
|
||||
frames: int,
|
||||
fps: float,
|
||||
video: ModalitySpec | None = None,
|
||||
audio: ModalitySpec | None = None,
|
||||
stepper: DiffusionStepProtocol | None = None,
|
||||
loop: Callable[..., tuple[LatentState | None, LatentState | None]] | None = None,
|
||||
max_batch_size: int = 1,
|
||||
) -> tuple[LatentState | None, LatentState | None]:
|
||||
"""Build transformer -> run denoising loop -> free transformer.
|
||||
Returns ``(video_state | None, audio_state | None)`` with cleared
|
||||
conditionings and unpatchified latents for present modalities.
|
||||
"""
|
||||
# Build video_tools up front so it can be forwarded to the transformer
|
||||
# context (required by TiledDataParallelBuilder in multi-GPU mode).
|
||||
# `run()` rebuilds its own tools internally; the duplication is cheap.
|
||||
video_tools: LatentTools | None = None
|
||||
if video is not None:
|
||||
pixel_shape = VideoPixelShape(batch=1, frames=frames, height=height, width=width, fps=fps)
|
||||
v_shape = VideoLatentShape.from_pixel_shape(pixel_shape)
|
||||
video_tools = VideoLatentTools(VideoLatentPatchifier(patch_size=1), v_shape, fps)
|
||||
|
||||
with self._transformer_ctx(video_tools=video_tools) as transformer:
|
||||
return self.run(
|
||||
transformer,
|
||||
denoiser,
|
||||
sigmas,
|
||||
noiser,
|
||||
width,
|
||||
height,
|
||||
frames,
|
||||
fps,
|
||||
video,
|
||||
audio,
|
||||
stepper,
|
||||
loop,
|
||||
max_batch_size,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PromptEncoder
|
||||
@@ -322,9 +358,11 @@ class PromptEncoder:
|
||||
dtype: torch.dtype,
|
||||
device: torch.device,
|
||||
registry: Registry | None = None,
|
||||
offload_mode: OffloadMode = OffloadMode.NONE,
|
||||
) -> None:
|
||||
self._dtype = dtype
|
||||
self._device = device
|
||||
self._offload_mode = offload_mode
|
||||
|
||||
module_ops = module_ops_from_gemma_root(gemma_root)
|
||||
model_folder = find_matching_file(gemma_root, "model*.safetensors").parent
|
||||
@@ -337,6 +375,15 @@ class PromptEncoder:
|
||||
module_ops=(GEMMA_MODEL_OPS, *module_ops),
|
||||
registry=registry or DummyRegistry(),
|
||||
)
|
||||
self._streaming_text_encoder_builder = StreamingModelBuilder(
|
||||
model_path=tuple(weight_paths),
|
||||
model_class_configurator=GemmaTextEncoderConfigurator,
|
||||
model_sd_ops=GEMMA_LLM_KEY_OPS,
|
||||
module_ops=(GEMMA_MODEL_OPS, *module_ops),
|
||||
registry=registry or DummyRegistry(),
|
||||
blocks_attr="model.model.language_model.layers",
|
||||
blocks_prefix="model.model.language_model.layers",
|
||||
)
|
||||
self._embeddings_processor_builder = Builder(
|
||||
model_path=checkpoint_path,
|
||||
model_class_configurator=EmbeddingsProcessorConfigurator,
|
||||
@@ -344,17 +391,9 @@ class PromptEncoder:
|
||||
registry=registry or DummyRegistry(),
|
||||
)
|
||||
|
||||
def _text_encoder_ctx(
|
||||
self,
|
||||
streaming_prefetch_count: int | None,
|
||||
) -> AbstractContextManager:
|
||||
if streaming_prefetch_count is not None:
|
||||
return _streaming_model(
|
||||
self._text_encoder_builder.build(device=torch.device("cpu"), dtype=self._dtype).eval(),
|
||||
layers_attr="model.model.language_model.layers",
|
||||
target_device=self._device,
|
||||
prefetch_count=streaming_prefetch_count,
|
||||
)
|
||||
def _text_encoder_ctx(self) -> AbstractContextManager:
|
||||
if self._offload_mode != OffloadMode.NONE:
|
||||
return _streaming_model(self._streaming_text_encoder_builder, self._offload_mode, self._device, self._dtype)
|
||||
return gpu_model(self._text_encoder_builder.build(device=self._device, dtype=self._dtype).eval())
|
||||
|
||||
def __call__(
|
||||
@@ -364,10 +403,9 @@ class PromptEncoder:
|
||||
enhance_first_prompt: bool = False,
|
||||
enhance_prompt_image: str | None = None,
|
||||
enhance_prompt_seed: int = 42,
|
||||
streaming_prefetch_count: int | None = None,
|
||||
) -> list[EmbeddingsProcessorOutput]:
|
||||
"""Encode *prompts* through Gemma → embeddings processor, freeing each model after use."""
|
||||
with self._text_encoder_ctx(streaming_prefetch_count) as text_encoder:
|
||||
"""Encode *prompts* through Gemma -> embeddings processor, freeing each model after use."""
|
||||
with self._text_encoder_ctx() as text_encoder:
|
||||
if enhance_first_prompt:
|
||||
prompts = list(prompts)
|
||||
prompts[0] = generate_enhanced_prompt(
|
||||
@@ -490,10 +528,17 @@ class VideoDecoder:
|
||||
latent: torch.Tensor,
|
||||
tiling_config: TilingConfig | None = None,
|
||||
generator: torch.Generator | None = None,
|
||||
*,
|
||||
output_dtype: torch.dtype = torch.uint8,
|
||||
) -> Iterator[torch.Tensor]:
|
||||
"""Decode *latent* to pixel-space video chunks. Decoder freed after exhaustion."""
|
||||
"""Decode *latent* to pixel-space video chunks. Decoder freed after exhaustion.
|
||||
Args:
|
||||
output_dtype: Target dtype for output tensors. ``torch.uint8``
|
||||
(default) maps to ``[0, 255]``. Any floating dtype returns
|
||||
``[0, 1]`` cast to that dtype.
|
||||
"""
|
||||
decoder = self._decoder_builder.build(device=self._device, dtype=self._dtype).to(self._device).eval()
|
||||
return _cleanup_iter(decoder.decode_video(latent, tiling_config, generator), decoder)
|
||||
return _cleanup_iter(decoder.decode_video(latent, tiling_config, generator, output_dtype=output_dtype), decoder)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -37,6 +37,11 @@ def cleanup_memory() -> None:
|
||||
gc.collect()
|
||||
torch.cuda.empty_cache()
|
||||
torch.cuda.synchronize()
|
||||
try:
|
||||
if hasattr(torch._C, "_host_emptyCache"):
|
||||
torch._C._host_emptyCache()
|
||||
except Exception:
|
||||
logging.warning("Host empty cache cleanup failed; ignoring.", exc_info=True)
|
||||
|
||||
|
||||
def _conform_latent_length(latent: torch.Tensor, expected_frames_count: int) -> torch.Tensor:
|
||||
|
||||
@@ -1,23 +1,34 @@
|
||||
import enum
|
||||
import logging
|
||||
import math
|
||||
from collections.abc import Generator, Iterator
|
||||
from fractions import Fraction
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
import av
|
||||
import numpy as np
|
||||
import OpenImageIO
|
||||
import torch
|
||||
from einops import rearrange
|
||||
from PIL import Image
|
||||
from torch._prims_common import DeviceLikeType
|
||||
from tqdm import tqdm
|
||||
|
||||
from ltx_core.hdr import LogC3
|
||||
from ltx_core.types import Audio, VideoPixelShape
|
||||
from ltx_pipelines.utils.constants import DEFAULT_IMAGE_CRF
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ResizeMode(enum.Enum):
|
||||
"""How to fit a conditioning video to the target resolution."""
|
||||
|
||||
CENTER_CROP = "center_crop"
|
||||
REFLECT_PAD = "reflect_pad"
|
||||
|
||||
|
||||
def resize_aspect_ratio_preserving(image: torch.Tensor, long_side: int) -> torch.Tensor:
|
||||
"""
|
||||
Resize image preserving aspect ratio (filling target long side).
|
||||
@@ -79,6 +90,16 @@ def normalize_latent(latent: torch.Tensor, device: torch.device, dtype: torch.dt
|
||||
return (latent / 127.5 - 1.0).to(device=device, dtype=dtype)
|
||||
|
||||
|
||||
def to_vae_range(x: torch.Tensor) -> torch.Tensor:
|
||||
"""Map [0, 1] to [-1, 1] (VAE input convention)."""
|
||||
return torch.clamp(x, 0.0, 1.0) * 2.0 - 1.0
|
||||
|
||||
|
||||
def from_vae_range(z: torch.Tensor) -> torch.Tensor:
|
||||
"""Map [-1, 1] (VAE output convention) to [0, 1]."""
|
||||
return torch.clamp((z + 1.0) / 2.0, 0.0, 1.0)
|
||||
|
||||
|
||||
def load_image_and_preprocess(
|
||||
image_path: str,
|
||||
height: int,
|
||||
@@ -124,6 +145,108 @@ def video_preprocess(
|
||||
return result
|
||||
|
||||
|
||||
def align_resolution(
|
||||
width: int,
|
||||
height: int,
|
||||
resize_mode: ResizeMode,
|
||||
divisor: int = 64,
|
||||
) -> tuple[int, int, int, int]:
|
||||
"""Compute aligned generation dimensions and crop-back size.
|
||||
Args:
|
||||
width: Source video width (need not be aligned).
|
||||
height: Source video height (need not be aligned).
|
||||
resize_mode: CENTER_CROP rounds down; REFLECT_PAD rounds up.
|
||||
divisor: Alignment divisor (default 64 for two-stage pipelines).
|
||||
Returns:
|
||||
``(gen_width, gen_height, crop_width, crop_height)`` where
|
||||
``gen_*`` are multiples of *divisor* and ``crop_*`` are the
|
||||
original dimensions to trim back to after decoding. When no
|
||||
cropping is needed ``crop_*`` equals ``gen_*``.
|
||||
"""
|
||||
if resize_mode is ResizeMode.REFLECT_PAD:
|
||||
gen_w = ((width + divisor - 1) // divisor) * divisor
|
||||
gen_h = ((height + divisor - 1) // divisor) * divisor
|
||||
else:
|
||||
gen_w = (width // divisor) * divisor
|
||||
gen_h = (height // divisor) * divisor
|
||||
|
||||
crop_w = width if gen_w != width else gen_w
|
||||
crop_h = height if gen_h != height else gen_h
|
||||
return gen_w, gen_h, crop_w, crop_h
|
||||
|
||||
|
||||
def resize_and_reflect_pad(tensor: torch.Tensor, height: int, width: int) -> torch.Tensor:
|
||||
"""Resize tensor to fit within target, then reflect-pad to exact dimensions.
|
||||
Unlike resize_and_center_crop which stretches and crops, this preserves the
|
||||
original aspect ratio and pads the shorter dimension with reflected pixels.
|
||||
When the target is already >= the source in both dimensions, interpolation
|
||||
is skipped entirely to preserve original pixels.
|
||||
Args:
|
||||
tensor: Input with shape (H, W, C) or (F, H, W, C)
|
||||
height: Target height
|
||||
width: Target width
|
||||
Returns:
|
||||
Tensor with shape (1, C, 1, height, width) for 3D or (1, C, F, height, width) for 4D
|
||||
"""
|
||||
if tensor.ndim == 3:
|
||||
tensor = rearrange(tensor, "h w c -> 1 c h w")
|
||||
elif tensor.ndim == 4:
|
||||
tensor = rearrange(tensor, "f h w c -> f c h w")
|
||||
else:
|
||||
raise ValueError(f"Expected input with 3 or 4 dimensions; got shape {tensor.shape}.")
|
||||
|
||||
_, _, src_h, src_w = tensor.shape
|
||||
|
||||
if height >= src_h and width >= src_w:
|
||||
new_h, new_w = src_h, src_w
|
||||
else:
|
||||
scale = min(height / src_h, width / src_w)
|
||||
new_h = round(src_h * scale)
|
||||
new_w = round(src_w * scale)
|
||||
tensor = torch.nn.functional.interpolate(tensor, size=(new_h, new_w), mode="bilinear", align_corners=False)
|
||||
|
||||
pad_bottom = height - new_h
|
||||
pad_right = width - new_w
|
||||
if pad_bottom > 0 or pad_right > 0:
|
||||
pad_mode = "reflect" if pad_bottom < new_h and pad_right < new_w else "replicate"
|
||||
tensor = torch.nn.functional.pad(tensor, (0, pad_right, 0, pad_bottom), mode=pad_mode)
|
||||
|
||||
tensor = rearrange(tensor, "f c h w -> 1 c f h w")
|
||||
return tensor
|
||||
|
||||
|
||||
def load_video_conditioning_hdr(
|
||||
video_path: str,
|
||||
height: int,
|
||||
width: int,
|
||||
frame_cap: int,
|
||||
dtype: torch.dtype,
|
||||
device: torch.device,
|
||||
hdr_transform: str = "logc3",
|
||||
resize_mode: ResizeMode = ResizeMode.CENTER_CROP,
|
||||
) -> Iterator[torch.Tensor]:
|
||||
"""Load a video and yield preprocessed frames for HDR IC-LoRA conditioning.
|
||||
Decodes through the standard path and applies the LDR compression that
|
||||
matches training. Callers are responsible for providing Rec.709 SDR
|
||||
input — the HDR IC-LoRA was trained on that color space.
|
||||
Args:
|
||||
hdr_transform: LDR-compression name (currently only ``logc3``).
|
||||
resize_mode: How to fit the video to the target resolution.
|
||||
Yields:
|
||||
Per-frame tensors of shape ``(1, C, 1, height, width)``.
|
||||
"""
|
||||
if hdr_transform != "logc3":
|
||||
raise ValueError(f"Unsupported HDR transform: {hdr_transform}")
|
||||
|
||||
resize_fn = resize_and_reflect_pad if resize_mode is ResizeMode.REFLECT_PAD else resize_and_center_crop
|
||||
|
||||
for f in decode_video_by_frame(path=video_path, frame_cap=frame_cap, device=device):
|
||||
frame = resize_fn(f.to(torch.float32), height, width)
|
||||
ldr = (frame / 255.0).clamp(0.0, 1.0)
|
||||
compressed = LogC3().compress_ldr(ldr)
|
||||
yield to_vae_range(compressed).to(device=device, dtype=dtype)
|
||||
|
||||
|
||||
def decode_image(image_path: str) -> np.ndarray:
|
||||
image = Image.open(image_path)
|
||||
np_array = np.array(image)[..., :3]
|
||||
@@ -481,3 +604,85 @@ def preprocess(image: np.array, crf: float = DEFAULT_IMAGE_CRF) -> np.array:
|
||||
with BytesIO(video_bytes) as video_file:
|
||||
image_array = decode_single_frame(video_file)
|
||||
return image_array
|
||||
|
||||
|
||||
def save_exr_tensor(tensor: torch.Tensor, file_path: str | Path, half: bool = False) -> None:
|
||||
"""Save a single tensor frame as EXR with linear sRGB colorspace metadata.
|
||||
Args:
|
||||
tensor: ``[H, W, C]`` or ``[C, H, W]`` float tensor.
|
||||
file_path: Output path (e.g. ``frame_0000.exr``).
|
||||
half: Force float16 output with ZIP compression.
|
||||
"""
|
||||
if tensor.dim() == 3 and tensor.shape[0] == 3:
|
||||
tensor = tensor.permute(1, 2, 0)
|
||||
use_half = half or tensor.dtype in (torch.float16, torch.half)
|
||||
img_np = np.ascontiguousarray(tensor.cpu().numpy().astype(np.float32))
|
||||
file_path = str(file_path)
|
||||
|
||||
h, w = img_np.shape[:2]
|
||||
fmt = OpenImageIO.HALF if use_half else OpenImageIO.FLOAT
|
||||
spec = OpenImageIO.ImageSpec(w, h, 3, fmt)
|
||||
spec.channelnames = ("R", "G", "B")
|
||||
spec.attribute("compression", "zip")
|
||||
spec.attribute("chromaticities", "float[8]", (0.64, 0.33, 0.30, 0.60, 0.15, 0.06, 0.3127, 0.3290))
|
||||
spec.attribute("colorSpace", "sRGB")
|
||||
|
||||
out = OpenImageIO.ImageOutput.create(file_path)
|
||||
if out is None:
|
||||
raise RuntimeError(
|
||||
f"Failed to create EXR writer for '{file_path}'. Ensure OpenImageIO is built with OpenEXR support."
|
||||
)
|
||||
try:
|
||||
if not out.open(file_path, spec):
|
||||
raise RuntimeError(f"Failed to open EXR file '{file_path}': {out.geterror()}")
|
||||
if not out.write_image(img_np):
|
||||
raise RuntimeError(f"Failed to write EXR image '{file_path}': {out.geterror()}")
|
||||
finally:
|
||||
out.close()
|
||||
|
||||
|
||||
def _linear_to_srgb(x: np.ndarray) -> np.ndarray:
|
||||
"""Linear -> sRGB OETF per IEC 61966-2-1. Input assumed in [0, 1]."""
|
||||
x = np.clip(x, 0.0, 1.0)
|
||||
return np.where(x <= 0.0031308, x * 12.92, 1.055 * np.power(x, 1.0 / 2.4) - 0.055)
|
||||
|
||||
|
||||
def encode_exr_sequence_to_mp4(exr_dir: Path, output_mp4: Path, frame_rate: float) -> None:
|
||||
"""Convert a linear EXR frame sequence to sRGB and encode to H.264 .mp4 via PyAV.
|
||||
Exposure is fixed at EV=0 (no gain). Each EXR frame is clamped to [0, 1],
|
||||
passed through the sRGB OETF, quantised to 8-bit BGR, and fed to a libx264
|
||||
stream (crf 18, yuv420p). ``frame_rate`` is the original source video's
|
||||
frame rate so playback matches the input timing.
|
||||
"""
|
||||
import os # noqa: PLC0415
|
||||
|
||||
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
|
||||
import cv2 # noqa: PLC0415
|
||||
|
||||
exr_files = sorted(exr_dir.glob("frame_*.exr"))
|
||||
if not exr_files:
|
||||
raise FileNotFoundError(f"No EXR frames found in {exr_dir}")
|
||||
|
||||
container = av.open(str(output_mp4), mode="w")
|
||||
stream = container.add_stream("libx264", rate=Fraction(frame_rate).limit_denominator(1000))
|
||||
stream.pix_fmt = "yuv420p"
|
||||
stream.options = {"crf": "18", "movflags": "+faststart"}
|
||||
|
||||
try:
|
||||
for i, exr_path in enumerate(exr_files):
|
||||
hdr = cv2.imread(str(exr_path), cv2.IMREAD_UNCHANGED).astype(np.float32)
|
||||
sdr = _linear_to_srgb(np.maximum(hdr, 0.0))
|
||||
bgr8 = (sdr * 255.0 + 0.5).astype(np.uint8)
|
||||
|
||||
if i == 0:
|
||||
stream.height = bgr8.shape[0]
|
||||
stream.width = bgr8.shape[1]
|
||||
|
||||
frame = av.VideoFrame.from_ndarray(bgr8, format="bgr24")
|
||||
for packet in stream.encode(frame):
|
||||
container.mux(packet)
|
||||
|
||||
for packet in stream.encode():
|
||||
container.mux(packet)
|
||||
finally:
|
||||
container.close()
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Protocol
|
||||
|
||||
import torch
|
||||
@@ -74,3 +77,21 @@ class ModalitySpec:
|
||||
noise_scale: float = 1.0
|
||||
frozen: bool = False
|
||||
initial_latent: torch.Tensor | None = None
|
||||
|
||||
|
||||
class OffloadMode(Enum):
|
||||
"""Weight offloading strategy.
|
||||
Controls where model weights reside during inference:
|
||||
- ``NONE``: All weights on GPU (no streaming). Fastest inference,
|
||||
requires enough VRAM for the full model (~28 GB for LTX-2).
|
||||
- ``CPU``: Weights pinned in CPU RAM, streamed layer-by-layer to a
|
||||
small GPU buffer. First pass reads from disk; subsequent passes
|
||||
reuse the CPU cache. Requires ~36 GB RAM + ~5 GB VRAM.
|
||||
- ``DISK``: Weights read from disk on demand through a small CPU
|
||||
buffer, then streamed to GPU. Every pass re-reads from disk.
|
||||
Lowest memory: ~5 GB RAM + ~5 GB VRAM.
|
||||
"""
|
||||
|
||||
NONE = "none"
|
||||
CPU = "cpu"
|
||||
DISK = "disk"
|
||||
|
||||
Reference in New Issue
Block a user