Skip to content

Commit 29cfcd6

Browse files
committed
feat(video): add Wan 2.2 and LTX 2.5 CUDA rendering
1 parent c4f9964 commit 29cfcd6

21 files changed

Lines changed: 1007 additions & 17 deletions

data.reference/media-models.json

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,87 @@
692692
"reviewedAt": "2026-08-30"
693693
}
694694
},
695+
{
696+
"id": "ltx25_cuda_distilled",
697+
"name": "LTX-2.5 CUDA Distilled (joint video + audio, ~72 GB download, streamed)",
698+
"repo": "Lightricks/LTX-2.5",
699+
"revision": "bf86adedf518142442575d1ce2e767b7d01c8c76",
700+
"repoFiles": [
701+
"diffusion_models/ltx-2.5-22b-distilled-transformer-bf16.safetensors",
702+
"text_encoders/gemma4-12b-with-proj-ltx-2.5-bf16.safetensors",
703+
"vae/ltx-2.5-video-vae-bf16.safetensors",
704+
"vae/ltx-2.5-audio-vae-bf16.safetensors",
705+
"model_patches/ltx-2.5-duration-head-bf16.safetensors",
706+
"latent_upscale_models/ltx-2.5-latent-spatial-upscaler-x2-bf16-1.0.safetensors"
707+
],
708+
"runtime": "ltx25_cuda",
709+
"supportedModes": ["text", "image"],
710+
"defaultFrames": 121,
711+
"resolutionStep": 64,
712+
"fpsOptions": [24],
713+
"steps": 8,
714+
"guidance": 1,
715+
"samplerLocked": true,
716+
"samplerNote": "LTX-2.5 Distilled uses the official fixed 8-step, CFG-free schedule.",
717+
"supportsNegativePrompt": false,
718+
"supportsTiling": false,
719+
"supportsDisableAudio": true,
720+
"requiresHfToken": true,
721+
"hardwareRequirements": {
722+
"minMemoryGb": 64,
723+
"minVramGb": 16,
724+
"minCudaComputeCapability": 8
725+
},
726+
"disclosure": {
727+
"modelCardUrl": "https://huggingface.co/Lightricks/LTX-2.5",
728+
"weightsLicense": {
729+
"name": "LTX-2.x Community License",
730+
"url": "https://github.com/Lightricks/LTX-2/blob/main/LICENSE.md"
731+
},
732+
"runtimeLicense": {
733+
"name": "Apache-2.0",
734+
"url": "https://github.com/Lightricks/LTX-2/blob/v1.2.0/LICENSE"
735+
},
736+
"estimatedDownloadGb": 72.1,
737+
"reviewedAt": "2026-08-30"
738+
}
739+
},
740+
{
741+
"id": "wan22_cuda_ti2v_5b",
742+
"name": "Wan 2.2 TI2V 5B CUDA (high quality, ~34 GB download, text-to-video)",
743+
"repo": "Wan-AI/Wan2.2-TI2V-5B-Diffusers",
744+
"revision": "b8fff7315c768468a5333511427288870b2e9635",
745+
"runtime": "wan22_cuda",
746+
"supportedModes": ["text"],
747+
"defaultWidth": 1280,
748+
"defaultHeight": 704,
749+
"resolutionStep": 16,
750+
"defaultFrames": 121,
751+
"frameStride": 4,
752+
"fpsOptions": [24],
753+
"steps": 50,
754+
"guidance": 5,
755+
"supportsNegativePrompt": true,
756+
"supportsTiling": false,
757+
"hardwareRequirements": {
758+
"minMemoryGb": 32,
759+
"minVramGb": 24,
760+
"minCudaComputeCapability": 8
761+
},
762+
"disclosure": {
763+
"modelCardUrl": "https://huggingface.co/Wan-AI/Wan2.2-TI2V-5B-Diffusers",
764+
"weightsLicense": {
765+
"name": "Apache-2.0",
766+
"url": "https://www.apache.org/licenses/LICENSE-2.0"
767+
},
768+
"runtimeLicense": {
769+
"name": "Apache-2.0",
770+
"url": "https://github.com/huggingface/diffusers/blob/main/LICENSE"
771+
},
772+
"estimatedDownloadGb": 34.2,
773+
"reviewedAt": "2026-08-30"
774+
}
775+
},
695776
{
696777
"id": "minimax_h3_cuda",
697778
"name": "MiniMax H3 CUDA int8 (joint video + audio, ~144 GB download, 24 GB VRAM + 96 GB RAM)",

scripts/generate_ltx25_cuda.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
#!/usr/bin/env python3
2+
"""Cache-only LTX-2.5 distilled video+audio runner for NVIDIA CUDA.
3+
4+
The Video Gen UI owns runtime installation and every Hugging Face download.
5+
This helper resolves the pinned split checkpoint only from the local cache,
6+
streams model blocks from disk on consumer GPUs, and writes one MP4.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import argparse
12+
import os
13+
import sys
14+
from pathlib import Path
15+
16+
sys.path.insert(0, str(Path(__file__).resolve().parent))
17+
from _runner_common import emit_runtime_fingerprint, establish_process_group, heartbeat # noqa: E402
18+
19+
20+
MODEL_FILES = {
21+
"transformer": "diffusion_models/ltx-2.5-22b-distilled-transformer-bf16.safetensors",
22+
"text_encoder": "text_encoders/gemma4-12b-with-proj-ltx-2.5-bf16.safetensors",
23+
"video_vae": "vae/ltx-2.5-video-vae-bf16.safetensors",
24+
"audio_vae": "vae/ltx-2.5-audio-vae-bf16.safetensors",
25+
"duration_head": "model_patches/ltx-2.5-duration-head-bf16.safetensors",
26+
"upsampler": "latent_upscale_models/ltx-2.5-latent-spatial-upscaler-x2-bf16-1.0.safetensors",
27+
}
28+
29+
30+
def log(message: str) -> None:
31+
print(message, file=sys.stderr, flush=True)
32+
33+
34+
def parse_args() -> argparse.Namespace:
35+
parser = argparse.ArgumentParser(description=__doc__)
36+
parser.add_argument("--model-repo", required=True)
37+
parser.add_argument("--model-revision", required=True)
38+
parser.add_argument("--repo-file", action="append", default=[])
39+
parser.add_argument("--prompt", required=True)
40+
parser.add_argument("--negative-prompt")
41+
parser.add_argument("--width", type=int, required=True)
42+
parser.add_argument("--height", type=int, required=True)
43+
parser.add_argument("--num-frames", type=int, required=True)
44+
parser.add_argument("--fps", type=float, required=True)
45+
parser.add_argument("--steps", type=int, default=8)
46+
parser.add_argument("--seed", type=int, required=True)
47+
parser.add_argument("--image")
48+
parser.add_argument("--image-strength", type=float, default=1.0)
49+
parser.add_argument("--disable-audio", action="store_true")
50+
parser.add_argument("--output", required=True)
51+
return parser.parse_args()
52+
53+
54+
def validate_args(args: argparse.Namespace) -> None:
55+
missing = sorted(set(MODEL_FILES.values()) - set(args.repo_file))
56+
if missing:
57+
raise SystemExit(f"LTX-2.5 model entry is missing required repo files: {', '.join(missing)}")
58+
if args.width % 64 or args.height % 64:
59+
raise SystemExit("The two-stage LTX-2.5 pipeline requires width and height divisible by 64.")
60+
if args.num_frames < 9 or args.num_frames % 8 != 1:
61+
raise SystemExit("LTX-2.5 num-frames must be at least 9 and satisfy frames % 8 == 1.")
62+
if args.steps != 8:
63+
raise SystemExit("The distilled LTX-2.5 schedule is fixed at 8 steps.")
64+
if not 0 <= args.image_strength <= 1:
65+
raise SystemExit("image-strength must be between 0 and 1.")
66+
67+
68+
def resolve_snapshot(args: argparse.Namespace) -> Path:
69+
from huggingface_hub import snapshot_download
70+
71+
try:
72+
root = snapshot_download(
73+
repo_id=args.model_repo,
74+
revision=args.model_revision,
75+
allow_patterns=args.repo_file,
76+
local_files_only=True,
77+
)
78+
except Exception as exc:
79+
raise RuntimeError(
80+
"The pinned LTX-2.5 files are not complete in the Hugging Face cache. "
81+
"Use Download or Repair on the Video Gen page."
82+
) from exc
83+
snapshot = Path(root)
84+
unresolved = [relative for relative in MODEL_FILES.values() if not (snapshot / relative).is_file()]
85+
if unresolved:
86+
raise RuntimeError(
87+
"The pinned LTX-2.5 snapshot is incomplete: " + ", ".join(unresolved)
88+
)
89+
return snapshot
90+
91+
92+
def main() -> None:
93+
establish_process_group()
94+
args = parse_args()
95+
validate_args(args)
96+
log("STAGE:resolve-cache")
97+
snapshot = resolve_snapshot(args)
98+
99+
# LTX's documented allocator setting reduces fragmentation on cards that
100+
# sit close to the model's supported VRAM floor. It must be set before
101+
# importing torch so CUDA reads it during allocator initialization.
102+
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
103+
import torch
104+
from ltx_core.model.video_vae import AUTO_TILING, get_video_chunks_number
105+
from ltx_core.quantization.fp8_cast import build_policy as build_fp8_cast_policy
106+
from ltx_pipelines.distilled import DistilledPipeline
107+
from ltx_pipelines.utils.args import ImageConditioningInput
108+
from ltx_pipelines.utils.media_io import encode_video
109+
from ltx_pipelines.utils.model_paths import ModelPaths
110+
from ltx_pipelines.utils.types import OffloadMode
111+
112+
if not torch.cuda.is_available():
113+
raise RuntimeError("LTX-2.5 CUDA needs a visible NVIDIA device. Repair the runtime from Video Gen.")
114+
emit_runtime_fingerprint(
115+
"ltx25_cuda",
116+
["torch", "ltx-core", "ltx-pipelines", "transformers", "accelerate", "huggingface-hub"],
117+
)
118+
paths = {key: str(snapshot / relative) for key, relative in MODEL_FILES.items()}
119+
model_paths = ModelPaths.from_split(
120+
transformer_path=paths["transformer"],
121+
text_encoder_path=paths["text_encoder"],
122+
video_vae_path=paths["video_vae"],
123+
audio_vae_path=paths["audio_vae"],
124+
duration_head_path=paths["duration_head"],
125+
)
126+
127+
log("STATUS:LTX-2.5 CUDA disk-streamed loading (FP8 transformer cache)")
128+
log("STAGE:load-pipeline")
129+
with heartbeat("ltx25-cuda-load"):
130+
pipe = DistilledPipeline(
131+
model_paths=model_paths,
132+
spatial_upsampler_path=paths["upsampler"],
133+
loras=[],
134+
device=torch.device("cuda"),
135+
quantization=build_fp8_cast_policy(paths["transformer"]),
136+
# CPU mode pins a model-sized prefetch buffer. The 24 GB VRAM /
137+
# 32 GB system-RAM tier can exhaust that buffer while Gemma is
138+
# resident; DISK is upstream's lowest-memory streaming mode.
139+
offload_mode=OffloadMode.DISK,
140+
)
141+
images = []
142+
if args.image:
143+
images.append(ImageConditioningInput(args.image, 0, args.image_strength))
144+
log("STAGE:inference")
145+
with heartbeat("ltx25-cuda-inference"):
146+
video, audio, resolved_frames, tiling = pipe(
147+
prompt=args.prompt,
148+
seed=args.seed,
149+
height=args.height,
150+
width=args.width,
151+
num_frames=args.num_frames,
152+
frame_rate=args.fps,
153+
images=images,
154+
tiling_config=AUTO_TILING,
155+
)
156+
if args.disable_audio:
157+
audio = None
158+
159+
output = Path(args.output)
160+
output.parent.mkdir(parents=True, exist_ok=True)
161+
log("STAGE:mux")
162+
encode_video(
163+
video=video,
164+
fps=int(args.fps),
165+
audio=audio,
166+
output_path=str(output),
167+
video_chunks_number=int(get_video_chunks_number(resolved_frames, tiling)),
168+
)
169+
if not output.is_file():
170+
raise RuntimeError(f"LTX-2.5 completed but did not write {output}.")
171+
log(f"STATUS:LTX-2.5 saved {output.name} ({resolved_frames} frames)")
172+
173+
174+
if __name__ == "__main__":
175+
main()

scripts/generate_wan22_cuda.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#!/usr/bin/env python3
2+
"""Cache-only Wan 2.2 TI2V 5B text-to-video runner for NVIDIA CUDA."""
3+
4+
from __future__ import annotations
5+
6+
import argparse
7+
import os
8+
import sys
9+
from pathlib import Path
10+
11+
sys.path.insert(0, str(Path(__file__).resolve().parent))
12+
from _runner_common import emit_runtime_fingerprint, establish_process_group, heartbeat # noqa: E402
13+
14+
15+
def log(message: str) -> None:
16+
print(message, file=sys.stderr, flush=True)
17+
18+
19+
def parse_args() -> argparse.Namespace:
20+
parser = argparse.ArgumentParser(description=__doc__)
21+
parser.add_argument("--model-repo", required=True)
22+
parser.add_argument("--model-revision", required=True)
23+
parser.add_argument("--prompt", required=True)
24+
parser.add_argument("--negative-prompt", default="")
25+
parser.add_argument("--width", type=int, required=True)
26+
parser.add_argument("--height", type=int, required=True)
27+
parser.add_argument("--num-frames", type=int, required=True)
28+
parser.add_argument("--fps", type=float, required=True)
29+
parser.add_argument("--steps", type=int, required=True)
30+
parser.add_argument("--guidance", type=float, required=True)
31+
parser.add_argument("--seed", type=int, required=True)
32+
parser.add_argument("--output", required=True)
33+
return parser.parse_args()
34+
35+
36+
def validate_args(args: argparse.Namespace) -> None:
37+
if args.width % 16 or args.height % 16:
38+
raise SystemExit("Wan 2.2 width and height must be divisible by 16.")
39+
if args.num_frames < 5 or (args.num_frames - 1) % 4:
40+
raise SystemExit("Wan 2.2 frame count must satisfy 4n+1.")
41+
if args.steps < 1:
42+
raise SystemExit("Wan 2.2 steps must be positive.")
43+
44+
45+
def main() -> None:
46+
establish_process_group()
47+
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
48+
args = parse_args()
49+
validate_args(args)
50+
from huggingface_hub import snapshot_download
51+
log("STAGE:resolve-cache")
52+
try:
53+
snapshot = snapshot_download(repo_id=args.model_repo, revision=args.model_revision, local_files_only=True)
54+
except Exception as exc:
55+
raise RuntimeError("The pinned Wan 2.2 snapshot is incomplete. Use Download or Repair in Video Gen.") from exc
56+
import torch
57+
from diffusers import AutoencoderKLWan, WanPipeline
58+
from diffusers.utils import export_to_video
59+
if not torch.cuda.is_available():
60+
raise RuntimeError("Wan 2.2 CUDA needs a visible NVIDIA device. Repair the runtime in Video Gen.")
61+
emit_runtime_fingerprint("wan22_cuda", ["torch", "diffusers", "transformers", "accelerate", "huggingface-hub", "hf-xet"])
62+
log("STATUS:Loading Wan 2.2 TI2V 5B with component CPU offload")
63+
log("STAGE:load-pipeline")
64+
with heartbeat("wan22-cuda-load"):
65+
vae = AutoencoderKLWan.from_pretrained(snapshot, subfolder="vae", torch_dtype=torch.float32, local_files_only=True)
66+
pipe = WanPipeline.from_pretrained(snapshot, vae=vae, torch_dtype=torch.bfloat16, local_files_only=True)
67+
# Each component fits independently on a 24 GB card (the largest is
68+
# the ~20 GB transformer). Model-level offload moves each component
69+
# once; sequential offload moved every layer every step and made a
70+
# 3090 needlessly slow without lowering the peak that matters here.
71+
pipe.enable_model_cpu_offload()
72+
pipe.vae.enable_tiling()
73+
generator = torch.Generator(device="cpu").manual_seed(args.seed)
74+
log("STAGE:inference")
75+
with heartbeat("wan22-cuda-inference"):
76+
frames = pipe(prompt=args.prompt, negative_prompt=args.negative_prompt or None, height=args.height, width=args.width, num_frames=args.num_frames, num_inference_steps=args.steps, guidance_scale=args.guidance, generator=generator).frames[0]
77+
output = Path(args.output)
78+
output.parent.mkdir(parents=True, exist_ok=True)
79+
log("STAGE:mux")
80+
export_to_video(frames, str(output), fps=int(args.fps))
81+
if not output.is_file():
82+
raise RuntimeError(f"Wan 2.2 completed but did not write {output}.")
83+
log(f"STATUS:Wan 2.2 saved {output.name} ({len(frames)} frames)")
84+
85+
86+
if __name__ == "__main__":
87+
main()
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# LTX-2.5 CUDA runtime for ~/.portos/ltx-2.5-cuda/.venv.
2+
# Torch, torchaudio and torchvision are installed separately as a matched stack
3+
# from the host-appropriate CUDA index.
4+
5+
ltx-core @ git+https://github.com/Lightricks/LTX-2.git@v1.2.0#subdirectory=packages/ltx-core
6+
ltx-pipelines @ git+https://github.com/Lightricks/LTX-2.git@v1.2.0#subdirectory=packages/ltx-pipelines
7+
transformers>=5.14.1,<5.15
8+
accelerate>=1.14.0
9+
huggingface_hub>=1.27.0
10+
imageio>=2.37.2
11+
imageio-ffmpeg>=0.6.0
12+
pillow>=12.3.0
13+
av>=18.1.0
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
diffusers==0.36.0
2+
transformers>=4.49,<5
3+
accelerate>=1.2,<2
4+
huggingface-hub>=0.27,<2
5+
hf-xet>=1.1,<2
6+
sentencepiece>=0.2,<1
7+
ftfy>=6.3,<7
8+
imageio>=2.36,<3
9+
imageio-ffmpeg>=0.5,<1
10+
av>=14,<17
11+
Pillow>=10,<13

0 commit comments

Comments
 (0)