Automated PR - 2026-06-17

This commit is contained in:
github-actions[bot]
2026-06-17 14:06:32 +00:00
parent d6053703e0
commit 0d3d3a3855
90 changed files with 8265 additions and 4511 deletions
+1
View File
@@ -17,6 +17,7 @@ Inference pipelines for LTX-2 audio-video generation. Depends on `ltx-core` for
| Pipeline | File | Stages | Model | Sampler | Use case |
|----------|------|--------|-------|---------|----------|
| `TI2VidOneStagePipeline` | `ti2vid_one_stage.py` | 1 | Full | Euler | Simple text/image-to-video |
| `T2AOneStagePipeline` | `t2a_one_stage.py` | 1 | Full | Euler | Text-to-audio (audio-only output, no video branch) |
| `TI2VidTwoStagesPipeline` | `ti2vid_two_stages.py` | 2 | Full + distilled LoRA | Euler | Production quality |
| `TI2VidTwoStagesHQPipeline` | `ti2vid_two_stages_hq.py` | 2 | Full + distilled LoRA (both stages) | Res2s | Highest quality, fewer steps |
| `A2VidPipelineTwoStage` | `a2vid_two_stage.py` | 2 | Full + distilled LoRA | Euler | Audio-conditioned video |
+78
View File
@@ -58,6 +58,7 @@ Available pipeline modules:
- `ltx_pipelines.ti2vid_two_stages` - Two-stage text/image-to-video (recommended).
- `ltx_pipelines.ti2vid_two_stages_hq` - Two-stage text/image-to-video (different sampler, better quality).
- `ltx_pipelines.ti2vid_one_stage` - Single-stage text/image-to-video.
- `ltx_pipelines.t2a_one_stage` - Single-stage text-to-audio (audio-only output).
- `ltx_pipelines.distilled` - Fast text/image-to-video pipeline using only the distilled model.
- `ltx_pipelines.ic_lora` - Video-to-video with IC-LoRA.
- `ltx_pipelines.keyframe_interpolation` - Keyframe interpolation.
@@ -254,6 +255,20 @@ Uses IC-LoRA on a **distilled** checkpoint with a **single** lip-dub IC-LoRA app
---
### 11. T2AOneStagePipeline
**Best for:** Text-to-audio — generating speech/audio only (no video) from a text prompt, e.g. driving an audio-style LoRA such as an accent LoRA.
**Source**: [`src/ltx_pipelines/t2a_one_stage.py`](src/ltx_pipelines/t2a_one_stage.py)
Single-stage, **audio-only** generation: the video branch is absent (`video=None`), so only the audio modality is denoised and decoded through the audio VAE + vocoder, producing a wave file. Audio duration is derived from `--num-frames` / `--frame-rate` (the same `8k+1` frame convention as video). Audio guidance (CFG/STG) is optional — the `--audio-*` flags default to the model's values; the video→audio cross-modal guidance is disabled since there is no video modality.
**Extra CLI arguments (all optional, with sensible defaults):** `--num-frames`, `--frame-rate`, `--negative-prompt`, `--audio-cfg-guidance-scale`, `--audio-stg-guidance-scale`, `--audio-stg-blocks`, `--audio-rescale-scale`, `--audio-skip-step`. No `--height/--width/--image` (audio has no spatial dimensions).
**Use when:** You need speech/audio from text alone, or to evaluate an audio-only LoRA (accent, voice style) without generating video.
---
## 🎨 Conditioning Types
Pipelines use different conditioning methods from [`ltx-core`](../ltx-core/) for controlling generation. See the [ltx-core conditioning documentation](../ltx-core/README.md#conditioning--control) for details.
@@ -400,6 +415,69 @@ By default, pipelines clean GPU memory (especially transformer weights) between
# utils.cleanup_memory() # Comment out if you have enough VRAM
```
### Compilation (`torch.compile`)
Compiling the transformer blocks with `torch.compile` speeds up inference. It is **opt-in and off by default**. The blocks are compiled shape-polymorphically (the sequence dimension is marked dynamic), so one compiled artifact serves any token count without recompiling.
**CLI** — the `--compile` flag maps directly to `CompilationConfig`:
| Form | Result |
| ---- | ------ |
| *(flag absent)* | eager, no compilation |
| `--compile` | compile with defaults |
| `--compile KEY=VALUE ...` | compile, overriding individual fields |
```bash
# Defaults
python -m ltx_pipelines.ti2vid_two_stages --compile --checkpoint-path=...
# reduce-overhead captures CUDA graphs -- the main latency lever for the denoising loop.
# Off by default because graph capture reserves static memory pools (extra VRAM), so it
# trades memory for speed; enable it when you have headroom.
python -m ltx_pipelines.ti2vid_two_stages --compile mode=reduce-overhead --checkpoint-path=...
# Several overrides at once
python -m ltx_pipelines.ti2vid_two_stages \
--compile mode=max-autotune fullgraph=true dynamic=true --checkpoint-path=...
```
| Field | Values | Default | Notes |
| ----- | ------ | ------- | ----- |
| `mode` | `none`, `reduce-overhead`, `max-autotune`, … | `none` | `reduce-overhead`/`max-autotune` enable CUDA graphs |
| `backend` | `inductor`, `eager`, … | `inductor` | |
| `fullgraph` | `true`/`false` | `false` | |
| `dynamic` | `auto`/`true`/`false` | `auto` | the seq dim is marked dynamic regardless |
| `inductor_config` | JSON object or path to a `.json` | `{}` | `torch._inductor.config` overrides |
| `dynamo_config` | JSON object or path to a `.json` | `{"inline_inbuilt_nn_modules": true, "cache_size_limit": 256}` | `torch._dynamo.config` overrides |
**Controlling inductor / dynamo configs.** `inductor_config` and `dynamo_config` take either an inline JSON object or a path to a `.json` file, applied via `torch._inductor.config.patch(...)` / `torch._dynamo.config.patch(...)` around the compiled forward. They **replace the defaults wholesale — they do not merge**, so when overriding `dynamo_config` re-include any defaults you want to keep:
```bash
python -m ltx_pipelines.ti2vid_two_stages \
--compile 'inductor_config={"max_autotune": true}' \
'dynamo_config={"inline_inbuilt_nn_modules": true, "cache_size_limit": 256, "recompile_limit": 32}' \
--checkpoint-path=...
```
**Programmatically**, pass a `CompilationConfig` to the pipeline:
```python
from ltx_core.model.transformer.compiling import CompilationConfig
pipeline = TI2VidTwoStagesPipeline(
...,
compilation_config=CompilationConfig(mode="reduce-overhead"),
)
```
**Faster cache loads: `unsafe_skip_cache_dynamic_shape_guards` (unsafe, opt-in).** Inductor's FX-graph cache re-checks the dynamic-shape guards stored with each entry on every lookup. Setting this flag skips that re-check (every entry is treated as a guard hit), which speeds up warm and cross-process cache loads. It is **not enabled by default** because it is a correctness hazard: a kernel first compiled at a small sequence length keeps int32 address arithmetic, and reusing it at a larger sequence length (roughly **>58k tokens/rank**) overflows int32 and reads out of bounds — surfacing as a CUDA illegal memory access or silently corrupted output. Only enable it when your token counts stay within the range the cached kernels were compiled for:
```bash
python -m ltx_pipelines.ti2vid_two_stages \
--compile 'inductor_config={"unsafe_skip_cache_dynamic_shape_guards": true}' \
--checkpoint-path=...
```
### Denoising Loop Optimization
**Gradient Estimation Denoising Loop:**
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "ltx-pipelines"
version = "1.1.5"
version = "v1.1.6"
description = "Pipelines implementation for Lightricks' LTX-2 model"
readme = "README.md"
requires-python = ">=3.10"
@@ -2,6 +2,7 @@
LTX-2 Pipelines: High-level video generation pipelines and utilities.
This package provides ready-to-use pipelines for video generation:
- TI2VidOneStagePipeline: Text/image-to-video in a single stage
- T2AOneStagePipeline: Text-to-audio in a single stage (audio-only output)
- TI2VidTwoStagesPipeline: Two-stage generation with upsampling
- DistilledPipeline: Fast distilled two-stage generation
- ICLoraPipeline: Image/video conditioning with distilled LoRA
@@ -18,6 +19,7 @@ from ltx_pipelines.ic_lora import ICLoraPipeline
from ltx_pipelines.keyframe_interpolation import KeyframeInterpolationPipeline
from ltx_pipelines.lipdub import LipDubPipeline
from ltx_pipelines.retake import RetakePipeline
from ltx_pipelines.t2a_one_stage import T2AOneStagePipeline
from ltx_pipelines.ti2vid_one_stage import TI2VidOneStagePipeline
from ltx_pipelines.ti2vid_two_stages import TI2VidTwoStagesPipeline
@@ -28,6 +30,7 @@ __all__ = [
"KeyframeInterpolationPipeline",
"LipDubPipeline",
"RetakePipeline",
"T2AOneStagePipeline",
"TI2VidOneStagePipeline",
"TI2VidTwoStagesPipeline",
]
@@ -14,6 +14,7 @@ from ltx_core.types import Audio, VideoPixelShape
from ltx_pipelines.iclora_utils import (
append_ic_lora_reference_video_conditionings,
read_lora_reference_downscale_factor,
read_lora_reference_temporal_scale_factor,
)
from ltx_pipelines.utils.args import (
ImageConditioningInput,
@@ -102,10 +103,11 @@ class ICLoraPipeline:
self.video_decoder = VideoDecoder(distilled_checkpoint_path, self.dtype, self.device, registry=registry)
self.audio_decoder = AudioDecoder(distilled_checkpoint_path, self.dtype, self.device, registry=registry)
# Read reference downscale factor from LoRA metadata.
# IC-LoRAs trained with low-resolution reference videos store this factor
# so inference can resize reference videos to match training conditions.
# Read reference scale factors from LoRA metadata.
# IC-LoRAs trained with scaled reference videos store these factors
# so inference can resize/subsample reference videos to match training conditions.
self.reference_downscale_factor = 1
self.reference_temporal_scale_factor = 1
for lora in loras:
scale = read_lora_reference_downscale_factor(lora.path)
if scale != 1:
@@ -116,6 +118,15 @@ class ICLoraPipeline:
f"specifies {scale}. Cannot combine LoRAs with different reference scales."
)
self.reference_downscale_factor = scale
temporal = read_lora_reference_temporal_scale_factor(lora.path)
if temporal != 1:
if self.reference_temporal_scale_factor not in (1, temporal):
raise ValueError(
f"Conflicting reference_temporal_scale_factor values in LoRAs: "
f"already have {self.reference_temporal_scale_factor}, but {lora.path} "
f"specifies {temporal}. Cannot combine LoRAs with different temporal scales."
)
self.reference_temporal_scale_factor = temporal
def __call__( # noqa: PLR0913
self,
@@ -318,6 +329,7 @@ class ICLoraPipeline:
dtype=self.dtype,
device=self.device,
reference_downscale_factor=self.reference_downscale_factor,
reference_temporal_scale_factor=self.reference_temporal_scale_factor,
conditioning_attention_strength=conditioning_attention_strength,
conditioning_attention_mask=conditioning_attention_mask,
tiling_config=None,
@@ -31,6 +31,17 @@ def read_lora_reference_downscale_factor(lora_path: str) -> int:
return 1
def read_lora_reference_temporal_scale_factor(lora_path: str) -> int:
"""Read ``reference_temporal_scale_factor`` from LoRA safetensors metadata (default 1)."""
try:
with safe_open(lora_path, framework="pt") as f:
metadata = f.metadata() or {}
return int(metadata.get("reference_temporal_scale_factor", 1))
except Exception as e:
logging.warning("Failed to read metadata from LoRA file '%s': %s", lora_path, e)
return 1
def downsample_mask_video_to_latent(
mask: torch.Tensor,
target_latent_shape: VideoLatentShape,
@@ -66,6 +77,12 @@ def downsample_mask_video_to_latent(
return rearrange(latent_mask, "b 1 f h w -> b (f h w)")
def temporal_subsample(video: torch.Tensor, temporal_scale_factor: int) -> torch.Tensor:
"""VAE-aligned temporal subsampling: keep frame 0, then every Nth frame."""
indices = [0, *list(range(1, video.shape[2], temporal_scale_factor))]
return video[:, :, indices]
def append_ic_lora_reference_video_conditionings( # noqa: PLR0913
conditionings: list[ConditioningItem],
video_conditioning: list[tuple[str, float]],
@@ -77,6 +94,7 @@ def append_ic_lora_reference_video_conditionings( # noqa: PLR0913
dtype: torch.dtype,
device: torch.device,
reference_downscale_factor: int,
reference_temporal_scale_factor: int = 1,
conditioning_attention_strength: float,
conditioning_attention_mask: torch.Tensor | None,
tiling_config: TilingConfig | None = None,
@@ -93,6 +111,8 @@ def append_ic_lora_reference_video_conditionings( # noqa: PLR0913
for video_path, strength in video_conditioning:
frame_gen = decode_video_by_frame(path=video_path, frame_cap=num_frames, device=device)
video = video_preprocess(frame_gen, ref_height, ref_width, dtype, device)
if reference_temporal_scale_factor > 1:
video = temporal_subsample(video, reference_temporal_scale_factor)
if tiling_config is not None:
encoded_video = video_encoder.tiled_encode(video, tiling_config)
else:
@@ -113,6 +133,7 @@ def append_ic_lora_reference_video_conditionings( # noqa: PLR0913
cond = VideoConditionByReferenceLatent(
latent=encoded_video,
downscale_factor=scale,
temporal_scale_factor=reference_temporal_scale_factor,
strength=strength,
)
if attn_mask is not None:
@@ -0,0 +1,192 @@
import logging
import torch
from ltx_core.components.guiders import (
MultiModalGuiderFactory,
MultiModalGuiderParams,
create_multimodal_guider_factory,
)
from ltx_core.components.noisers import GaussianNoiser
from ltx_core.components.schedulers import LTX2Scheduler
from ltx_core.loader import LoraPathStrengthAndSDOps
from ltx_core.loader.registry import Registry
from ltx_core.model.transformer import LTXV_AUDIO_ONLY_MODEL_COMFY_RENAMING_MAP, LTXAudioOnlyModelConfigurator
from ltx_core.model.transformer.compiling import CompilationConfig
from ltx_core.quantization import QuantizationPolicy
from ltx_core.types import Audio
from ltx_pipelines.utils import get_device
from ltx_pipelines.utils.args import (
default_1_stage_t2a_arg_parser,
detect_checkpoint_path,
)
from ltx_pipelines.utils.blocks import (
AudioDecoder,
DiffusionStage,
PromptEncoder,
)
from ltx_pipelines.utils.constants import detect_params
from ltx_pipelines.utils.denoisers import FactoryGuidedDenoiser
from ltx_pipelines.utils.media_io import encode_audio
from ltx_pipelines.utils.types import ModalitySpec, OffloadMode
# Placeholder pixel dimensions used for ``VideoPixelShape`` construction.
# Audio-only generation reads ``frames`` and ``fps`` from the pixel shape via
# ``AudioLatentShape.from_video_pixel_shape`` (height/width are unused).
_AUDIO_ONLY_PLACEHOLDER_RES = 512
class T2AOneStagePipeline:
"""
Single-stage text-to-audio generation pipeline.
Generates audio at the target duration in a single diffusion pass with
classifier-free guidance (CFG) on the audio modality only. The video
modality is fully absent — the transformer runs audio-only by passing
``video=None`` to the ``DiffusionStage``.
Assumes full non distilled model is provided in the checkpoint_path.
"""
def __init__(
self,
checkpoint_path: str,
gemma_root: str,
loras: list[LoraPathStrengthAndSDOps],
device: torch.device | None = None,
quantization: QuantizationPolicy | None = None,
registry: Registry | None = None,
compilation_config: CompilationConfig | None = None,
offload_mode: OffloadMode = OffloadMode.NONE,
):
self.dtype = torch.bfloat16
self.device = device or get_device()
self._scheduler = LTX2Scheduler()
self.prompt_encoder = PromptEncoder(
checkpoint_path=checkpoint_path,
gemma_root=gemma_root,
dtype=self.dtype,
device=self.device,
registry=registry,
offload_mode=offload_mode,
)
# Audio-only: build an audio-only transformer (model_configurator) so the video
# weights are never instantiated, plus a use-case-specific SDOps that restricts
# checkpoint reads to the audio model's keys, so the video weights are never even
# read from disk (the loader skips any key the SDOps maps to None).
self.stage = DiffusionStage(
checkpoint_path=checkpoint_path,
dtype=self.dtype,
device=self.device,
loras=tuple(loras),
quantization=quantization,
registry=registry,
compilation_config=compilation_config,
offload_mode=offload_mode,
model_configurator=LTXAudioOnlyModelConfigurator,
model_sd_ops=LTXV_AUDIO_ONLY_MODEL_COMFY_RENAMING_MAP,
)
self.audio_decoder = AudioDecoder(
checkpoint_path=checkpoint_path,
dtype=self.dtype,
device=self.device,
registry=registry,
)
def __call__(
self,
prompt: str,
negative_prompt: str,
seed: int,
num_frames: int,
frame_rate: float,
num_inference_steps: int,
audio_guider_params: MultiModalGuiderParams | MultiModalGuiderFactory,
enhance_prompt: bool = False,
max_batch_size: int = 1,
sigmas: torch.Tensor | None = None,
) -> Audio:
generator = torch.Generator(device=self.device).manual_seed(seed)
noiser = GaussianNoiser(generator=generator)
ctx_p, ctx_n = self.prompt_encoder(
[prompt, negative_prompt],
enhance_first_prompt=enhance_prompt,
enhance_prompt_image=None,
enhance_prompt_seed=seed,
)
a_context_p = ctx_p.audio_encoding
a_context_n = ctx_n.audio_encoding
sigmas = (sigmas if sigmas is not None else self._scheduler.execute(steps=num_inference_steps)).to(
dtype=torch.float32, device=self.device
)
# Normalize to a guider factory. Plain ``MultiModalGuiderParams`` (the default /
# CLI case) becomes a simple sigma-independent guider, but callers may also pass
# their own factory for sigma-dependent guidance; ``FactoryGuidedDenoiser`` always
# consumes a factory.
audio_guider_factory = create_multimodal_guider_factory(
params=audio_guider_params,
negative_context=a_context_n,
)
_, audio_state = self.stage(
denoiser=FactoryGuidedDenoiser(
v_context=None,
a_context=a_context_p,
video_guider_factory=None,
audio_guider_factory=audio_guider_factory,
),
sigmas=sigmas,
noiser=noiser,
width=_AUDIO_ONLY_PLACEHOLDER_RES,
height=_AUDIO_ONLY_PLACEHOLDER_RES,
frames=num_frames,
fps=frame_rate,
video=None,
audio=ModalitySpec(context=a_context_p),
max_batch_size=max_batch_size,
)
return self.audio_decoder(audio_state.latent)
@torch.inference_mode()
def main() -> None:
logging.basicConfig(level=logging.INFO)
checkpoint_path = detect_checkpoint_path()
params = detect_params(checkpoint_path)
parser = default_1_stage_t2a_arg_parser(params=params)
args = parser.parse_args()
pipeline = T2AOneStagePipeline(
checkpoint_path=args.checkpoint_path,
gemma_root=args.gemma_root,
loras=tuple(args.lora) if args.lora else (),
quantization=args.quantization,
compilation_config=args.compile,
offload_mode=args.offload_mode,
)
audio = pipeline(
prompt=args.prompt,
negative_prompt=args.negative_prompt,
seed=args.seed,
num_frames=args.num_frames,
frame_rate=args.frame_rate,
num_inference_steps=args.num_inference_steps,
audio_guider_params=MultiModalGuiderParams(
cfg_scale=args.audio_cfg_guidance_scale,
stg_scale=args.audio_stg_guidance_scale,
rescale_scale=args.audio_rescale_scale,
# Audio-only generation has no video modality, so the video->audio
# (v2a) cross-modal guidance is meaningless here. 1.0 disables it.
modality_scale=1.0,
skip_step=args.audio_skip_step,
stg_blocks=args.audio_stg_blocks,
),
max_batch_size=args.max_batch_size,
)
encode_audio(audio=audio, output_path=args.output_path)
if __name__ == "__main__":
main()
@@ -650,6 +650,62 @@ def default_1_stage_arg_parser(params: PipelineParams = LTX_2_3_PARAMS) -> argpa
return parser
def default_1_stage_t2a_arg_parser(params: PipelineParams = LTX_2_3_PARAMS) -> argparse.ArgumentParser:
"""Argument parser for single-stage text-to-audio pipelines (audio-only)."""
audio_guider = params.audio_guider_params
parser = basic_arg_parser(params=params)
parser.add_argument(
"--num-frames",
type=int,
default=params.num_frames,
help="Number of frames used to derive audio duration (num-frames / frame-rate).",
)
parser.add_argument(
"--frame-rate",
type=float,
default=params.frame_rate,
help="Frame rate used with --num-frames to derive the audio duration.",
)
parser.add_argument(
"--negative-prompt",
type=str,
default=DEFAULT_NEGATIVE_PROMPT,
help="Negative prompt to steer audio generation away from artifacts.",
)
parser.add_argument(
"--audio-cfg-guidance-scale",
type=float,
default=audio_guider.cfg_scale,
help=f"Audio CFG scale (default: {audio_guider.cfg_scale}).",
)
parser.add_argument(
"--audio-stg-guidance-scale",
type=float,
default=audio_guider.stg_scale,
help=f"Audio STG scale (default: {audio_guider.stg_scale}).",
)
parser.add_argument(
"--audio-rescale-scale",
type=float,
default=audio_guider.rescale_scale,
help=f"Audio rescale scale (default: {audio_guider.rescale_scale}).",
)
parser.add_argument(
"--audio-stg-blocks",
type=int,
nargs="*",
default=audio_guider.stg_blocks,
help=f"Blocks to perturb for Audio STG (default: {audio_guider.stg_blocks}).",
)
parser.add_argument(
"--audio-skip-step",
type=int,
default=audio_guider.skip_step,
help=f"Audio skip step (default: {audio_guider.skip_step}).",
)
return parser
def default_2_stage_arg_parser(params: PipelineParams = LTX_2_3_PARAMS) -> argparse.ArgumentParser:
parser = default_1_stage_arg_parser(params=params)
parser.set_defaults(height=params.stage_2_height, width=params.stage_2_width)
@@ -7,7 +7,6 @@ removes the need for :class:`ModelLedger`.
from __future__ import annotations
import copy
import dataclasses
import logging
from collections.abc import Iterator
from contextlib import AbstractContextManager, contextmanager
@@ -40,9 +39,9 @@ from ltx_core.model.audio_vae import (
from ltx_core.model.audio_vae import (
decode_audio as vae_decode_audio,
)
from ltx_core.model.model_protocol import LTXModelProtocol, ModelConfigurator
from ltx_core.model.transformer import (
LTXV_MODEL_COMFY_RENAMING_MAP,
LTXModel,
LTXModelConfigurator,
X0Model,
)
@@ -144,7 +143,7 @@ def _streaming_model(
"""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,
device=target_device,
dtype=dtype,
cpu_slots_count=cpu_slots_count,
)
@@ -195,7 +194,7 @@ class DiffusionStage:
pattern in every pipeline.
"""
def __init__(
def __init__( # noqa: PLR0913
self,
checkpoint_path: str,
dtype: torch.dtype,
@@ -205,7 +204,9 @@ class DiffusionStage:
registry: Registry | None = None,
compilation_config: CompilationConfig | None = None,
offload_mode: OffloadMode = OffloadMode.NONE,
transformer_builder: ModelBuilderProtocol[LTXModel] | None = None,
transformer_builder: ModelBuilderProtocol[LTXModelProtocol] | None = None,
model_configurator: type[ModelConfigurator] = LTXModelConfigurator,
model_sd_ops: SDOps = LTXV_MODEL_COMFY_RENAMING_MAP,
) -> None:
self._checkpoint_path = checkpoint_path
self._dtype = dtype
@@ -213,10 +214,12 @@ class DiffusionStage:
self._quantization = quantization
self._compilation_config = compilation_config
self._offload_mode = offload_mode
# A quantization policy may pin its own configurator; otherwise use the one
# provided by the caller (defaults to the audio-video LTXModelConfigurator).
configurator = (
quantization.model_configurator
if quantization is not None and quantization.model_configurator is not None
else LTXModelConfigurator
else model_configurator
)
if transformer_builder is not None:
self._transformer_builder = transformer_builder
@@ -224,14 +227,12 @@ class DiffusionStage:
self._transformer_builder = Builder(
model_path=checkpoint_path,
model_class_configurator=configurator,
model_sd_ops=LTXV_MODEL_COMFY_RENAMING_MAP,
model_sd_ops=model_sd_ops,
loras=tuple(loras),
registry=registry or DummyRegistry(),
)
if offload_mode != OffloadMode.NONE:
if compilation_config is not None:
raise ValueError("torch.compile is not supported with layer streaming")
# WeightsProvider currently only supports plain bf16 + fp8_cast LoRA fusion
# (no companion-key emission). Quantization policies that emit
# companion keys (e.g. ``.weight_scale``) cannot be streamed yet.
@@ -240,8 +241,15 @@ class DiffusionStage:
"Block streaming is not supported with this quantization policy "
"(only bf16 and fp8_cast are currently supported)."
)
streaming_sd_ops: SDOps = LTXV_MODEL_COMFY_RENAMING_MAP
streaming_sd_ops: SDOps = model_sd_ops
streaming_module_ops: tuple[ModuleOps, ...] = ()
streaming_loras = tuple(loras)
if compilation_config:
number_of_layers = self._transformer_builder.model_config()["transformer"]["num_layers"]
streaming_sd_ops, streaming_module_ops, streaming_loras = _apply_compile_ops(
streaming_sd_ops, streaming_module_ops, streaming_loras, number_of_layers
)
if quantization is not None:
streaming_sd_ops, streaming_module_ops = _chain_quantization(
streaming_sd_ops, streaming_module_ops, quantization
@@ -251,7 +259,7 @@ class DiffusionStage:
model_path=checkpoint_path,
model_sd_ops=streaming_sd_ops,
module_ops=streaming_module_ops,
loras=tuple(loras),
loras=streaming_loras,
registry=registry or DummyRegistry(),
fuse_rule=quantization.fuse_rule if quantization is not None else bf16_fuse_rule,
blocks_attr="transformer_blocks",
@@ -273,9 +281,8 @@ class DiffusionStage:
(*self._transformer_builder.module_ops, op),
)
if self._offload_mode != OffloadMode.NONE:
new._streaming_builder = dataclasses.replace(
self._streaming_builder,
module_ops=(*self._streaming_builder.module_ops, op),
new._streaming_builder = self._streaming_builder.with_module_ops(
(*self._streaming_builder.module_ops, op),
)
return new
@@ -531,7 +538,7 @@ class PromptEncoder:
prompts[0] = generate_enhanced_prompt(
text_encoder, prompts[0], enhance_prompt_image, seed=enhance_prompt_seed
)
raw_outputs = [text_encoder.encode(p) for p in prompts]
raw_outputs = text_encoder.encode(prompts)
logger.info("Text encoder done, building embeddings processor from %s", self._checkpoint_path)
with gpu_model(self._build_embeddings_processor()) as embeddings_processor:
@@ -182,6 +182,8 @@ def _guided_denoise( # noqa: PLR0913,PLR0915
r = dict(zip(pass_names, zip(splits_v, splits_a, strict=True), strict=True))
cond_v, cond_a = r["cond"]
cond_v = cond_v if isinstance(cond_v, torch.Tensor) else torch.tensor(cond_v)
cond_a = cond_a if isinstance(cond_a, torch.Tensor) else torch.tensor(cond_a)
uncond_v, uncond_a = r.get("uncond", (0.0, 0.0))
ptb_v, ptb_a = r.get("ptb", (0.0, 0.0))
mod_v, mod_a = r.get("mod", (0.0, 0.0))
@@ -391,6 +391,24 @@ def encode_video(
logger.info(f"Video saved to {output_path}")
def encode_audio(audio: Audio, output_path: str) -> None:
"""Save an audio waveform as a 16-bit PCM ``.wav`` file at the source sampling rate.
Reuses :func:`_write_audio` (the same muxing path used by :func:`encode_video`);
the only difference is a PCM (``pcm_s16le``) stream in a WAV container instead of
the AAC stream used for muxed video.
"""
container = av.open(output_path, mode="w")
audio_stream = container.add_stream("pcm_s16le", rate=audio.sampling_rate)
audio_stream.codec_context.sample_rate = audio.sampling_rate
audio_stream.codec_context.layout = "stereo"
audio_stream.codec_context.time_base = Fraction(1, audio.sampling_rate)
try:
_write_audio(container, audio_stream, audio)
finally:
container.close()
logger.info(f"Audio saved to {output_path}")
def _encode_chunks_threaded(
container: av.container.Container,
stream: av.video.stream.VideoStream,
@@ -436,7 +436,7 @@ def res2s_audio_video_denoising_loop( # noqa: PLR0913,PLR0915,PLR0912
return video_state, audio_state
def euler_cfg_pp_denoising_loop(
def euler_cfg_pp_denoising_loop( # noqa: PLR0912
sigmas: torch.Tensor,
video_state: LatentState | None,
audio_state: LatentState | None,
@@ -514,9 +514,15 @@ def euler_cfg_pp_denoising_loop(
)
if video_state is not None and denoised_video is not None:
denoised_video = post_process_latent(denoised_video, video_state.denoise_mask, video_state.clean_latent)
denoised_video = post_process_latent(
denoised_video.float(), video_state.denoise_mask, video_state.clean_latent
)
noisy_video = video_state.latent.float()
if audio_state is not None and denoised_audio is not None:
denoised_audio = post_process_latent(denoised_audio, audio_state.denoise_mask, audio_state.clean_latent)
denoised_audio = post_process_latent(
denoised_audio.float(), audio_state.denoise_mask, audio_state.clean_latent
)
noisy_audio = audio_state.latent.float()
if sigmas[step_idx + 1] == 0:
if video_state is not None and denoised_video is not None:
@@ -525,30 +531,31 @@ def euler_cfg_pp_denoising_loop(
audio_state = replace(audio_state, latent=denoised_audio.to(model_dtype))
return video_state, audio_state
# Draw noise consecutively from the same generator: video first, audio second.
noise_video = new_noise_fn(video_state.latent, generator) if (video_state is not None and draw_noise) else None
noise_audio = new_noise_fn(audio_state.latent, generator) if (audio_state is not None and draw_noise) else None
if video_state is not None and denoised_video is not None:
video_noise = new_noise_fn(video_state.latent, generator) if draw_noise else None
x_next = stepper.step(
sample=video_state.latent,
sample=noisy_video,
denoised_sample=denoised_video,
sigmas=sigmas,
step_index=step_idx,
uncond_denoised=uncond_video,
noise=noise_video,
noise=video_noise,
)
if draw_noise:
x_next = post_process_latent(x_next, video_state.denoise_mask, video_state.clean_latent)
video_state = replace(video_state, latent=x_next.to(model_dtype))
if audio_state is not None and denoised_audio is not None:
audio_noise = new_noise_fn(audio_state.latent, generator) if draw_noise else None
x_next = stepper.step(
sample=audio_state.latent,
sample=noisy_audio,
denoised_sample=denoised_audio,
sigmas=sigmas,
step_index=step_idx,
uncond_denoised=uncond_audio,
noise=noise_audio,
noise=audio_noise,
)
if draw_noise:
x_next = post_process_latent(x_next, audio_state.denoise_mask, audio_state.clean_latent)
audio_state = replace(audio_state, latent=x_next.to(model_dtype))
return video_state, audio_state