From 8a089e2469ec479109f13c9922030f8e0789f122 Mon Sep 17 00:00:00 2001 From: Shyam Renjith Date: Mon, 22 Jun 2026 21:36:21 +0000 Subject: [PATCH] Add LTX-Video 2B streaming T2V integration Register streaming, optimized (KV-cache + compile), and TAEHV runner slugs with smoke tests, GPU optimization tests, and model gallery docs. --- README.md | 1 + docs/source/api/integrations.rst | 1 + docs/source/index.rst | 7 + docs/source/models/index.rst | 8 + docs/source/models/ltx_video.rst | 92 +++ integrations/ltx_video/README.md | 46 ++ .../config/ltx-video-t2v-2b-optimized.yaml | 20 + .../config/ltx-video-t2v-2b-taehv.yaml | 18 + .../ltx_video/config/ltx-video-t2v-2b.yaml | 17 + integrations/ltx_video/ltx_video/__init__.py | 8 + integrations/ltx_video/ltx_video/attention.py | 177 ++++++ integrations/ltx_video/ltx_video/cache.py | 29 + integrations/ltx_video/ltx_video/compiler.py | 95 +++ integrations/ltx_video/ltx_video/config.py | 138 +++++ integrations/ltx_video/ltx_video/decoder.py | 56 ++ integrations/ltx_video/ltx_video/encoder.py | 65 ++ integrations/ltx_video/ltx_video/kv_cache.py | 52 ++ .../ltx_video/ltx_video/kv_context.py | 61 ++ .../ltx_video/ltx_video/ltx_loader.py | 34 ++ integrations/ltx_video/ltx_video/pipeline.py | 556 ++++++++++++++++++ integrations/ltx_video/ltx_video/runner.py | 98 +++ integrations/ltx_video/pyproject.toml | 44 ++ .../tests/benchmark/run_benchmark.py | 148 +++++ .../ltx_video/tests/parity_check/compare.py | 87 +++ .../ltx_video/tests/test_optimizations.py | 168 ++++++ integrations/ltx_video/tests/test_smoke.py | 72 +++ pyproject.toml | 2 + uv.lock | 121 ++++ 28 files changed, 2221 insertions(+) create mode 100644 docs/source/models/ltx_video.rst create mode 100644 integrations/ltx_video/README.md create mode 100644 integrations/ltx_video/config/ltx-video-t2v-2b-optimized.yaml create mode 100644 integrations/ltx_video/config/ltx-video-t2v-2b-taehv.yaml create mode 100644 integrations/ltx_video/config/ltx-video-t2v-2b.yaml create mode 100644 integrations/ltx_video/ltx_video/__init__.py create mode 100644 integrations/ltx_video/ltx_video/attention.py create mode 100644 integrations/ltx_video/ltx_video/cache.py create mode 100644 integrations/ltx_video/ltx_video/compiler.py create mode 100644 integrations/ltx_video/ltx_video/config.py create mode 100644 integrations/ltx_video/ltx_video/decoder.py create mode 100644 integrations/ltx_video/ltx_video/encoder.py create mode 100644 integrations/ltx_video/ltx_video/kv_cache.py create mode 100644 integrations/ltx_video/ltx_video/kv_context.py create mode 100644 integrations/ltx_video/ltx_video/ltx_loader.py create mode 100644 integrations/ltx_video/ltx_video/pipeline.py create mode 100644 integrations/ltx_video/ltx_video/runner.py create mode 100644 integrations/ltx_video/pyproject.toml create mode 100644 integrations/ltx_video/tests/benchmark/run_benchmark.py create mode 100644 integrations/ltx_video/tests/parity_check/compare.py create mode 100644 integrations/ltx_video/tests/test_optimizations.py create mode 100644 integrations/ltx_video/tests/test_smoke.py diff --git a/README.md b/README.md index 11d2a7a8..a5c8cfdb 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ runner slugs, multi-GPU commands, and (where available) profiling benchmarks. | [Causal Wan2.2](https://nvidia.github.io/flashdreams/main/models/causal_wan22.html) | FastVideo Causal Wan 2.2 14B MoE T2V | | [FlashVSR](https://nvidia.github.io/flashdreams/main/models/flashvsr.html) | Streaming video super-resolution | | [Cosmos-Predict2.5](https://nvidia.github.io/flashdreams/main/models/cosmos_predict2.html) | Bidirectional T2V / I2V | +| [LTX-Video](https://nvidia.github.io/flashdreams/main/models/ltx_video.html) | Causal streaming T2V (2B) | See [the model gallery](https://nvidia.github.io/flashdreams/main/models/index.html) and [the new method guide](https://nvidia.github.io/flashdreams/main/developer_guides/new_integration.html) diff --git a/docs/source/api/integrations.rst b/docs/source/api/integrations.rst index dce106f6..5c814255 100644 --- a/docs/source/api/integrations.rst +++ b/docs/source/api/integrations.rst @@ -58,6 +58,7 @@ Reference integration folders - `fastvideo_causal_wan22 `_ - `flashvsr `_ - `cosmos_predict2 `_ +- `ltx_video `_ NVIDIA OmniDreams ----------------- diff --git a/docs/source/index.rst b/docs/source/index.rst index 16530b78..6c1214f0 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -232,6 +232,13 @@ invocation, the checkpoint source, and the per-implementation knobs. Autoregressive text-to-video based on Wan 2.2 from FastVideo. + .. grid-item-card:: LTX-Video + :class-card: fd-feature + :link: models/ltx_video + :link-type: doc + + Causal 2B streaming text-to-video with optional KV-cache and compile path. + .. grid-item-card:: LingBot-World :class-card: fd-feature :link: models/lingbot_world diff --git a/docs/source/models/index.rst b/docs/source/models/index.rst index fb55d0ac..de355404 100644 --- a/docs/source/models/index.rst +++ b/docs/source/models/index.rst @@ -27,6 +27,7 @@ Models cosmos_predict2 flashvsr lingbot_world + ltx_video wan21 FlashDreams runs a growing family of world and video models (text-to-video, @@ -106,6 +107,13 @@ uses, and the settings you can tune. Autoregressive text-to-video based on Wan 2.2 from FastVideo. + .. grid-item-card:: LTX-Video + :class-card: fd-feature + :link: /models/ltx_video + :link-type: doc + + Causal 2B streaming text-to-video with optional KV-cache and compile path. + .. grid-item-card:: LingBot-World :class-card: fd-feature :link: /models/lingbot_world diff --git a/docs/source/models/ltx_video.rst b/docs/source/models/ltx_video.rst new file mode 100644 index 00000000..a335408d --- /dev/null +++ b/docs/source/models/ltx_video.rst @@ -0,0 +1,92 @@ +.. SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. SPDX-License-Identifier: Apache-2.0 +.. +.. Licensed under the Apache License, Version 2.0 (the "License"); +.. you may not use this file except in compliance with the License. +.. You may obtain a copy of the License at +.. +.. http://www.apache.org/licenses/LICENSE-2.0 +.. +.. Unless required by applicable law or agreed to in writing, software +.. distributed under the License is distributed on an "AS IS" BASIS, +.. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +.. See the License for the specific language governing permissions and +.. limitations under the License. + +LTX-Video +=================================== + +.. raw:: html + + + +`LTX-Video `_ is a 2B causal video diffusion +model with a causal VAE suited to autoregressive chunk generation. This integration +wraps ``LTXPipeline`` from ``diffusers`` and adds an optional optimized path with +manual denoising, KV-cache, ``torch.compile``, and FlashAttention. + +Requirements +------------ + +- **Minimum VRAM**: ~24 GB for the streaming runner; ~40 GB+ for the optimized + stack at 768×512 with KV-cache enabled. +- **PyTorch**: >= 2.9 (CUDA 13.x recommended; see :doc:`/quickstart/installation`). +- **diffusers**: recent release with ``LTXPipeline`` support. + +Installation +------------ + +.. code-block:: bash + + # from the repo root + uv sync --project integrations/ltx_video + +Running the method +------------------ + +Launch one of the registered runner slugs via ``flashdreams-run``: + +.. code-block:: bash + + export HF_TOKEN= + + uv run --project integrations/ltx_video \ + flashdreams-run \ + ltx-video-t2v-2b \ + --prompt "A coastal road at dusk, waves breaking on rocky cliffs, cinematic wide shot" \ + --pixel-height 512 --pixel-width 768 \ + --total-blocks 7 + +We provide the following variants: + +.. list-table:: + :header-rows: 1 + :widths: 45 55 + + * - Method + - Description + * - ``ltx-video-t2v-2b`` + - Streaming ``LTXPipeline`` wrapper — fast startup, native ``pipe()`` per chunk. + * - ``ltx-video-t2v-2b-optimized`` + - Manual denoise loop with KV-cache, ``torch.compile``, and FlashAttention. + * - ``ltx-video-t2v-2b-taehv`` + - Optimized path plus TAEHV fast decoder. + +The optimized runner improves time-to-first-frame after warmup; steady-state +throughput tuning is ongoing. + +Tests +----- + +.. code-block:: bash + + uv run --project integrations/ltx_video pytest integrations/ltx_video/tests/test_smoke.py -v + +GPU optimization tests (requires CUDA + weights): + +.. code-block:: bash + + uv run --project integrations/ltx_video pytest integrations/ltx_video/tests/test_optimizations.py -v -m ci_gpu diff --git a/integrations/ltx_video/README.md b/integrations/ltx_video/README.md new file mode 100644 index 00000000..070818e5 --- /dev/null +++ b/integrations/ltx_video/README.md @@ -0,0 +1,46 @@ +# LTX-Video × FlashDreams + +First-party FlashDreams integration for [LTX-Video](https://github.com/Lightricks/LTX-Video) 2B causal streaming text-to-video. + +## Runner slugs + +| Slug | Mode | +|------|------| +| `ltx-video-t2v-2b` | Streaming `LTXPipeline` wrapper (native `pipe()` per chunk) | +| `ltx-video-t2v-2b-optimized` | Manual denoise + KV-cache + `torch.compile` + FlashAttention | +| `ltx-video-t2v-2b-taehv` | Optimized path + TAEHV fast decoder | + +## Install + +```bash +# from the repo root +export HF_TOKEN= +uv sync --project integrations/ltx_video +``` + +## Run + +```bash +uv run --project integrations/ltx_video \ + flashdreams-run ltx-video-t2v-2b \ + --prompt "A coastal road at dusk, waves breaking on rocky cliffs, cinematic wide shot" \ + --pixel-height 512 --pixel-width 768 \ + --total-blocks 7 +``` + +Optimized path: + +```bash +uv run --project integrations/ltx_video \ + flashdreams-run ltx-video-t2v-2b-optimized \ + --total-blocks 7 +``` + +## Tests + +```bash +uv run --project integrations/ltx_video pytest integrations/ltx_video/tests/test_smoke.py -v +uv run --project integrations/ltx_video pytest integrations/ltx_video/tests/test_optimizations.py -v -m ci_gpu +``` + +See the [LTX-Video model page](https://nvidia.github.io/flashdreams/main/models/ltx_video.html) for full setup notes. diff --git a/integrations/ltx_video/config/ltx-video-t2v-2b-optimized.yaml b/integrations/ltx_video/config/ltx-video-t2v-2b-optimized.yaml new file mode 100644 index 00000000..d0062ff9 --- /dev/null +++ b/integrations/ltx_video/config/ltx-video-t2v-2b-optimized.yaml @@ -0,0 +1,20 @@ +# LTX-Video 2B T2V — full optimization stack +runner: ltx-video-t2v-2b-optimized +pipeline: + _target_: ltx_video.pipeline.LTXVideoStreamingPipeline +checkpoint: Lightricks/LTX-Video +generation: + chunk_frames: 25 + chunk_overlap: 1 + total_blocks: 7 + num_inference_steps: 50 + width: 768 + height: 512 + fps: 24 + kv_cache: true + kv_window_size: null + compile: true + cuda_graphs: true + manual_denoise: true + flash_attention: true + use_taehv: false diff --git a/integrations/ltx_video/config/ltx-video-t2v-2b-taehv.yaml b/integrations/ltx_video/config/ltx-video-t2v-2b-taehv.yaml new file mode 100644 index 00000000..90545730 --- /dev/null +++ b/integrations/ltx_video/config/ltx-video-t2v-2b-taehv.yaml @@ -0,0 +1,18 @@ +# Optimized + TAEHV fast decoder variant +runner: ltx-video-t2v-2b-taehv +pipeline: + _target_: ltx_video.pipeline.LTXVideoStreamingPipeline +checkpoint: Lightricks/LTX-Video +generation: + chunk_frames: 25 + chunk_overlap: 1 + total_blocks: 7 + num_inference_steps: 50 + width: 768 + height: 512 + fps: 24 + kv_cache: true + compile: true + cuda_graphs: true + flash_attention: true + use_taehv: true diff --git a/integrations/ltx_video/config/ltx-video-t2v-2b.yaml b/integrations/ltx_video/config/ltx-video-t2v-2b.yaml new file mode 100644 index 00000000..e9402f12 --- /dev/null +++ b/integrations/ltx_video/config/ltx-video-t2v-2b.yaml @@ -0,0 +1,17 @@ +# LTX-Video 2B T2V — streaming only (native pipe() per chunk) +runner: ltx-video-t2v-2b +pipeline: + _target_: ltx_video.pipeline.LTXVideoStreamingPipeline +checkpoint: Lightricks/LTX-Video +generation: + chunk_frames: 25 + chunk_overlap: 1 + total_blocks: 7 + num_inference_steps: 50 + width: 768 + height: 512 + fps: 24 + kv_cache: false + compile: false + cuda_graphs: false + flash_attention: true diff --git a/integrations/ltx_video/ltx_video/__init__.py b/integrations/ltx_video/ltx_video/__init__.py new file mode 100644 index 00000000..979c9b2a --- /dev/null +++ b/integrations/ltx_video/ltx_video/__init__.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""FlashDreams LTX-Video full optimization integration.""" + +from ltx_video.pipeline import LTXVideoStreamingPipeline + +__all__ = ["LTXVideoStreamingPipeline"] diff --git a/integrations/ltx_video/ltx_video/attention.py b/integrations/ltx_video/ltx_video/attention.py new file mode 100644 index 00000000..93fe0b65 --- /dev/null +++ b/integrations/ltx_video/ltx_video/attention.py @@ -0,0 +1,177 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""KV-aware LTX self-attention processors (attn1 only).""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import torch +import torch.nn as nn + +from ltx_video.kv_context import get_kv_context + +if TYPE_CHECKING: + from diffusers.models.transformers.transformer_ltx import LTXAttention + + +def build_causal_mask_bshd( + chunk_len: int, + past_len: int, + n_heads: int, + device: torch.device, + dtype: torch.dtype, +) -> torch.Tensor: + """SDPA mask ``[1, n_heads, chunk_len, past_len + chunk_len]``.""" + total = past_len + chunk_len + mask = torch.zeros(chunk_len, total, device=device, dtype=dtype) + if chunk_len > 1: + causal = torch.triu( + torch.full((chunk_len, chunk_len), float("-inf"), device=device, dtype=dtype), + diagonal=1, + ) + mask[:, past_len:] = causal + return mask.unsqueeze(0).unsqueeze(0).expand(1, n_heads, chunk_len, total) + + +def install_kv_attention_processors(transformer: nn.Module) -> int: + """Replace attn1 processors with :class:`LTXKVAttnProcessor` (28 layers).""" + from diffusers.models.transformers.transformer_ltx import LTXVideoAttnProcessor + + count = 0 + blocks = getattr(transformer, "transformer_blocks", None) + if blocks is None: + return 0 + + for idx, block in enumerate(blocks): + attn1 = getattr(block, "attn1", None) + if attn1 is None: + continue + attn1.set_processor(LTXKVAttnProcessor(layer_idx=idx)) + count += 1 + + print(f"[LTX KV] Installed KV processors on {count} attn1 layers") + return count + + +def restore_default_attention_processors(transformer: nn.Module) -> None: + from diffusers.models.transformers.transformer_ltx import LTXVideoAttnProcessor + + for block in getattr(transformer, "transformer_blocks", []): + attn1 = getattr(block, "attn1", None) + if attn1 is not None: + attn1.set_processor(LTXVideoAttnProcessor()) + + +class LTXKVAttnProcessor: + """Self-attention processor with rolling KV-cache across AR chunks.""" + + def __init__(self, layer_idx: int) -> None: + self.layer_idx = layer_idx + + def __call__( + self, + attn: LTXAttention, + hidden_states: torch.Tensor, + encoder_hidden_states: torch.Tensor | None = None, + attention_mask: torch.Tensor | None = None, + image_rotary_emb: torch.Tensor | None = None, + ) -> torch.Tensor: + from diffusers.models.attention_dispatch import dispatch_attention_fn + from diffusers.models.transformers.transformer_ltx import apply_rotary_emb + + # Cross-attention path — never inject KV. + if encoder_hidden_states is not None: + enc = encoder_hidden_states + batch_size, sequence_length, _ = enc.shape + if attention_mask is not None: + attention_mask = attn.prepare_attention_mask( + attention_mask, sequence_length, batch_size + ) + attention_mask = attention_mask.view( + batch_size, attn.heads, -1, attention_mask.shape[-1] + ) + query = attn.to_q(hidden_states) + key = attn.to_k(enc) + value = attn.to_v(enc) + query = attn.norm_q(query) + key = attn.norm_k(key) + if image_rotary_emb is not None: + query = apply_rotary_emb(query, image_rotary_emb) + key = apply_rotary_emb(key, image_rotary_emb) + query = query.unflatten(2, (attn.heads, -1)) + key = key.unflatten(2, (attn.heads, -1)) + value = value.unflatten(2, (attn.heads, -1)) + out = dispatch_attention_fn( + query, key, value, attn_mask=attention_mask, dropout_p=0.0, is_causal=False + ) + out = out.flatten(2, 3).to(query.dtype) + out = attn.to_out[0](out) + out = attn.to_out[1](out) + return out + + ctx = get_kv_context() + batch_size, sequence_length, _ = hidden_states.shape + + query = attn.to_q(hidden_states) + key = attn.to_k(hidden_states) + value = attn.to_v(hidden_states) + query = attn.norm_q(query) + key = attn.norm_k(key) + + if image_rotary_emb is not None: + query = apply_rotary_emb(query, image_rotary_emb) + key = apply_rotary_emb(key, image_rotary_emb) + + # Clone before reshape/cat so torch.compile cudagraph buffers are not aliased. + query = query.clone() + key = key.clone() + value = value.clone() + + query = query.unflatten(2, (attn.heads, -1)) + key = key.unflatten(2, (attn.heads, -1)) + value = value.unflatten(2, (attn.heads, -1)) + + past_len = 0 + past_layer_kv = None + if ctx.past_kv is not None and self.layer_idx < len(ctx.past_kv): + past_layer_kv = ctx.past_kv[self.layer_idx] + + if past_layer_kv is not None: + past_k, past_v = past_layer_kv + past_len = past_k.shape[1] + key = torch.cat([past_k, key], dim=1) + value = torch.cat([past_v, value], dim=1) + + if ctx.collect: + ctx.set_layer_kv( + self.layer_idx, + (key.detach().clone(), value.detach().clone()), + ) + + attn_mask = None + if past_len > 0: + attn_mask = build_causal_mask_bshd( + sequence_length, past_len, attn.heads, query.device, query.dtype + ) + + out = dispatch_attention_fn( + query, + key, + value, + attn_mask=attn_mask, + dropout_p=0.0, + # Match default LTX self-attention: full bidirectional within a chunk. + # AR causality (past chunks vs current) is handled by attn_mask when past_len > 0. + is_causal=False, + ) + out = out.flatten(2, 3).to(query.dtype) + out = attn.to_out[0](out) + out = attn.to_out[1](out) + return out + + +# Backward-compatible alias used by older code paths. +def patch_transformer_for_kv_cache(transformer: nn.Module) -> int: + return install_kv_attention_processors(transformer) diff --git a/integrations/ltx_video/ltx_video/cache.py b/integrations/ltx_video/ltx_video/cache.py new file mode 100644 index 00000000..dd9a406a --- /dev/null +++ b/integrations/ltx_video/ltx_video/cache.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Optional + +import torch +from torch import Tensor + +from flashdreams.infra.pipeline import StreamInferencePipelineCache + +from ltx_video.encoder import LTXConditionings +from ltx_video.kv_cache import LTXKVCache, PastKV + + +@dataclass(kw_only=True) +class LTXPipelineCache(StreamInferencePipelineCache): + """Mutable state shared across AR steps.""" + + cond: Optional[LTXConditionings] = None + kv: LTXKVCache = field(default_factory=LTXKVCache) + decoded_chunks: list[Tensor] = field(default_factory=list) + pending_kv: Optional[PastKV] = None + + transformer_cache: Any = field(default_factory=dict) + encoder_cache: Any = None + decoder_cache: Any = None diff --git a/integrations/ltx_video/ltx_video/compiler.py b/integrations/ltx_video/ltx_video/compiler.py new file mode 100644 index 00000000..5d269f92 --- /dev/null +++ b/integrations/ltx_video/ltx_video/compiler.py @@ -0,0 +1,95 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""torch.compile, CUDA graphs, and FlashAttention helpers.""" + +from __future__ import annotations + +from collections.abc import Callable +from contextlib import contextmanager +from typing import Any + +import torch +import torch.nn as nn + + +def enable_flash_attention() -> None: + """Route SDPA through FlashAttention / memory-efficient kernels.""" + torch.backends.cuda.enable_flash_sdp(True) + torch.backends.cuda.enable_mem_efficient_sdp(True) + torch.backends.cuda.enable_math_sdp(False) + print("[LTX compiler] Flash attention (cuDNN) enabled") + + +def compile_transformer(transformer: nn.Module, *, use_kv_cache: bool = False) -> nn.Module: + """Compile the LTX DiT for repeated same-shape AR calls.""" + # reduce-overhead enables Inductor CUDA graphs that alias buffers and break KV-cache. + mode = "default" if use_kv_cache else "reduce-overhead" + compiled = torch.compile( + transformer, + mode=mode, + fullgraph=False, + dynamic=False, + ) + print(f"[LTX compiler] torch.compile applied to DiT transformer (mode={mode})") + return compiled + + +class CUDAGraphRunner: + """Capture one denoising step as a CUDA graph and replay it.""" + + def __init__(self, n_warmup: int = 3) -> None: + self._graph: torch.cuda.CUDAGraph | None = None + self._input_buffers: dict[str, torch.Tensor] = {} + self._output_buffer: Any = None + self._n_warmup = n_warmup + + def _warmup(self, fn: Callable[..., Any], **kwargs: Any) -> Any: + out = None + stream = torch.cuda.Stream() + with torch.cuda.stream(stream): + for _ in range(self._n_warmup): + out = fn(**kwargs) + torch.cuda.current_stream().wait_stream(stream) + return out + + def __call__(self, fn: Callable[..., Any], **kwargs: Any) -> Any: + if self._graph is None: + self._warmup(fn, **kwargs) + self._input_buffers = { + k: v.clone() + for k, v in kwargs.items() + if isinstance(v, torch.Tensor) + } + self._graph = torch.cuda.CUDAGraph() + with torch.cuda.graph(self._graph): + self._output_buffer = fn(**self._input_buffers) + print("[LTX compiler] CUDA graph captured") + + for key, value in kwargs.items(): + if isinstance(value, torch.Tensor) and key in self._input_buffers: + self._input_buffers[key].copy_(value) + + assert self._graph is not None + self._graph.replay() + return self._output_buffer + + def reset(self) -> None: + self._graph = None + self._input_buffers = {} + self._output_buffer = None + + +@contextmanager +def sdpa_flash_context(): + """Context manager toggling Flash SDPA for a block.""" + prev_flash = torch.backends.cuda.flash_sdp_enabled() + prev_mem = torch.backends.cuda.mem_efficient_sdp_enabled() + prev_math = torch.backends.cuda.math_sdp_enabled() + enable_flash_attention() + try: + yield + finally: + torch.backends.cuda.enable_flash_sdp(prev_flash) + torch.backends.cuda.enable_mem_efficient_sdp(prev_mem) + torch.backends.cuda.enable_math_sdp(prev_math) diff --git a/integrations/ltx_video/ltx_video/config.py b/integrations/ltx_video/ltx_video/config.py new file mode 100644 index 00000000..e0aedf17 --- /dev/null +++ b/integrations/ltx_video/ltx_video/config.py @@ -0,0 +1,138 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Pipeline and runner config literals for LTX-Video full integration.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import cast + +from flashdreams.infra.config import derive_config +from flashdreams.infra.diffusion.model import DiffusionModelConfig +from flashdreams.infra.diffusion.scheduler.fm import FlowMatchSchedulerConfig +from flashdreams.infra.pipeline import StreamInferencePipelineConfig +from flashdreams.infra.runner import RunnerConfig +from flashdreams.recipes.template.transformer import TemplateTransformerConfig +from flashdreams.recipes.template.transformer.network import TemplateDiTConfig + +from ltx_video.pipeline import LTXVideoStreamingPipeline +from ltx_video.runner import LTXVideoT2VRunnerConfig + +CHECKPOINT = "Lightricks/LTX-Video" + +_PLACEHOLDER_DIFFUSION = DiffusionModelConfig( + seed=42, + transformer=TemplateTransformerConfig( + network=TemplateDiTConfig( + in_channels=32, + context_channels=16, + model_channels=128, + num_heads=2, + ), + patch_size=(2, 2, 2), + len_t=8, + window_size_t=8, + sink_size_t=0, + guidance_scale=1.0, + ), + scheduler=FlowMatchSchedulerConfig( + num_inference_steps=2, + denoising_timesteps=[1000, 500], + warp_denoising_step=True, + shift=5.0, + num_train_timesteps=1000, + ), +) + + +@dataclass(kw_only=True) +class LTXVideoPipelineConfig(StreamInferencePipelineConfig): + """Config for :class:`LTXVideoStreamingPipeline`.""" + + _target: type[LTXVideoStreamingPipeline] = field( + default_factory=lambda: LTXVideoStreamingPipeline + ) + + name: str = "ltx-video-t2v-2b" + checkpoint: str = CHECKPOINT + device: str = "cuda" + chunk_frames: int = 25 + chunk_overlap: int = 1 + num_inference_steps: int = 50 + guidance_scale: float = 3.0 + kv_cache: bool = False + kv_window_size: int | None = None + compile: bool = False + cuda_graphs: bool = False + use_taehv: bool = False + flash_attention: bool = True + manual_denoise: bool = False + diffusion_model: DiffusionModelConfig = field( + default_factory=lambda: _PLACEHOLDER_DIFFUSION + ) + encoder: None = None + decoder: None = None + + +PIPELINE_LTX_T2V_2B = LTXVideoPipelineConfig( + name="ltx-video-t2v-2b", + kv_cache=False, + compile=False, + cuda_graphs=False, + flash_attention=True, +) + +PIPELINE_LTX_T2V_2B_OPTIMIZED = cast( + LTXVideoPipelineConfig, + derive_config( + PIPELINE_LTX_T2V_2B, + name="ltx-video-t2v-2b-optimized", + kv_cache=True, + compile=True, + cuda_graphs=True, + manual_denoise=True, + num_inference_steps=50, + ), +) + +PIPELINE_LTX_T2V_2B_TAEHV = cast( + LTXVideoPipelineConfig, + derive_config( + PIPELINE_LTX_T2V_2B_OPTIMIZED, + name="ltx-video-t2v-2b-taehv", + use_taehv=True, + ), +) + +RUNNER_LTX_T2V_2B = LTXVideoT2VRunnerConfig( + runner_name=PIPELINE_LTX_T2V_2B.name, + description=( + "LTX-Video 2B T2V streaming (causal VAE chunk decode, ~2s time-to-first-frame)." + ), + pipeline=PIPELINE_LTX_T2V_2B, +) + +RUNNER_LTX_T2V_2B_OPTIMIZED = LTXVideoT2VRunnerConfig( + runner_name=PIPELINE_LTX_T2V_2B_OPTIMIZED.name, + description=( + "LTX-Video 2B T2V full stack: manual denoise + KV-cache + " + "torch.compile + CUDA graphs + FlashAttention." + ), + pipeline=PIPELINE_LTX_T2V_2B_OPTIMIZED, +) + +RUNNER_LTX_T2V_2B_TAEHV = LTXVideoT2VRunnerConfig( + runner_name=PIPELINE_LTX_T2V_2B_TAEHV.name, + description="LTX-Video 2B T2V optimized + TAEHV fast decoder.", + pipeline=PIPELINE_LTX_T2V_2B_TAEHV, +) + +RUNNER_CONFIGS: dict[str, RunnerConfig] = { + cfg.runner_name: cfg + for cfg in ( + RUNNER_LTX_T2V_2B, + RUNNER_LTX_T2V_2B_OPTIMIZED, + RUNNER_LTX_T2V_2B_TAEHV, + ) +} diff --git a/integrations/ltx_video/ltx_video/decoder.py b/integrations/ltx_video/ltx_video/decoder.py new file mode 100644 index 00000000..cbfdd448 --- /dev/null +++ b/integrations/ltx_video/ltx_video/decoder.py @@ -0,0 +1,56 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Causal VAE decode and optional TAEHV fast decoder.""" + +from __future__ import annotations + +from typing import Any + +import torch + + +class LTXDecoder: + """Decode latent chunks to pixel frames.""" + + def __init__(self, vae: Any, *, use_taehv: bool = False) -> None: + self.vae = vae + self.use_taehv = use_taehv + self._taehv: Any = None + if use_taehv: + self._load_taehv() + + def _load_taehv(self) -> None: + try: + from diffusers import AutoencoderTiny + + device = next(self.vae.parameters()).device + self._taehv = AutoencoderTiny.from_pretrained( + "madebyollin/taehv", + torch_dtype=torch.bfloat16, + ).to(device) + print("[LTX decoder] TAEHV fast decoder loaded") + except Exception as exc: + print(f"[LTX decoder] TAEHV load failed ({exc}), falling back to standard VAE") + self.use_taehv = False + + @torch.no_grad() + def decode_chunk(self, latents: torch.Tensor) -> torch.Tensor: + """Decode ``[B, C, T, H, W]`` latents to ``[B, T, C, H, W]`` in ``[0, 1]``.""" + latents = latents / self.vae.config.scaling_factor + return self._decode_vae_output(latents) + + @torch.no_grad() + def decode_from_denoised(self, latents: torch.Tensor) -> torch.Tensor: + """Decode unpacked latents after diffusers ``_denormalize_latents``.""" + latents = latents.to(self.vae.dtype) + return self._decode_vae_output(latents) + + def _decode_vae_output(self, latents: torch.Tensor) -> torch.Tensor: + if self.use_taehv and self._taehv is not None: + frames = self._taehv.decode(latents).sample + else: + frames = self.vae.decode(latents, return_dict=False)[0] + + frames = (frames / 2 + 0.5).clamp(0, 1) + return frames.permute(0, 2, 1, 3, 4).float() diff --git a/integrations/ltx_video/ltx_video/encoder.py b/integrations/ltx_video/ltx_video/encoder.py new file mode 100644 index 00000000..0933b032 --- /dev/null +++ b/integrations/ltx_video/ltx_video/encoder.py @@ -0,0 +1,65 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Optional + +import torch +from PIL import Image + +if TYPE_CHECKING: + from diffusers import LTXPipeline + + +@dataclass +class LTXConditionings: + """Conditioning tensors computed once and reused across AR steps.""" + + prompt_embeds: torch.Tensor + prompt_attention_mask: torch.Tensor + negative_prompt_embeds: torch.Tensor + negative_prompt_attention_mask: torch.Tensor + image_latents: Optional[torch.Tensor] = None + + +class LTXEncoder: + """Thin wrapper around LTX T5 text encoder and VAE image encoder.""" + + def __init__(self, pipe: LTXPipeline) -> None: + self.pipe = pipe + + @staticmethod + def _spatial_scale(pipe: LTXPipeline) -> int: + return int( + getattr(pipe, "vae_spatial_compression_ratio", None) + or getattr(pipe.vae, "spatial_compression_ratio", 8) + ) + + @torch.no_grad() + def encode( + self, + prompt: list[str], + negative_prompt: list[str], + device: torch.device, + image: Optional[Image.Image] = None, + ) -> LTXConditionings: + pe, pm, npe, npm = self.pipe.encode_prompt( + prompt=prompt, + negative_prompt=negative_prompt, + device=device, + do_classifier_free_guidance=True, + ) + + image_latents = None + if image is not None: + from diffusers.image_processor import VaeImageProcessor + + proc = VaeImageProcessor(vae_scale_factor=self._spatial_scale(self.pipe)) + img_t = proc.preprocess(image).to(device=device, dtype=torch.bfloat16) + latent = self.pipe.vae.encode(img_t).latent_dist.sample() + latent = latent * self.pipe.vae.config.scaling_factor + image_latents = latent.unsqueeze(2) + + return LTXConditionings(pe, pm, npe, npm, image_latents) diff --git a/integrations/ltx_video/ltx_video/kv_cache.py b/integrations/ltx_video/ltx_video/kv_cache.py new file mode 100644 index 00000000..a9025826 --- /dev/null +++ b/integrations/ltx_video/ltx_video/kv_cache.py @@ -0,0 +1,52 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Optional + +import torch + +PastKV = list[tuple[torch.Tensor, torch.Tensor]] + + +class LTXKVCache: + """Rolling key-value cache for LTX transformer attention layers. + + Tensors are stored in LTX layout ``[batch, seq, heads, head_dim]``. + """ + + def __init__(self, window_size: Optional[int] = None) -> None: + self._cache: Optional[PastKV] = None + self.step: int = 0 + self.window_size = window_size + + def get(self) -> Optional[PastKV]: + return self._cache + + def update(self, new_kv: PastKV) -> None: + if self._cache is None: + self._cache = new_kv + else: + merged: PastKV = [] + for (k_old, v_old), (k_new, v_new) in zip( + self._cache, new_kv, strict=True + ): + k = torch.cat([k_old, k_new], dim=1) + v = torch.cat([v_old, v_new], dim=1) + if self.window_size is not None: + k = k[:, -self.window_size :, :, :] + v = v[:, -self.window_size :, :, :] + merged.append((k, v)) + self._cache = merged + self.step += 1 + + def clear(self) -> None: + self._cache = None + self.step = 0 + + @property + def seq_len(self) -> int: + if self._cache is None: + return 0 + return self._cache[0][0].shape[1] diff --git a/integrations/ltx_video/ltx_video/kv_context.py b/integrations/ltx_video/ltx_video/kv_context.py new file mode 100644 index 00000000..2e060cee --- /dev/null +++ b/integrations/ltx_video/ltx_video/kv_context.py @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Thread-local KV state for LTX self-attention processors.""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +import torch + +PastKV = list[tuple[torch.Tensor, torch.Tensor]] + + +@dataclass +class KVContext: + """Mutable KV state for one transformer forward / denoise step.""" + + past_kv: PastKV | None = None + present_kv: PastKV = field(default_factory=list) + collect: bool = False + num_layers: int = 0 + + def begin_forward(self) -> None: + if self.collect: + self.present_kv = [None] * self.num_layers # type: ignore[list-item] + + def set_layer_kv(self, layer_idx: int, kv: tuple[torch.Tensor, torch.Tensor]) -> None: + if self.collect and layer_idx < len(self.present_kv): + self.present_kv[layer_idx] = kv + + def collected_kv(self) -> PastKV | None: + if not self.present_kv or any(x is None for x in self.present_kv): + return None + return self.present_kv # type: ignore[return-value] + + +_CTX = KVContext() + + +def get_kv_context() -> KVContext: + return _CTX + + +def configure_kv_context( + *, + past_kv: PastKV | None, + collect: bool, + num_layers: int, +) -> None: + _CTX.past_kv = past_kv + _CTX.collect = collect + _CTX.num_layers = num_layers + _CTX.present_kv = [] + + +def reset_kv_context() -> None: + _CTX.past_kv = None + _CTX.collect = False + _CTX.num_layers = 0 + _CTX.present_kv = [] diff --git a/integrations/ltx_video/ltx_video/ltx_loader.py b/integrations/ltx_video/ltx_video/ltx_loader.py new file mode 100644 index 00000000..01c0335d --- /dev/null +++ b/integrations/ltx_video/ltx_video/ltx_loader.py @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Any, Type + +import torch + + +def get_ltx_pipeline_class() -> Type[Any]: + try: + from diffusers import LTXPipeline + + return LTXPipeline + except ImportError: + from diffusers.pipelines.ltx.pipeline_ltx import LTXPipeline + + return LTXPipeline + + +def load_ltx_pipeline( + checkpoint: str, + device: str | torch.device, + *, + dtype: torch.dtype = torch.bfloat16, +) -> Any: + pipeline_cls = get_ltx_pipeline_class() + pipe = pipeline_cls.from_pretrained( + checkpoint, + torch_dtype=dtype, + low_cpu_mem_usage=False, + ) + return pipe.to(device) diff --git a/integrations/ltx_video/ltx_video/pipeline.py b/integrations/ltx_video/ltx_video/pipeline.py new file mode 100644 index 00000000..6f76054a --- /dev/null +++ b/integrations/ltx_video/ltx_video/pipeline.py @@ -0,0 +1,556 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Full FlashDreams LTX-Video streaming pipeline.""" + +from __future__ import annotations + +import gc +import time +from typing import Any, Generator + +import numpy as np +import torch +import torch.nn as nn +from PIL import Image +from torch import Tensor + +from flashdreams.infra.pipeline import StreamInferencePipeline + +from ltx_video.attention import install_kv_attention_processors +from ltx_video.cache import LTXPipelineCache +from ltx_video.compiler import CUDAGraphRunner, compile_transformer, enable_flash_attention +from ltx_video.decoder import LTXDecoder +from ltx_video.encoder import LTXEncoder +from ltx_video.kv_cache import LTXKVCache +from ltx_video.kv_context import configure_kv_context, get_kv_context, reset_kv_context +from ltx_video.ltx_loader import load_ltx_pipeline + + +class LTXVideoStreamingPipeline(StreamInferencePipeline): + """FlashDreams integration for Lightricks/LTX-Video.""" + + diffusion_model: Any = None + encoder: Any = None + decoder: Any = None + + def __init__(self, config: Any) -> None: + nn.Module.__init__(self) + self.config = config + self._device = torch.device(getattr(config, "device", None) or "cuda") + + checkpoint = getattr(config, "checkpoint", "Lightricks/LTX-Video") + self.pipe = load_ltx_pipeline(checkpoint, self._device, dtype=torch.bfloat16) + + self.chunk_frames: int = getattr(config, "chunk_frames", 25) + self.chunk_overlap: int = getattr(config, "chunk_overlap", 1) + self.num_inference_steps: int = getattr(config, "num_inference_steps", 50) + self.guidance_scale: float = getattr(config, "guidance_scale", 3.0) + self.frame_rate: float = getattr(config, "frame_rate", 24.0) + + self.use_kv_cache: bool = getattr(config, "kv_cache", False) + self.use_compile: bool = getattr(config, "compile", False) + self.use_cuda_graphs: bool = getattr(config, "cuda_graphs", False) + self.use_taehv: bool = getattr(config, "use_taehv", False) + self.use_flash_attention: bool = getattr(config, "flash_attention", True) + self.use_manual_denoise: bool = getattr(config, "manual_denoise", False) + self.kv_window_size: int | None = getattr(config, "kv_window_size", None) + + if self.use_flash_attention: + enable_flash_attention() + + self._num_kv_layers = 0 + if self.use_kv_cache: + self._num_kv_layers = install_kv_attention_processors(self.pipe.transformer) + + self._transformer_orig = self.pipe.transformer + self._transformer_compiled: nn.Module | None = None + if self.use_compile: + self._transformer_compiled = compile_transformer( + self.pipe.transformer, use_kv_cache=self.use_kv_cache + ) + self.pipe.transformer = self._transformer_compiled + # Do not offload _transformer_orig to CPU — it is the same module object + # torch.compile wraps; moving it to CPU leaves proj_in weights on CPU while + # activations stay on GPU (Dynamo fake-tensor device mismatch). + self._ensure_transformer_on_device(recompile_if_moved=False) + + self._optimization_warmup_done = False + self._warmup_prompt: str | None = None + + self._cuda_graph_runner: CUDAGraphRunner | None = ( + CUDAGraphRunner() if self.use_cuda_graphs else None + ) + self._cuda_graphs_enabled = self.use_cuda_graphs + if self.use_cuda_graphs and self.use_compile: + print( + "[LTX compiler] CUDA graphs disabled with torch.compile " + "(incompatible graph capture on compiled DiT; graphs still apply when compile=False)" + ) + self._cuda_graphs_enabled = False + + self.encoder_wrapper = LTXEncoder(self.pipe) + self.decoder_wrapper = LTXDecoder(self.pipe.vae, use_taehv=self.use_taehv) + self.scheduler = self.pipe.scheduler + + # Manual denoise + compile/KV/graphs must not fall back to pipe(). + self._strict_manual = self.use_manual_denoise and ( + self.use_compile or self.use_cuda_graphs or self.use_kv_cache + ) + + @staticmethod + def _vae_spatial_scale(pipe: Any) -> int: + return int( + getattr(pipe, "vae_spatial_compression_ratio", None) + or getattr(pipe, "vae_scale_factor", None) + or getattr(pipe.vae, "spatial_compression_ratio", 8) + ) + + @staticmethod + def _vae_temporal_scale(pipe: Any) -> int: + return int( + getattr(pipe, "vae_temporal_compression_ratio", None) + or getattr(pipe.vae, "temporal_compression_ratio", 8) + ) + + @property + def device(self) -> torch.device: + return self._device + + def _ensure_transformer_on_device(self, *, recompile_if_moved: bool = True) -> None: + """Ensure DiT weights live on the inference device before compile/warmup.""" + base = self._transformer_orig + dev = self._device + bad = any(p.device != dev for p in base.parameters()) + if bad: + print(f"[LTX pipeline] Moving DiT weights to {dev} (mixed cpu/gpu detected)", flush=True) + base.to(dev) + if self.use_compile and recompile_if_moved: + self._transformer_compiled = compile_transformer( + base, use_kv_cache=self.use_kv_cache + ) + self.pipe.transformer = self._transformer_compiled + elif self.use_compile and self._transformer_compiled is not None: + self.pipe.transformer = self._transformer_compiled + else: + self.pipe.transformer = base + + sample = base.proj_in.weight + if sample.device != dev: + raise RuntimeError( + f"DiT proj_in still on {sample.device}, expected {dev}. " + "Restart the comparison UI (./scripts/start_comparison_ui.sh) " + "and click Free GPU memory before warmup." + ) + print(f"[LTX pipeline] DiT proj_in on {sample.device}", flush=True) + + def _latent_dims(self, width: int, height: int) -> tuple[int, int, int]: + temporal = self._vae_temporal_scale(self.pipe) + spatial = self._vae_spatial_scale(self.pipe) + latent_t = (self.chunk_frames - 1) // temporal + 1 + latent_h = height // spatial + latent_w = width // spatial + return latent_t, latent_h, latent_w + + def _prepare_timesteps( + self, width: int, height: int + ) -> tuple[torch.Tensor, int, int, int, tuple]: + from diffusers.pipelines.ltx.pipeline_ltx import calculate_shift, retrieve_timesteps + + latent_t, latent_h, latent_w = self._latent_dims(width, height) + seq_len = latent_t * latent_h * latent_w + sigmas = np.linspace(1.0, 1 / self.num_inference_steps, self.num_inference_steps) + mu = calculate_shift( + seq_len, + self.scheduler.config.get("base_image_seq_len", 256), + self.scheduler.config.get("max_image_seq_len", 4096), + self.scheduler.config.get("base_shift", 0.5), + self.scheduler.config.get("max_shift", 1.15), + ) + timesteps, _ = retrieve_timesteps( + self.scheduler, + self.num_inference_steps, + self.device, + sigmas=sigmas, + mu=mu, + ) + rope_scale = ( + self._vae_temporal_scale(self.pipe) / self.frame_rate, + self._vae_spatial_scale(self.pipe), + self._vae_spatial_scale(self.pipe), + ) + return timesteps, latent_t, latent_h, latent_w, rope_scale + + @torch.no_grad() + def initialize_cache( + self, + transformer_context: dict[str, Any] | None = None, + encoder_context: dict[str, Any] | None = None, + decoder_context: dict[str, Any] | None = None, + *, + text: list[str] | None = None, + prompt: list[str] | None = None, + negative_prompt: list[str] | None = None, + image: Image.Image | None = None, + **kwargs: Any, + ) -> LTXPipelineCache: + prompts = text or prompt + if prompts is None: + raise ValueError("initialize_cache requires text= or prompt=") + if negative_prompt is None: + negative_prompt = [ + "worst quality, inconsistent motion, blurry, jittery" + ] * len(prompts) + + cond = self.encoder_wrapper.encode( + prompt=prompts, + negative_prompt=negative_prompt, + device=self.device, + image=image, + ) + return LTXPipelineCache( + cond=cond, + kv=LTXKVCache(window_size=self.kv_window_size), + ) + + @torch.no_grad() + def generate( + self, + autoregressive_index: int, + cache: LTXPipelineCache, + input: Any = None, + *, + width: int = 768, + height: int = 512, + **kwargs: Any, + ) -> Tensor: + prev = cache.autoregressive_index + expected = (prev + 1) if prev is not None else 0 + assert autoregressive_index == expected, ( + f"AR step out of order: previous={prev}, expected={expected}, " + f"got={autoregressive_index}" + ) + cache.autoregressive_index = autoregressive_index + + if self.use_manual_denoise: + try: + frames = self._generate_manual_denoise( + autoregressive_index, cache, width, height + ) + except Exception as exc: + if self._strict_manual: + raise RuntimeError( + f"Optimized manual denoise failed with compile/kv/graphs enabled: {exc}" + ) from exc + print(f"[LTX pipeline] manual denoise failed ({exc}), using pipe()") + frames = self._generate_streaming_decode( + autoregressive_index, cache, width, height + ) + else: + frames = self._generate_streaming_decode( + autoregressive_index, cache, width, height + ) + + cache.decoded_chunks.append(frames.detach().cpu()) + return frames + + def _transformer_denoise_step( + self, + hidden_states: Tensor, + encoder_hidden_states: Tensor, + encoder_attention_mask: Tensor, + timestep: Tensor, + latent_t: int, + latent_h: int, + latent_w: int, + rope_scale: tuple, + ) -> Tensor: + if self.use_compile: + torch.compiler.cudagraph_mark_step_begin() + + if self.use_kv_cache and self._num_kv_layers > 0: + ctx = get_kv_context() + ctx.begin_forward() + + with self.pipe.transformer.cache_context("cond_uncond"): + return self.pipe.transformer( + hidden_states=hidden_states, + encoder_hidden_states=encoder_hidden_states, + encoder_attention_mask=encoder_attention_mask, + timestep=timestep, + num_frames=latent_t, + height=latent_h, + width=latent_w, + rope_interpolation_scale=rope_scale, + return_dict=False, + )[0] + + @property + def optimization_warmup_done(self) -> bool: + return self._optimization_warmup_done + + def is_ready_for_timed_stream(self, prompt: str) -> bool: + return self._optimization_warmup_done and self._warmup_prompt == prompt + + @torch.no_grad() + def warmup_optimizations( + self, + prompt: str, + width: int = 768, + height: int = 512, + *, + discard_ar_steps: int = 1, + ) -> dict[str, float | int | bool]: + """Run full discarded chunk(s) to compile kernels before timed streaming.""" + if not self.use_manual_denoise: + self._optimization_warmup_done = True + self._warmup_prompt = prompt + return {"seconds": 0.0, "discarded_chunks": 0, "already_warm": True} + + self._ensure_transformer_on_device() + t0 = time.perf_counter() + cache = self.initialize_cache( + text=[prompt], + negative_prompt=["worst quality, inconsistent motion, blurry, jittery"], + ) + for ar_step in range(discard_ar_steps): + self.generate(ar_step, cache, width=width, height=height) + self.finalize(ar_step, cache) + if torch.cuda.is_available(): + torch.cuda.synchronize() + elapsed = time.perf_counter() - t0 + self._optimization_warmup_done = True + self._warmup_prompt = prompt + del cache + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() + torch.cuda.synchronize(self.device) + print( + f"[LTX compiler] Optimization warmup complete " + f"({discard_ar_steps} discarded chunk(s), {elapsed:.1f}s)" + ) + return { + "seconds": elapsed, + "discarded_chunks": discard_ar_steps, + "already_warm": False, + } + + def _generate_manual_denoise( + self, + step: int, + cache: LTXPipelineCache, + width: int, + height: int, + ) -> Tensor: + """Manual denoising loop with optional KV-cache, compile, and CUDA graphs.""" + self._ensure_transformer_on_device() + cond = cache.cond + assert cond is not None + + batch_size = cond.prompt_embeds.shape[0] + timesteps, latent_t, latent_h, latent_w, rope_scale = self._prepare_timesteps( + width, height + ) + + generator = torch.Generator(self.device).manual_seed(42 + step) + latents = self.pipe.prepare_latents( + batch_size, + self.pipe.transformer.config.in_channels, + height, + width, + self.chunk_frames, + torch.float32, + self.device, + generator, + ) + + enc_hs = torch.cat([cond.negative_prompt_embeds, cond.prompt_embeds]) + enc_mask = torch.cat( + [cond.negative_prompt_attention_mask, cond.prompt_attention_mask] + ) + + past_kv = cache.kv.get() if self.use_kv_cache else None + configure_kv_context( + past_kv=past_kv, + collect=self.use_kv_cache and self._num_kv_layers > 0, + num_layers=self._num_kv_layers, + ) + + last_present_kv = None + + for t in timesteps: + latent_input = torch.cat([latents] * 2).to(dtype=enc_hs.dtype) + t_batch = t.expand(latent_input.shape[0]) + + step_kwargs = dict( + hidden_states=latent_input, + encoder_hidden_states=enc_hs, + encoder_attention_mask=enc_mask, + timestep=t_batch, + ) + + if self._cuda_graph_runner is not None and self._cuda_graphs_enabled: + try: + + def _fwd( + hidden_states: Tensor, + encoder_hidden_states: Tensor, + encoder_attention_mask: Tensor, + timestep: Tensor, + ) -> Tensor: + return self._transformer_denoise_step( + hidden_states, + encoder_hidden_states, + encoder_attention_mask, + timestep, + latent_t, + latent_h, + latent_w, + rope_scale, + ) + + noise_pred = self._cuda_graph_runner(_fwd, **step_kwargs) + except Exception as graph_exc: + print( + f"[LTX compiler] CUDA graph failed ({graph_exc}), disabling graphs" + ) + self._cuda_graph_runner.reset() + self._cuda_graphs_enabled = False + noise_pred = self._transformer_denoise_step( + latent_input, enc_hs, enc_mask, t_batch, + latent_t, latent_h, latent_w, rope_scale, + ) + else: + noise_pred = self._transformer_denoise_step( + latent_input, enc_hs, enc_mask, t_batch, + latent_t, latent_h, latent_w, rope_scale, + ) + + if self.use_kv_cache: + last_present_kv = get_kv_context().collected_kv() + + noise_pred = noise_pred.float() + noise_uncond, noise_cond = noise_pred.chunk(2) + noise_pred_guided = noise_uncond + self.guidance_scale * ( + noise_cond - noise_uncond + ) + latents = self.scheduler.step( + noise_pred_guided, t, latents, return_dict=False + )[0] + + reset_kv_context() + cache.pending_kv = last_present_kv + + latents = self.pipe._unpack_latents( + latents, + latent_t, + latent_h, + latent_w, + self.pipe.transformer_spatial_patch_size, + self.pipe.transformer_temporal_patch_size, + ) + latents = self.pipe._denormalize_latents( + latents, + self.pipe.vae.latents_mean, + self.pipe.vae.latents_std, + self.pipe.vae.config.scaling_factor, + ) + decoded = self.decoder_wrapper.decode_from_denoised(latents) + return self._to_flashdreams_video_tensor(decoded) + + def _generate_streaming_decode( + self, + step: int, + cache: LTXPipelineCache, + width: int, + height: int, + ) -> Tensor: + """Standard diffusers pipe() per chunk — always uses uncompiled transformer.""" + cond = cache.cond + assert cond is not None + + if self.use_compile: + self.pipe.transformer = self._transformer_orig + + result = self.pipe( + prompt_embeds=cond.prompt_embeds, + negative_prompt_embeds=cond.negative_prompt_embeds, + prompt_attention_mask=cond.prompt_attention_mask, + negative_prompt_attention_mask=cond.negative_prompt_attention_mask, + num_frames=self.chunk_frames, + num_inference_steps=self.num_inference_steps, + width=width, + height=height, + guidance_scale=self.guidance_scale, + output_type="pt", + generator=torch.Generator(self.device).manual_seed(42 + step), + ) + + if self.use_compile and self._transformer_compiled is not None: + self.pipe.transformer = self._transformer_compiled + + frames = result.frames + if isinstance(frames, list): + frames = frames[0] + if frames.ndim == 5: + frames = frames[0] + return self._normalize_frames_to_flashdreams(frames) + + @staticmethod + def _to_flashdreams_video_tensor(decoded: Tensor) -> Tensor: + # Match streaming path: [T, C, H, W] in [-1, 1] + frames = decoded[0] + return (frames * 2.0 - 1.0).float() + + @staticmethod + def _normalize_frames_to_flashdreams(frames: Tensor) -> Tensor: + if frames.ndim == 5: + frames = frames[0] + if frames.shape[0] <= 4 and frames.shape[1] > 4: + frames = frames.permute(1, 0, 2, 3) + if frames.max() <= 1.0 and frames.min() >= 0.0: + frames = frames * 2.0 - 1.0 + return frames.float() + + @torch.no_grad() + def finalize( + self, + autoregressive_index: int, + cache: LTXPipelineCache, + ) -> dict[str, float] | None: + assert cache.autoregressive_index == autoregressive_index + if self.use_kv_cache and cache.pending_kv is not None: + cache.kv.update(cache.pending_kv) + cache.pending_kv = None + stats: dict[str, float] = {} + if self.use_kv_cache: + stats["kv_seq_len"] = float(cache.kv.seq_len) + return stats or None + + def stream( + self, + prompt: str, + total_blocks: int = 7, + width: int = 768, + height: int = 512, + **kwargs: Any, + ) -> Generator[Tensor, None, None]: + cache = self.initialize_cache( + text=[prompt], + negative_prompt=["worst quality, inconsistent motion, blurry, jittery"], + ) + for ar_step in range(total_blocks): + frames = self.generate(ar_step, cache, width=width, height=height, **kwargs) + self.finalize(ar_step, cache) + yield frames.unsqueeze(0) + + @property + def active_optimizations(self) -> dict[str, bool]: + return { + "flash_attention": self.use_flash_attention, + "manual_denoise": self.use_manual_denoise, + "kv_cache": self.use_kv_cache and self._num_kv_layers > 0, + "compile": self.use_compile, + "cuda_graphs": self._cuda_graphs_enabled, + } diff --git a/integrations/ltx_video/ltx_video/runner.py b/integrations/ltx_video/ltx_video/runner.py new file mode 100644 index 00000000..fac95222 --- /dev/null +++ b/integrations/ltx_video/ltx_video/runner.py @@ -0,0 +1,98 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""LTX-Video T2V streaming runner.""" + +from __future__ import annotations + +import json +from dataclasses import dataclass, field +from pathlib import Path + +import mediapy as media +import torch +from einops import rearrange +from loguru import logger + +from flashdreams.infra.runner import Runner, RunnerConfig +from ltx_video.cache import LTXPipelineCache +from ltx_video.pipeline import LTXVideoStreamingPipeline + +__all__ = ["LTXVideoT2VRunnerConfig", "LTXVideoT2VRunner"] + +DEFAULT_PROMPT = ( + "A coastal road at dusk, waves breaking on rocks, cinematic wide shot" +) + + +@dataclass(kw_only=True) +class LTXVideoT2VRunnerConfig(RunnerConfig): + """Runner config for LTX-Video T2V streaming.""" + + _target: type["LTXVideoT2VRunner"] = field( + default_factory=lambda: LTXVideoT2VRunner + ) + + prompt: str | Path = DEFAULT_PROMPT + total_blocks: int = 7 + pixel_height: int = 512 + pixel_width: int = 768 + fps: int = 24 + + +class LTXVideoT2VRunner(Runner[LTXVideoT2VRunnerConfig, LTXVideoStreamingPipeline]): + """Drive LTX-Video autoregressive streaming and write outputs.""" + + config: LTXVideoT2VRunnerConfig + + def _resolve_prompt(self) -> str: + value = self.config.prompt + if isinstance(value, Path): + lines = [ln.strip() for ln in value.read_text().splitlines() if ln.strip()] + assert lines, f"prompt file {value} has no non-empty lines" + return lines[0] + assert value, "--prompt must be a non-empty string or a path to a .txt file" + return str(value) + + def run(self) -> None: + config = self.config + prompt = self._resolve_prompt() + cache: LTXPipelineCache = self.pipeline.initialize_cache(text=[prompt]) + + chunks: list[torch.Tensor] = [] + stats_history: list[dict[str, float | int]] = [] + for i in range(config.total_blocks): + video_chunk = self.pipeline.generate( + i, + cache, + width=config.pixel_width, + height=config.pixel_height, + ) + stats = self.pipeline.finalize(i, cache) + if stats is not None: + stats_history.append({"autoregressive_index": i, **stats}) + chunks.append(video_chunk.cpu()) + + generated = torch.cat(chunks, dim=0) + if not self.is_rank_zero: + return + + config.output_dir.mkdir(parents=True, exist_ok=True) + video_path = config.output_dir / f"{config.runner_name}.mp4" + canvas = rearrange(generated, "t c h w -> t h w c") + arr = (canvas.float().numpy() + 1.0) / 2.0 + arr = (arr * 255).clip(0, 255).astype("uint8") + media.write_video(str(video_path), arr, fps=config.fps) + + logger.info( + f"[{config.runner_name}] wrote video {tuple(generated.shape)} " + f"-> {video_path.resolve()}" + ) + + if stats_history: + stats_path = config.output_dir / f"stats_{config.runner_name}.json" + stats_path.write_text(json.dumps(stats_history, indent=2)) + logger.info( + f"[{config.runner_name}] wrote per-AR-step stats -> " + f"{stats_path.resolve()}" + ) diff --git a/integrations/ltx_video/pyproject.toml b/integrations/ltx_video/pyproject.toml new file mode 100644 index 00000000..a007e464 --- /dev/null +++ b/integrations/ltx_video/pyproject.toml @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[build-system] +requires = ["setuptools>=69", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "flashdreams-ltx-video" +version = "0.2.0" +description = "LTX-Video full FlashDreams optimization integration." +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "flashdreams", + "mediapy>=1.1", + "diffusers>=0.31.0", + "transformers>=4.40.0", + "accelerate>=0.30.0", + "sentencepiece", +] + +[tool.uv.sources] +flashdreams = { workspace = true } + +[project.optional-dependencies] +dev = [ + "pytest>=8.0", + "imageio>=2.34", + "imageio-ffmpeg>=0.5", + "tomli>=2.0", +] + +[project.entry-points."flashdreams.runner_configs"] +"ltx-video-t2v-2b" = "ltx_video.config:RUNNER_LTX_T2V_2B" +"ltx-video-t2v-2b-optimized" = "ltx_video.config:RUNNER_LTX_T2V_2B_OPTIMIZED" +"ltx-video-t2v-2b-taehv" = "ltx_video.config:RUNNER_LTX_T2V_2B_TAEHV" + +[tool.setuptools.packages.find] +include = ["ltx_video*"] +exclude = ["tests"] + +[tool.uv] +managed = true diff --git a/integrations/ltx_video/tests/benchmark/run_benchmark.py b/integrations/ltx_video/tests/benchmark/run_benchmark.py new file mode 100644 index 00000000..92cb5892 --- /dev/null +++ b/integrations/ltx_video/tests/benchmark/run_benchmark.py @@ -0,0 +1,148 @@ +"""DiT runtime benchmark: baseline vs streaming vs optimized.""" + +from __future__ import annotations + +import argparse +import json +import statistics +import time +from typing import Any + +import torch + + +PROMPT = "A coastal road at dusk, waves breaking on rocky cliffs, cinematic wide shot" +N_WARMUP = 3 +N_MEASURE = 10 +STEP_TO_MEASURE = 5 + + +def measure_dit_time(fn, n_warmup: int = N_WARMUP, n_measure: int = N_MEASURE) -> list[float]: + for _ in range(n_warmup): + fn() + torch.cuda.synchronize() + times: list[float] = [] + for _ in range(n_measure): + t0 = time.perf_counter() + fn() + torch.cuda.synchronize() + times.append((time.perf_counter() - t0) * 1000) + return times + + +def run_baseline() -> dict[str, Any]: + from diffusers import LTXPipeline + + pipe = LTXPipeline.from_pretrained( + "Lightricks/LTX-Video", torch_dtype=torch.bfloat16 + ).to("cuda") + torch.backends.cuda.enable_flash_sdp(True) + + pe, pm, npe, npm = pipe.encode_prompt( + prompt=PROMPT, + negative_prompt="worst quality", + device="cuda", + do_classifier_free_guidance=True, + ) + pipe.scheduler.set_timesteps(50, device="cuda") + t = pipe.scheduler.timesteps[STEP_TO_MEASURE] + latents = torch.randn( + 1, + pipe.transformer.config.in_channels, + 4, + 64, + 96, + device="cuda", + dtype=torch.bfloat16, + ) + latent_input = torch.cat([latents] * 2) + enc_hs = torch.cat([npe, pe]) + enc_mask = torch.cat([npm, pm]) + t_batch = t.expand(2) + + def step() -> None: + with torch.no_grad(): + pipe.transformer( + hidden_states=latent_input, + encoder_hidden_states=enc_hs, + encoder_attention_mask=enc_mask, + timestep=t_batch, + ) + + return {"mode": "baseline", "times_ms": measure_dit_time(step)} + + +def run_flashdreams(mode: str) -> dict[str, Any]: + from flashdreams.infra.config import derive_config + from ltx_video.config import PIPELINE_LTX_T2V_2B, PIPELINE_LTX_T2V_2B_OPTIMIZED + from ltx_video.pipeline import LTXVideoStreamingPipeline + + base = PIPELINE_LTX_T2V_2B if mode == "streaming" else PIPELINE_LTX_T2V_2B_OPTIMIZED + pipe = LTXVideoStreamingPipeline(derive_config(base, device="cuda")) + cache = pipe.initialize_cache(text=[PROMPT]) + + for s in range(STEP_TO_MEASURE): + pipe.generate(s, cache, width=768, height=512) + pipe.finalize(s, cache) + cache = pipe.initialize_cache(text=[PROMPT]) + + def step() -> None: + pipe.generate(STEP_TO_MEASURE, cache, width=768, height=512) + + return {"mode": mode, "times_ms": measure_dit_time(step)} + + +def summarise(result: dict[str, Any], baseline_mean: float | None = None) -> dict[str, Any]: + ts = result["times_ms"] + summary: dict[str, Any] = { + "mode": result["mode"], + "dit_runtime_ms_mean": round(statistics.mean(ts), 1), + "dit_runtime_ms_p50": round(statistics.median(ts), 1), + "dit_runtime_ms_p95": round(sorted(ts)[int(len(ts) * 0.95)], 1), + } + if baseline_mean: + summary["speedup_vs_baseline"] = round( + baseline_mean / summary["dit_runtime_ms_mean"], 2 + ) + return summary + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "--mode", + choices=["baseline", "streaming", "optimized", "all"], + default="all", + ) + parser.add_argument("--output", default="benchmark_results.json") + args = parser.parse_args() + + if not torch.cuda.is_available(): + raise SystemExit("CUDA required for benchmark") + + results: dict[str, Any] = {} + + if args.mode in ("baseline", "all"): + r = run_baseline() + results["baseline"] = summarise(r) + print("Baseline:", results["baseline"]) + + bm = results.get("baseline", {}).get("dit_runtime_ms_mean") + + if args.mode in ("streaming", "all"): + r = run_flashdreams("streaming") + results["streaming"] = summarise(r, bm) + print("Streaming:", results["streaming"]) + + if args.mode in ("optimized", "all"): + r = run_flashdreams("optimized") + results["optimized"] = summarise(r, bm) + print("Optimized:", results["optimized"]) + + with open(args.output, "w") as fh: + json.dump(results, fh, indent=2) + print(f"\nResults saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/integrations/ltx_video/tests/parity_check/compare.py b/integrations/ltx_video/tests/parity_check/compare.py new file mode 100644 index 00000000..f3ab8541 --- /dev/null +++ b/integrations/ltx_video/tests/parity_check/compare.py @@ -0,0 +1,87 @@ +"""Frame-level quality comparison between streaming and reference videos.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +import numpy as np + + +def load_video_frames(path: Path) -> np.ndarray: + try: + import mediapy as media + + return media.read_video(str(path)) + except ImportError: + import imageio.v3 as iio + + return iio.imread(str(path)) + + +def compute_psnr(a: np.ndarray, b: np.ndarray) -> float: + mse = np.mean((a.astype(np.float64) - b.astype(np.float64)) ** 2) + if mse == 0: + return float("inf") + return float(10 * np.log10(255.0**2 / mse)) + + +def compute_ssim_frame(a: np.ndarray, b: np.ndarray) -> float: + a = a.astype(np.float64) + b = b.astype(np.float64) + c1 = (0.01 * 255) ** 2 + c2 = (0.03 * 255) ** 2 + mu_a, mu_b = a.mean(), b.mean() + sigma_a, sigma_b = a.var(), b.var() + sigma_ab = ((a - mu_a) * (b - mu_b)).mean() + num = (2 * mu_a * mu_b + c1) * (2 * sigma_ab + c2) + den = (mu_a**2 + mu_b**2 + c1) * (sigma_a + sigma_b + c2) + return float(num / den) if den else 1.0 + + +def compare_videos(streaming: Path, reference: Path) -> dict: + stream_frames = load_video_frames(streaming) + ref_frames = load_video_frames(reference) + n = min(len(stream_frames), len(ref_frames)) + stream_frames = stream_frames[:n] + ref_frames = ref_frames[:n] + + if stream_frames.shape[1:3] != ref_frames.shape[1:3]: + from PIL import Image + + resized = [] + for frame in stream_frames: + img = Image.fromarray(frame).resize( + (ref_frames.shape[2], ref_frames.shape[1]), Image.Resampling.BILINEAR + ) + resized.append(np.array(img)) + stream_frames = np.stack(resized) + + psnrs = [compute_psnr(s, r) for s, r in zip(stream_frames, ref_frames, strict=True)] + ssims = [ + compute_ssim_frame(s, r) for s, r in zip(stream_frames, ref_frames, strict=True) + ] + return { + "frames_compared": n, + "psnr_mean": float(np.mean(psnrs)), + "psnr_min": float(np.min(psnrs)), + "ssim_mean": float(np.mean(ssims)), + "ssim_min": float(np.min(ssims)), + } + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--streaming", type=Path, required=True) + parser.add_argument("--reference", type=Path, required=True) + parser.add_argument("--output", type=Path, default=Path("parity_results.json")) + args = parser.parse_args() + + result = compare_videos(args.streaming, args.reference) + args.output.write_text(json.dumps(result, indent=2)) + print(json.dumps(result, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/integrations/ltx_video/tests/test_optimizations.py b/integrations/ltx_video/tests/test_optimizations.py new file mode 100644 index 00000000..04081c81 --- /dev/null +++ b/integrations/ltx_video/tests/test_optimizations.py @@ -0,0 +1,168 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Isolated GPU tests for each optimization layer (run in order).""" + +from __future__ import annotations + +import time +from dataclasses import dataclass +from typing import Any + +import pytest +import torch +from flashdreams.infra.config import derive_config +from ltx_video.config import PIPELINE_LTX_T2V_2B +from ltx_video.pipeline import LTXVideoStreamingPipeline + +pytestmark = pytest.mark.ci_gpu + +PROMPT = "A coastal road at dusk, waves breaking on rocks" +WIDTH, HEIGHT = 768, 512 + + +def _require_cuda() -> None: + if not torch.cuda.is_available(): + pytest.skip("CUDA required") + + +@dataclass +class OptResult: + name: str + seconds: float + shape: tuple[int, ...] + optimizations: dict[str, bool] + kv_seq_len: int = 0 + + +def _run_one_chunk(cfg_overrides: dict[str, Any]) -> OptResult: + _require_cuda() + torch.cuda.empty_cache() + overrides = dict(cfg_overrides) + label = overrides.pop("_label", "run") + manual_denoise = overrides.pop("manual_denoise", False) + base = derive_config( + PIPELINE_LTX_T2V_2B, + device="cuda:0", + num_inference_steps=8, + manual_denoise=manual_denoise, + **overrides, + ) + pipe = LTXVideoStreamingPipeline(base) + cache = pipe.initialize_cache(text=[PROMPT]) + torch.cuda.synchronize() + t0 = time.perf_counter() + out = pipe.generate(0, cache, width=WIDTH, height=HEIGHT) + pipe.finalize(0, cache) + torch.cuda.synchronize() + elapsed = time.perf_counter() - t0 + kv_len = cache.kv.seq_len if pipe.use_kv_cache else 0 + return OptResult( + name=label, + seconds=elapsed, + shape=tuple(out.shape), + optimizations=pipe.active_optimizations, + kv_seq_len=kv_len, + ) + + +def test_baseline_streaming_pipe() -> None: + """Streaming path: native pipe() per chunk, no manual denoise loop.""" + r = _run_one_chunk({"_label": "streaming_pipe", "manual_denoise": False}) + assert len(r.shape) == 4 and r.shape[0] > 0 + assert r.optimizations["manual_denoise"] is False + + +def test_manual_denoise_only() -> None: + r = _run_one_chunk({"_label": "manual_denoise", "manual_denoise": True}) + assert r.shape[0] > 0 + assert r.optimizations["manual_denoise"] is True + assert r.optimizations["kv_cache"] is False + assert r.optimizations["compile"] is False + + +def test_cuda_graphs_isolated() -> None: + r = _run_one_chunk( + { + "_label": "cuda_graphs", + "manual_denoise": True, + "cuda_graphs": True, + "compile": False, + "kv_cache": False, + } + ) + assert r.shape[0] > 0 + # Graphs may self-disable if capture fails; test must still produce frames. + assert r.optimizations["manual_denoise"] is True + + +def test_compile_isolated() -> None: + r = _run_one_chunk( + { + "_label": "compile", + "manual_denoise": True, + "compile": True, + "cuda_graphs": False, + "kv_cache": False, + } + ) + assert r.shape[0] > 0 + assert r.optimizations["compile"] is True + + +def test_kv_cache_isolated() -> None: + r = _run_one_chunk( + { + "_label": "kv_cache", + "manual_denoise": True, + "kv_cache": True, + "compile": False, + "cuda_graphs": False, + } + ) + assert r.shape[0] > 0 + assert r.optimizations["kv_cache"] is True + + +def test_kv_cache_accumulates_across_ar_steps() -> None: + _require_cuda() + cfg = derive_config( + PIPELINE_LTX_T2V_2B, + device="cuda:0", + num_inference_steps=6, + manual_denoise=True, + kv_cache=True, + compile=False, + cuda_graphs=False, + ) + pipe = LTXVideoStreamingPipeline(cfg) + cache = pipe.initialize_cache(text=[PROMPT]) + pipe.generate(0, cache, width=WIDTH, height=HEIGHT) + pipe.finalize(0, cache) + seq_after_0 = cache.kv.seq_len + pipe.generate(1, cache, width=WIDTH, height=HEIGHT) + pipe.finalize(1, cache) + seq_after_1 = cache.kv.seq_len + assert seq_after_0 > 0 + assert seq_after_1 > seq_after_0 + + +def test_full_optimization_stack() -> None: + from ltx_video.config import PIPELINE_LTX_T2V_2B_OPTIMIZED + + _require_cuda() + cfg = derive_config(PIPELINE_LTX_T2V_2B_OPTIMIZED, device="cuda:0", num_inference_steps=8) + pipe = LTXVideoStreamingPipeline(cfg) + cache = pipe.initialize_cache(text=[PROMPT]) + torch.cuda.synchronize() + t0 = time.perf_counter() + out = pipe.generate(0, cache, width=WIDTH, height=HEIGHT) + pipe.finalize(0, cache) + torch.cuda.synchronize() + elapsed = time.perf_counter() - t0 + opts = pipe.active_optimizations + assert out.shape[0] > 0 + assert opts["manual_denoise"] is True + assert opts["kv_cache"] is True + assert opts["compile"] is True + print(f"full_stack: {elapsed:.2f}s opts={opts}") diff --git a/integrations/ltx_video/tests/test_smoke.py b/integrations/ltx_video/tests/test_smoke.py new file mode 100644 index 00000000..88034b9d --- /dev/null +++ b/integrations/ltx_video/tests/test_smoke.py @@ -0,0 +1,72 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Cheap import-time checks for the ltx_video plugin.""" + +from __future__ import annotations + +import sys +from pathlib import Path +from typing import cast + +import pytest +import tomli as tomllib +from ltx_video import config as config_mod +from ltx_video.config import RUNNER_CONFIGS + +from flashdreams.infra.runner import RunnerConfig + +pytestmark = pytest.mark.ci_cpu + +ENTRY_POINT_GROUP = "flashdreams.runner_configs" + + +def test_runners_dict_is_non_empty() -> None: + assert RUNNER_CONFIGS, "RUNNER_CONFIGS is empty" + + +def test_three_runners_registered() -> None: + assert set(RUNNER_CONFIGS) == { + "ltx-video-t2v-2b", + "ltx-video-t2v-2b-optimized", + "ltx-video-t2v-2b-taehv", + } + + +def test_runner_name_mirrors_pipeline_name() -> None: + drifted = { + slug: (cfg.runner_name, cfg.pipeline.name) + for slug, cfg in RUNNER_CONFIGS.items() + if cfg.runner_name != cfg.pipeline.name + } + assert not drifted, f"runner_name != pipeline.name: {drifted}" + + +def test_entry_points_match_module_literals() -> None: + pyproject = Path(__file__).resolve().parents[1] / "pyproject.toml" + with pyproject.open("rb") as fh: + meta = tomllib.load(fh) + entries = meta["project"]["entry-points"][ENTRY_POINT_GROUP] + declared_slugs = set(entries) + module_slugs = set(RUNNER_CONFIGS) + assert declared_slugs == module_slugs + + for slug, target in entries.items(): + module_name, attr = target.split(":", 1) + assert module_name == "ltx_video.config" + cfg = cast(RunnerConfig, getattr(config_mod, attr)) + assert cfg.runner_name == slug + + +@pytest.mark.skipif( + sys.version_info < (3, 11), + reason="entry-point discovery test relies on importlib.metadata 3.10+ shape", +) +def test_entry_points_discoverable_when_installed() -> None: + from importlib.metadata import entry_points + + eps = entry_points(group=ENTRY_POINT_GROUP) + discovered = {ep.name for ep in eps if ep.value.startswith("ltx_video.")} + if not discovered: + pytest.skip("plugin not installed; run `uv sync --project integrations/ltx_video` first") + assert discovered == set(RUNNER_CONFIGS) diff --git a/pyproject.toml b/pyproject.toml index d383b386..7624524e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ extraPaths = [ "integrations/fastvideo_causal_wan22", "integrations/hy_worldplay", "integrations/lingbot", + "integrations/ltx_video", "integrations/self_forcing", "integrations/wan21", "integrations/wan22", @@ -70,6 +71,7 @@ extra-paths = [ "integrations/fastvideo_causal_wan22", "integrations/hy_worldplay", "integrations/lingbot", + "integrations/ltx_video", "integrations/self_forcing", "integrations/wan21", "integrations/wan22", diff --git a/uv.lock b/uv.lock index 39e436ab..9ed18c6b 100644 --- a/uv.lock +++ b/uv.lock @@ -27,6 +27,7 @@ members = [ "flashdreams-flashvsr", "flashdreams-hy-worldplay", "flashdreams-lingbot", + "flashdreams-ltx-video", "flashdreams-omnidreams", "flashdreams-self-forcing", "flashdreams-wan21", @@ -77,6 +78,26 @@ test = [ { name = "tomli", specifier = ">=2.0" }, ] +[[package]] +name = "accelerate" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch", version = "2.11.0+cu130", source = { registry = "https://download.pytorch.org/whl/cu130" }, marker = "sys_platform == 'win32'" }, + { name = "torch", version = "2.12.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/75/94cd5d389649578aca399e5aa822637eec18319a1dadc400ffe2f9a7493f/accelerate-1.14.0.tar.gz", hash = "sha256:41b9c4377a54e0b460a959b0defa1b736e4ca0a2373252d9a539964c2afe3c8d", size = 412167, upload-time = "2026-06-11T13:45:52.326Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/db/253133d7e7cb40d3af384bb2f5c0b4a2b7fdcffbc95c688cc67a20a3c103/accelerate-1.14.0-py3-none-any.whl", hash = "sha256:e94390c2863b873be18f623f9df48a0d8fe5eff13ea7f1a00092b0a7904888c6", size = 389246, upload-time = "2026-06-11T13:45:50.477Z" }, +] + [[package]] name = "accessible-pygments" version = "0.0.5" @@ -1362,6 +1383,42 @@ requires-dist = [ ] provides-extras = ["dev"] +[[package]] +name = "flashdreams-ltx-video" +version = "0.2.0" +source = { editable = "integrations/ltx_video" } +dependencies = [ + { name = "accelerate" }, + { name = "diffusers" }, + { name = "flashdreams" }, + { name = "mediapy" }, + { name = "sentencepiece" }, + { name = "transformers" }, +] + +[package.optional-dependencies] +dev = [ + { name = "imageio" }, + { name = "imageio-ffmpeg" }, + { name = "pytest" }, + { name = "tomli" }, +] + +[package.metadata] +requires-dist = [ + { name = "accelerate", specifier = ">=0.30.0" }, + { name = "diffusers", specifier = ">=0.31.0" }, + { name = "flashdreams", editable = "flashdreams" }, + { name = "imageio", marker = "extra == 'dev'", specifier = ">=2.34" }, + { name = "imageio-ffmpeg", marker = "extra == 'dev'", specifier = ">=0.5" }, + { name = "mediapy", specifier = ">=1.1" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, + { name = "sentencepiece" }, + { name = "tomli", marker = "extra == 'dev'", specifier = ">=2.0" }, + { name = "transformers", specifier = ">=4.40.0" }, +] +provides-extras = ["dev"] + [[package]] name = "flashdreams-omnidreams" version = "0.1.0" @@ -4653,6 +4710,70 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, ] +[[package]] +name = "sentencepiece" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/15/2e7a025fc62d764b151ae6d0f2a92f8081755ebe8d4a64099accc6f77ba6/sentencepiece-0.2.1.tar.gz", hash = "sha256:8138cec27c2f2282f4a34d9a016e3374cd40e5c6e9cb335063db66a0a3b71fad", size = 3228515, upload-time = "2025-08-12T07:00:51.718Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/31/5b7cccb307b485db1a2372d6d2980b0a65d067f8be5ca943a103b4acd5b3/sentencepiece-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e10fa50bdbaa5e2445dbd387979980d391760faf0ec99a09bd7780ff37eaec44", size = 1942557, upload-time = "2025-08-12T06:59:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/1f/41/0ac923a8e685ad290c5afc8ae55c5844977b8d75076fcc04302b9a324274/sentencepiece-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f27ae6deea72efdb6f361750c92f6c21fd0ad087445082770cc34015213c526", size = 1325384, upload-time = "2025-08-12T06:59:14.334Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ef/3751555d67daf9003384978f169d31c775cb5c7baf28633caaf1eb2b2b4d/sentencepiece-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:60937c959e6f44159fdd9f56fbdd302501f96114a5ba436829496d5f32d8de3f", size = 1253317, upload-time = "2025-08-12T06:59:16.247Z" }, + { url = "https://files.pythonhosted.org/packages/46/a5/742c69b7bd144eb32b6e5fd50dbd8abbbc7a95fce2fe16e50156fa400e3b/sentencepiece-0.2.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8b1d91545578852f128650b8cce4ec20f93d39b378ff554ebe66290f2dabb92", size = 1316379, upload-time = "2025-08-12T06:59:17.825Z" }, + { url = "https://files.pythonhosted.org/packages/c8/89/8deeafbba2871e8fa10f20f17447786f4ac38085925335728d360eaf4cae/sentencepiece-0.2.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27e38eee653abc3d387862e67bc5c8b6f428cd604e688b85d29170b7e725c26c", size = 1387926, upload-time = "2025-08-12T06:59:19.395Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ca/67fe73005f0ab617c6a970b199754e28e524b6873aa7025224fad3cda252/sentencepiece-0.2.1-cp310-cp310-win32.whl", hash = "sha256:251874d720ac7f28024a168501f3c7bb15d1802245f6e66de565f18bbb9b5eaa", size = 999550, upload-time = "2025-08-12T06:59:20.844Z" }, + { url = "https://files.pythonhosted.org/packages/6d/33/dc5b54042050d2dda4229c3ce1f862541c99966390b6aa20f54d520d2dc2/sentencepiece-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:e52144670738b4b477fade6c2a9b6af71a8d0094514c9853ac9f6fc1fcfabae7", size = 1054613, upload-time = "2025-08-12T06:59:22.255Z" }, + { url = "https://files.pythonhosted.org/packages/fa/19/1ea47f46ff97fe04422b78997da1a37cd632f414aae042d27a9009c5b733/sentencepiece-0.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:9076430ac25dfa7147d9d05751dbc66a04bc1aaac371c07f84952979ea59f0d0", size = 1033884, upload-time = "2025-08-12T06:59:24.194Z" }, + { url = "https://files.pythonhosted.org/packages/d8/15/46afbab00733d81788b64be430ca1b93011bb9388527958e26cc31832de5/sentencepiece-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6356d0986b8b8dc351b943150fcd81a1c6e6e4d439772e8584c64230e58ca987", size = 1942560, upload-time = "2025-08-12T06:59:25.82Z" }, + { url = "https://files.pythonhosted.org/packages/fa/79/7c01b8ef98a0567e9d84a4e7a910f8e7074fcbf398a5cd76f93f4b9316f9/sentencepiece-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f8ba89a3acb3dc1ae90f65ec1894b0b9596fdb98ab003ff38e058f898b39bc7", size = 1325385, upload-time = "2025-08-12T06:59:27.722Z" }, + { url = "https://files.pythonhosted.org/packages/bb/88/2b41e07bd24f33dcf2f18ec3b74247aa4af3526bad8907b8727ea3caba03/sentencepiece-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:02593eca45440ef39247cee8c47322a34bdcc1d8ae83ad28ba5a899a2cf8d79a", size = 1253319, upload-time = "2025-08-12T06:59:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a0/54/38a1af0c6210a3c6f95aa46d23d6640636d020fba7135cd0d9a84ada05a7/sentencepiece-0.2.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a0d15781a171d188b661ae4bde1d998c303f6bd8621498c50c671bd45a4798e", size = 1316162, upload-time = "2025-08-12T06:59:30.914Z" }, + { url = "https://files.pythonhosted.org/packages/ef/66/fb191403ade791ad2c3c1e72fe8413e63781b08cfa3aa4c9dfc536d6e795/sentencepiece-0.2.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f5a3e0d9f445ed9d66c0fec47d4b23d12cfc858b407a03c194c1b26c2ac2a63", size = 1387785, upload-time = "2025-08-12T06:59:32.491Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2d/3bd9b08e70067b2124518b308db6a84a4f8901cc8a4317e2e4288cdd9b4d/sentencepiece-0.2.1-cp311-cp311-win32.whl", hash = "sha256:6d297a1748d429ba8534eebe5535448d78b8acc32d00a29b49acf28102eeb094", size = 999555, upload-time = "2025-08-12T06:59:34.475Z" }, + { url = "https://files.pythonhosted.org/packages/32/b8/f709977f5fda195ae1ea24f24e7c581163b6f142b1005bc3d0bbfe4d7082/sentencepiece-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:82d9ead6591015f009cb1be1cb1c015d5e6f04046dbb8c9588b931e869a29728", size = 1054617, upload-time = "2025-08-12T06:59:36.461Z" }, + { url = "https://files.pythonhosted.org/packages/7a/40/a1fc23be23067da0f703709797b464e8a30a1c78cc8a687120cd58d4d509/sentencepiece-0.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:39f8651bd10974eafb9834ce30d9bcf5b73e1fc798a7f7d2528f9820ca86e119", size = 1033877, upload-time = "2025-08-12T06:59:38.391Z" }, + { url = "https://files.pythonhosted.org/packages/4a/be/32ce495aa1d0e0c323dcb1ba87096037358edee539cac5baf8755a6bd396/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57cae326c8727de58c85977b175af132a7138d84c764635d7e71bbee7e774133", size = 1943152, upload-time = "2025-08-12T06:59:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/88/7e/ff23008899a58678e98c6ff592bf4d368eee5a71af96d0df6b38a039dd4f/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:56dd39a3c4d6493db3cdca7e8cc68c6b633f0d4195495cbadfcf5af8a22d05a6", size = 1325651, upload-time = "2025-08-12T06:59:41.536Z" }, + { url = "https://files.pythonhosted.org/packages/19/84/42eb3ce4796777a1b5d3699dfd4dca85113e68b637f194a6c8d786f16a04/sentencepiece-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9381351182ff9888cc80e41c632e7e274b106f450de33d67a9e8f6043da6f76", size = 1253645, upload-time = "2025-08-12T06:59:42.903Z" }, + { url = "https://files.pythonhosted.org/packages/89/fa/d3d5ebcba3cb9e6d3775a096251860c41a6bc53a1b9461151df83fe93255/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99f955df238021bf11f0fc37cdb54fd5e5b5f7fd30ecc3d93fb48b6815437167", size = 1316273, upload-time = "2025-08-12T06:59:44.476Z" }, + { url = "https://files.pythonhosted.org/packages/04/88/14f2f4a2b922d8b39be45bf63d79e6cd3a9b2f248b2fcb98a69b12af12f5/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cdfecef430d985f1c2bcbfff3defd1d95dae876fbd0173376012d2d7d24044b", size = 1387881, upload-time = "2025-08-12T06:59:46.09Z" }, + { url = "https://files.pythonhosted.org/packages/fd/b8/903e5ccb77b4ef140605d5d71b4f9e0ad95d456d6184688073ed11712809/sentencepiece-0.2.1-cp312-cp312-win32.whl", hash = "sha256:a483fd29a34c3e34c39ac5556b0a90942bec253d260235729e50976f5dba1068", size = 999540, upload-time = "2025-08-12T06:59:48.023Z" }, + { url = "https://files.pythonhosted.org/packages/2d/81/92df5673c067148c2545b1bfe49adfd775bcc3a169a047f5a0e6575ddaca/sentencepiece-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:4cdc7c36234fda305e85c32949c5211faaf8dd886096c7cea289ddc12a2d02de", size = 1054671, upload-time = "2025-08-12T06:59:49.895Z" }, + { url = "https://files.pythonhosted.org/packages/fe/02/c5e3bc518655d714622bec87d83db9cdba1cd0619a4a04e2109751c4f47f/sentencepiece-0.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:daeb5e9e9fcad012324807856113708614d534f596d5008638eb9b40112cd9e4", size = 1033923, upload-time = "2025-08-12T06:59:51.952Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4a/85fbe1706d4d04a7e826b53f327c4b80f849cf1c7b7c5e31a20a97d8f28b/sentencepiece-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dcd8161eee7b41aae57ded06272905dbd680a0a04b91edd0f64790c796b2f706", size = 1943150, upload-time = "2025-08-12T06:59:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/c2/83/4cfb393e287509fc2155480b9d184706ef8d9fa8cbf5505d02a5792bf220/sentencepiece-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c6c8f42949f419ff8c7e9960dbadcfbc982d7b5efc2f6748210d3dd53a7de062", size = 1325651, upload-time = "2025-08-12T06:59:55.073Z" }, + { url = "https://files.pythonhosted.org/packages/8d/de/5a007fb53b1ab0aafc69d11a5a3dd72a289d5a3e78dcf2c3a3d9b14ffe93/sentencepiece-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:097f3394e99456e9e4efba1737c3749d7e23563dd1588ce71a3d007f25475fff", size = 1253641, upload-time = "2025-08-12T06:59:56.562Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d2/f552be5928105588f4f4d66ee37dd4c61460d8097e62d0e2e0eec41bc61d/sentencepiece-0.2.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7b670879c370d350557edabadbad1f6561a9e6968126e6debca4029e5547820", size = 1316271, upload-time = "2025-08-12T06:59:58.109Z" }, + { url = "https://files.pythonhosted.org/packages/96/df/0cfe748ace5485be740fed9476dee7877f109da32ed0d280312c94ec259f/sentencepiece-0.2.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7f0fd2f2693309e6628aeeb2e2faf6edd221134dfccac3308ca0de01f8dab47", size = 1387882, upload-time = "2025-08-12T07:00:00.701Z" }, + { url = "https://files.pythonhosted.org/packages/ac/dd/f7774d42a881ced8e1739f393ab1e82ece39fc9abd4779e28050c2e975b5/sentencepiece-0.2.1-cp313-cp313-win32.whl", hash = "sha256:92b3816aa2339355fda2c8c4e021a5de92180b00aaccaf5e2808972e77a4b22f", size = 999541, upload-time = "2025-08-12T07:00:02.709Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e9/932b9eae6fd7019548321eee1ab8d5e3b3d1294df9d9a0c9ac517c7b636d/sentencepiece-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:10ed3dab2044c47f7a2e7b4969b0c430420cdd45735d78c8f853191fa0e3148b", size = 1054669, upload-time = "2025-08-12T07:00:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/c9/3a/76488a00ea7d6931689cda28726a1447d66bf1a4837943489314593d5596/sentencepiece-0.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac650534e2251083c5f75dde4ff28896ce7c8904133dc8fef42780f4d5588fcd", size = 1033922, upload-time = "2025-08-12T07:00:06.496Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b6/08fe2ce819e02ccb0296f4843e3f195764ce9829cbda61b7513f29b95718/sentencepiece-0.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8dd4b477a7b069648d19363aad0cab9bad2f4e83b2d179be668efa672500dc94", size = 1946052, upload-time = "2025-08-12T07:00:08.136Z" }, + { url = "https://files.pythonhosted.org/packages/ab/d9/1ea0e740591ff4c6fc2b6eb1d7510d02f3fb885093f19b2f3abd1363b402/sentencepiece-0.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0c0f672da370cc490e4c59d89e12289778310a0e71d176c541e4834759e1ae07", size = 1327408, upload-time = "2025-08-12T07:00:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/99/7e/1fb26e8a21613f6200e1ab88824d5d203714162cf2883248b517deb500b7/sentencepiece-0.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ad8493bea8432dae8d6830365352350f3b4144415a1d09c4c8cb8d30cf3b6c3c", size = 1254857, upload-time = "2025-08-12T07:00:11.021Z" }, + { url = "https://files.pythonhosted.org/packages/bc/85/c72fd1f3c7a6010544d6ae07f8ddb38b5e2a7e33bd4318f87266c0bbafbf/sentencepiece-0.2.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b81a24733726e3678d2db63619acc5a8dccd074f7aa7a54ecd5ca33ca6d2d596", size = 1315722, upload-time = "2025-08-12T07:00:12.989Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e8/661e5bd82a8aa641fd6c1020bd0e890ef73230a2b7215ddf9c8cd8e941c2/sentencepiece-0.2.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a81799d0a68d618e89063fb423c3001a034c893069135ffe51fee439ae474d6", size = 1387452, upload-time = "2025-08-12T07:00:15.088Z" }, + { url = "https://files.pythonhosted.org/packages/99/5e/ae66c361023a470afcbc1fbb8da722c72ea678a2fcd9a18f1a12598c7501/sentencepiece-0.2.1-cp313-cp313t-win32.whl", hash = "sha256:89a3ea015517c42c0341d0d962f3e6aaf2cf10d71b1932d475c44ba48d00aa2b", size = 1002501, upload-time = "2025-08-12T07:00:16.966Z" }, + { url = "https://files.pythonhosted.org/packages/c1/03/d332828c4ff764e16c1b56c2c8f9a33488bbe796b53fb6b9c4205ddbf167/sentencepiece-0.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:33f068c9382dc2e7c228eedfd8163b52baa86bb92f50d0488bf2b7da7032e484", size = 1057555, upload-time = "2025-08-12T07:00:18.573Z" }, + { url = "https://files.pythonhosted.org/packages/88/14/5aee0bf0864df9bd82bd59e7711362908e4935e3f9cdc1f57246b5d5c9b9/sentencepiece-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:b3616ad246f360e52c85781e47682d31abfb6554c779e42b65333d4b5f44ecc0", size = 1036042, upload-time = "2025-08-12T07:00:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/24/9c/89eb8b2052f720a612478baf11c8227dcf1dc28cd4ea4c0c19506b5af2a2/sentencepiece-0.2.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5d0350b686c320068702116276cfb26c066dc7e65cfef173980b11bb4d606719", size = 1943147, upload-time = "2025-08-12T07:00:21.809Z" }, + { url = "https://files.pythonhosted.org/packages/82/0b/a1432bc87f97c2ace36386ca23e8bd3b91fb40581b5e6148d24b24186419/sentencepiece-0.2.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c7f54a31cde6fa5cb030370566f68152a742f433f8d2be458463d06c208aef33", size = 1325624, upload-time = "2025-08-12T07:00:23.289Z" }, + { url = "https://files.pythonhosted.org/packages/ea/99/bbe054ebb5a5039457c590e0a4156ed073fb0fe9ce4f7523404dd5b37463/sentencepiece-0.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c83b85ab2d6576607f31df77ff86f28182be4a8de6d175d2c33ca609925f5da1", size = 1253670, upload-time = "2025-08-12T07:00:24.69Z" }, + { url = "https://files.pythonhosted.org/packages/19/ad/d5c7075f701bd97971d7c2ac2904f227566f51ef0838dfbdfdccb58cd212/sentencepiece-0.2.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1855f57db07b51fb51ed6c9c452f570624d2b169b36f0f79ef71a6e6c618cd8b", size = 1316247, upload-time = "2025-08-12T07:00:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/fb/03/35fbe5f3d9a7435eebd0b473e09584bd3cc354ce118b960445b060d33781/sentencepiece-0.2.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01e6912125cb45d3792f530a4d38f8e21bf884d6b4d4ade1b2de5cf7a8d2a52b", size = 1387894, upload-time = "2025-08-12T07:00:28.339Z" }, + { url = "https://files.pythonhosted.org/packages/dc/aa/956ef729aafb6c8f9c443104c9636489093bb5c61d6b90fc27aa1a865574/sentencepiece-0.2.1-cp314-cp314-win32.whl", hash = "sha256:c415c9de1447e0a74ae3fdb2e52f967cb544113a3a5ce3a194df185cbc1f962f", size = 1096698, upload-time = "2025-08-12T07:00:29.764Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/fe400d8836952cc535c81a0ce47dc6875160e5fedb71d2d9ff0e9894c2a6/sentencepiece-0.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:881b2e44b14fc19feade3cbed314be37de639fc415375cefaa5bc81a4be137fd", size = 1155115, upload-time = "2025-08-12T07:00:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/32/89/047921cf70f36c7b6b6390876b2399b3633ab73b8d0cb857e5a964238941/sentencepiece-0.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:2005242a16d2dc3ac5fe18aa7667549134d37854823df4c4db244752453b78a8", size = 1133890, upload-time = "2025-08-12T07:00:34.763Z" }, + { url = "https://files.pythonhosted.org/packages/a1/11/5b414b9fae6255b5fb1e22e2ed3dc3a72d3a694e5703910e640ac78346bb/sentencepiece-0.2.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a19adcec27c524cb7069a1c741060add95f942d1cbf7ad0d104dffa0a7d28a2b", size = 1946081, upload-time = "2025-08-12T07:00:36.97Z" }, + { url = "https://files.pythonhosted.org/packages/77/eb/7a5682bb25824db8545f8e5662e7f3e32d72a508fdce086029d89695106b/sentencepiece-0.2.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e37e4b4c4a11662b5db521def4e44d4d30ae69a1743241412a93ae40fdcab4bb", size = 1327406, upload-time = "2025-08-12T07:00:38.669Z" }, + { url = "https://files.pythonhosted.org/packages/03/b0/811dae8fb9f2784e138785d481469788f2e0d0c109c5737372454415f55f/sentencepiece-0.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:477c81505db072b3ab627e7eab972ea1025331bd3a92bacbf798df2b75ea86ec", size = 1254846, upload-time = "2025-08-12T07:00:40.611Z" }, + { url = "https://files.pythonhosted.org/packages/ef/23/195b2e7ec85ebb6a547969f60b723c7aca5a75800ece6cc3f41da872d14e/sentencepiece-0.2.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:010f025a544ef770bb395091d57cb94deb9652d8972e0d09f71d85d5a0816c8c", size = 1315721, upload-time = "2025-08-12T07:00:42.914Z" }, + { url = "https://files.pythonhosted.org/packages/7e/aa/553dbe4178b5f23eb28e59393dddd64186178b56b81d9b8d5c3ff1c28395/sentencepiece-0.2.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:733e59ff1794d26db706cd41fc2d7ca5f6c64a820709cb801dc0ea31780d64ab", size = 1387458, upload-time = "2025-08-12T07:00:44.56Z" }, + { url = "https://files.pythonhosted.org/packages/66/7c/08ff0012507297a4dd74a5420fdc0eb9e3e80f4e88cab1538d7f28db303d/sentencepiece-0.2.1-cp314-cp314t-win32.whl", hash = "sha256:d3233770f78e637dc8b1fda2cd7c3b99ec77e7505041934188a4e7fe751de3b0", size = 1099765, upload-time = "2025-08-12T07:00:46.058Z" }, + { url = "https://files.pythonhosted.org/packages/91/d5/2a69e1ce15881beb9ddfc7e3f998322f5cedcd5e4d244cb74dade9441663/sentencepiece-0.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e4366c97b68218fd30ea72d70c525e6e78a6c0a88650f57ac4c43c63b234a9d", size = 1157807, upload-time = "2025-08-12T07:00:47.673Z" }, + { url = "https://files.pythonhosted.org/packages/f3/16/54f611fcfc2d1c46cbe3ec4169780b2cfa7cf63708ef2b71611136db7513/sentencepiece-0.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:105e36e75cbac1292642045458e8da677b2342dcd33df503e640f0b457cb6751", size = 1136264, upload-time = "2025-08-12T07:00:49.485Z" }, +] + [[package]] name = "setuptools" version = "81.0.0"