[docs] LTX-2.3 distilled i2v example with compile + timing breakdown - #1430
Conversation
Add `examples/inference/basic/basic_ltx2_3_distilled_i2v.py`: a single-GPU LTX-2.3 distilled image-to-video example with torch.compile fully enabled, two warmup runs to settle Inductor's per-shape autotune, two measured runs, and a per-stage timing breakdown. Fills a gap on `main`: the existing `basic_ltx2*.py` examples are t2v-only and target LTX-2.0 distilled. After PR hao-ai-lab#1397 merged LTX-2.3, there was no copy-paste example for the LTX-2.3 i2v path with compile + benchmark plumbing wired in. The script reads the conditioning image from `LTX23_I2V_IMAGE` (errors out with a helpful message if unset) and uses a generic fashion-runway prompt that the user can override via `LTX23_I2V_PROMPT`. Defaults match the production recipe documented in the docstring: 8 denoise + 3 refine steps, CFG=1, 832x1280 portrait, 121 frames @ 24fps. Includes a comment calling out the Blackwell `shape_padding=False` requirement and the `env -u LD_LIBRARY_PATH` launch tip from prior experience on GB200.
There was a problem hiding this comment.
Code Review
This pull request introduces a new example script, basic_ltx2_3_distilled_i2v.py, which demonstrates running the LTX-2.3 distilled image-to-video model with torch.compile enabled and provides a detailed per-stage timing breakdown. Feedback on the changes points out a potential issue with the cleanup of warmup video files: since the generator automatically appends numeric suffixes to filenames if they already exist, hardcoded cleanup paths might fail to delete the actual generated files. It is recommended to capture and use the actual video paths returned by the generator for a robust cleanup.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| for w in range(warmup_runs): | ||
| t0 = time.perf_counter() | ||
| print(f"\n[warmup {w + 1}/{warmup_runs}] compiling + generating…") | ||
| generator.generate_video( | ||
| output_path=str(OUTPUT_DIR / f"_warmup_{w + 1}.mp4"), | ||
| seed=7, | ||
| **common_kwargs, | ||
| ) | ||
| dt = time.perf_counter() - t0 | ||
| warmup_secs.append(dt) | ||
| print(f"[warmup {w + 1}/{warmup_runs}] wall={dt:.1f}s") | ||
|
|
||
| # Cleanup warmup artifacts so the user only sees measured outputs. | ||
| for w in range(warmup_runs): | ||
| (OUTPUT_DIR / f"_warmup_{w + 1}.mp4").unlink(missing_ok=True) |
There was a problem hiding this comment.
In VideoGenerator, the _prepare_output_path method automatically appends a numeric suffix (e.g., _1, _2) to the output filename if a file with the same name already exists in the target directory. If a previous run of this script was interrupted or if the warmup files were not cleaned up, _warmup_1.mp4 might already exist. Consequently, the generator will write the new warmup video to _warmup_1_1.mp4, but the cleanup loop will only attempt to delete _warmup_1.mp4, leaving the newly generated warmup file behind. To prevent this and ensure robust cleanup, capture the returned result from generate_video and use the actual video_path returned by the generator to perform the cleanup.
| for w in range(warmup_runs): | |
| t0 = time.perf_counter() | |
| print(f"\n[warmup {w + 1}/{warmup_runs}] compiling + generating…") | |
| generator.generate_video( | |
| output_path=str(OUTPUT_DIR / f"_warmup_{w + 1}.mp4"), | |
| seed=7, | |
| **common_kwargs, | |
| ) | |
| dt = time.perf_counter() - t0 | |
| warmup_secs.append(dt) | |
| print(f"[warmup {w + 1}/{warmup_runs}] wall={dt:.1f}s") | |
| # Cleanup warmup artifacts so the user only sees measured outputs. | |
| for w in range(warmup_runs): | |
| (OUTPUT_DIR / f"_warmup_{w + 1}.mp4").unlink(missing_ok=True) | |
| warmup_paths = [] | |
| for w in range(warmup_runs): | |
| t0 = time.perf_counter() | |
| print(f"\n[warmup {w + 1}/{warmup_runs}] compiling + generating…") | |
| result = generator.generate_video( | |
| output_path=str(OUTPUT_DIR / f"_warmup_{w + 1}.mp4"), | |
| seed=7, | |
| **common_kwargs, | |
| ) | |
| if isinstance(result, dict) and result.get("video_path"): | |
| warmup_paths.append(result["video_path"]) | |
| dt = time.perf_counter() - t0 | |
| warmup_secs.append(dt) | |
| print(f"[warmup {w + 1}/{warmup_runs}] wall={dt:.1f}s") | |
| # Cleanup warmup artifacts so the user only sees measured outputs. | |
| for path in warmup_paths: | |
| Path(path).unlink(missing_ok=True) |
Merge ProtectionsYour pull request matches the following merge protections and will not be merged until they are valid. 🟢 PR merge requirementsWonderful, this rule succeeded.
|
a50bd67 to
4eee71f
Compare
|
4eee71f to
a50bd67
Compare
`PipelineConfig.from_pretrained(model_root)` instantiates the registered pipeline-config subclass with `model_path` threaded into the constructor, which carries model-specific defaults (notably VAE precision / decoder configuration) that the generic `PipelineConfig()` path lacks. Mirrors the existing `basic_ltx2_distilled_fast_profile.py` pattern. Validated end-to-end on GB200: e2e drops modestly (5.31s -> 4.98s) from slightly faster DiT denoise + refine timings. The remaining ~0.8s decode-stage gap vs the internal compare_gallery harness is unrelated — that one is from the LTX-2 VAE forward methods running eager on `main` (internal applies `@torch.compile` decorators to `VideoEncoder.forward` / `VideoDecoder.forward`). To be addressed in a separate follow-up PR.
The LTX-2 VAE class declares `_compile_conditions = [_is_ltx2_vae_codec]`
so `composed_pipeline_base._compile_with_conditions` targets just the
`encoder` / `decoder` submodules (leaving the surrounding tiling control
flow eager). Setting `enable_torch_compile_vae=True` lets that path
trigger and brings the decoding stage from ~1.3s to ~0.3s.
Validated end-to-end on a single GB200, cold cache:
warmup wall-times: [2588.3, 4.1]
measured e2e (n=2): [3.97, 3.74] -> avg 3.85s
stage breakdown: denoise 1.244s, refine 1.646s, decode 0.275s,
audio 0.124s, prompt_encoding 0.046s
stage_sum_avg 3.831s ~ e2e 3.85s (no hidden overhead)
This matches the per-stage profile we get from the internal benchmark
harness (which uses an older unconditional decorator approach on the
same VAE forward methods) within measurement variance.
|
E2E validated on a single GB200, fork @
Marking ready-for-review. |
|
The pre-commit workflow's pull_request event triggers on default types (opened/synchronize/reopened) and was skipped on the original commits because the PR was still in draft. This empty commit re-triggers the synchronize event now that the PR is ready-for-review.
Summary
Adds
examples/inference/basic/basic_ltx2_3_distilled_i2v.py— a single-GPU LTX-2.3 distilled image-to-video example withtorch.compilefully enabled and a per-stage timing breakdown.Why
After #1397 merged LTX-2.3 transformer support into
main, the existing LTX-2 examples leave LTX-2.3 i2v uncovered:basic_ltx2.pybasic_ltx2_distilled.pybasic_ltx2_distilled_fast_profile.pyAnyone trying to run the just-merged LTX-2.3 i2v path on
maincurrently has to stitch together the right preset, refine config, image conditioning kwargs, and compile flags from the codebase. This file is the one-stop copy-paste.What the script does
FastVideo/LTX-2.3-Distilled-Diffusers(registered in [feat] LTX-2.3 transformer support (config-gated extension of LTX-2) #1397).fullgraph=True,max-autotune-no-cudagraphs,dynamic=False.Usage
Hardware notes (in docstring)
env -u LD_LIBRARY_PATH ...to avoid the system-vs-torch cuBLAS mismatch, and_inductor.shape_padding = False(set in the script) to avoid a pad_mm crash in refine.Test plan
py_compile— clean.ruff check— clean.enable_torch_compile=True(drafting this PR so reviewers can comment on style/structure in parallel with a full run; will mark ready-for-review once a clean full run lands).Related