Instructions to use PrunaAI/PrunaVAED with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use PrunaAI/PrunaVAED with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("PrunaAI/PrunaVAED", torch_dtype=torch.bfloat16, device_map="cuda") prompt = "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k" image = pipe(prompt).images[0] - LTX.io
How to use PrunaAI/PrunaVAED with LTX.io:
# Install the LTX-2 pipelines git clone https://github.com/Lightricks/LTX-2.git cd LTX-2 uv sync --frozen
# Download the weights from this repo, plus the Gemma text encoder hf download PrunaAI/PrunaVAED --local-dir models/PrunaVAED hf download google/gemma-3-12b-it-qat-q4_0-unquantized --local-dir models/gemma-3-12b
# Fast pipeline (distilled model, no distilled LoRA needed) uv run python -m ltx_pipelines.distilled \ --distilled-checkpoint-path models/PrunaVAED/<distilled-checkpoint>.safetensors \ --spatial-upsampler-path models/PrunaVAED/<spatial-upsampler>.safetensors \ --gemma-root models/gemma-3-12b \ --prompt "A beautiful sunset over the ocean" \ --output-path output.mp4 # For image-to-video, add: --image path/to/image.jpg 0 0.8# HQ pipeline (two-stage, higher quality) uv run python -m ltx_pipelines.ti2vid_two_stages_hq \ --checkpoint-path models/PrunaVAED/<checkpoint>.safetensors \ --distilled-lora models/PrunaVAED/<distilled-lora>.safetensors 0.8 \ --spatial-upsampler-path models/PrunaVAED/<spatial-upsampler>.safetensors \ --gemma-root models/gemma-3-12b \ --prompt "A beautiful sunset over the ocean" \ --output-path output.mp4 # For image-to-video, add: --image path/to/image.jpg 0 0.8 - Notebooks
- Google Colab
- Kaggle
Upload demo/demo_distilled_decode.py
Browse files- demo/demo_distilled_decode.py +219 -0
demo/demo_distilled_decode.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Quick demo: generate a short LTX-2.3 video latent, then decode it twice.
|
| 3 |
+
|
| 4 |
+
Compares the stock LTX-2.3 VAE decoder with this repo's pruned decoder
|
| 5 |
+
(PrunaVAED) on the *same* latent. Prints decode time (ms) and writes two mp4s.
|
| 6 |
+
|
| 7 |
+
Requires a CUDA GPU. From the repo root:
|
| 8 |
+
|
| 9 |
+
pip install -r requirements-test.txt
|
| 10 |
+
python tests/test_distilled_decode.py
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import statistics
|
| 16 |
+
import sys
|
| 17 |
+
import time
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
|
| 20 |
+
import imageio.v3 as iio
|
| 21 |
+
import torch
|
| 22 |
+
from diffusers import LTX2LatentUpsamplePipeline, LTX2Pipeline
|
| 23 |
+
from diffusers.models.autoencoders import AutoencoderKLLTX2Video
|
| 24 |
+
from diffusers.pipelines.ltx2.latent_upsampler import LTX2LatentUpsamplerModel
|
| 25 |
+
from diffusers.pipelines.ltx2.utils import (
|
| 26 |
+
DEFAULT_NEGATIVE_PROMPT,
|
| 27 |
+
DISTILLED_SIGMA_VALUES,
|
| 28 |
+
STAGE_2_DISTILLED_SIGMA_VALUES,
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
torch.backends.cuda.enable_cudnn_sdp(False)
|
| 32 |
+
|
| 33 |
+
# ---------------------------------------------------------------------------
|
| 34 |
+
# Settings (edit these if you want)
|
| 35 |
+
# ---------------------------------------------------------------------------
|
| 36 |
+
|
| 37 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 38 |
+
sys.path.insert(0, str(REPO_ROOT))
|
| 39 |
+
|
| 40 |
+
from patch_diffusers import patch_pruna_ltx2_decoder # noqa: E402
|
| 41 |
+
|
| 42 |
+
# Official Diffusers checkpoints for the 2-stage distilled recipe.
|
| 43 |
+
DISTILLED_MODEL = "diffusers/LTX-2.3-Distilled-Diffusers"
|
| 44 |
+
SPATIAL_UPSAMPLER = "dg845/LTX-2.3-Spatial-Upsampler-Diffusers"
|
| 45 |
+
LTX23_VAE = "diffusers/LTX-2.3-Diffusers" # stock decoder (baseline)
|
| 46 |
+
PRUNED_VAE = REPO_ROOT # this repo's vae/ folder
|
| 47 |
+
|
| 48 |
+
# ~720p for 5 s @ 24 fps. Height/width must be multiples of 64 (2-stage).
|
| 49 |
+
HEIGHT, WIDTH, NUM_FRAMES, FPS = 1088, 1920, 121, 24.0
|
| 50 |
+
SEED = 42
|
| 51 |
+
DECODE_WARMUP, DECODE_RUNS = 1, 3 # timing: 1 warm-up + median of 3 runs
|
| 52 |
+
|
| 53 |
+
PROMPT = (
|
| 54 |
+
"The video shows a hockey player in a green jersey and blue helmet skating on the ice with a hockey stick. The player is seen moving around the ice, passing the puck to another player who is also wearing a green jersey and blue helmet. The player in the green jersey is seen skating away from the camera, and then turning around to face the camera. The ice rink is surrounded by boards with advertisements, and there are other players in the background. The player in the green jersey is wearing black gloves and black skates. The player in the green jersey is also seen skating towards the camera and then away from the camera again."
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
DEVICE = "cuda"
|
| 58 |
+
DTYPE = torch.bfloat16
|
| 59 |
+
OUTPUT_DIR = REPO_ROOT / "outputs" / "test_distilled"
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# ---------------------------------------------------------------------------
|
| 63 |
+
# Helpers
|
| 64 |
+
# ---------------------------------------------------------------------------
|
| 65 |
+
|
| 66 |
+
def save_mp4(video: torch.Tensor, path: Path) -> None:
|
| 67 |
+
"""Save a BCTHW tensor in [-1, 1] as an H.264 mp4."""
|
| 68 |
+
frames = video[0].permute(1, 2, 3, 0).clamp(-1, 1)
|
| 69 |
+
frames = ((frames + 1) / 2 * 255).byte().cpu().numpy()
|
| 70 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 71 |
+
iio.imwrite(path, frames, fps=FPS, codec="libx264")
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def load_vae(model_id: str) -> AutoencoderKLLTX2Video:
|
| 75 |
+
"""Load a video VAE decoder and put it on GPU."""
|
| 76 |
+
vae = AutoencoderKLLTX2Video.from_pretrained(
|
| 77 |
+
model_id, subfolder="vae", torch_dtype=DTYPE
|
| 78 |
+
)
|
| 79 |
+
vae = vae.to(DEVICE).eval()
|
| 80 |
+
vae.use_tiling = False
|
| 81 |
+
if hasattr(vae, "disable_tiling"):
|
| 82 |
+
vae.disable_tiling()
|
| 83 |
+
return vae
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
@torch.inference_mode()
|
| 87 |
+
def timed_decode(vae: AutoencoderKLLTX2Video, latent: torch.Tensor) -> tuple[torch.Tensor, float]:
|
| 88 |
+
"""Decode once after warm-up; return (video on CPU, median latency in ms)."""
|
| 89 |
+
latent = latent.to(DEVICE, DTYPE)
|
| 90 |
+
|
| 91 |
+
with torch.autocast(DEVICE, dtype=DTYPE):
|
| 92 |
+
for _ in range(DECODE_WARMUP):
|
| 93 |
+
vae.decode(latent, return_dict=False)
|
| 94 |
+
|
| 95 |
+
times_ms = []
|
| 96 |
+
video = None
|
| 97 |
+
for _ in range(DECODE_RUNS):
|
| 98 |
+
torch.cuda.synchronize()
|
| 99 |
+
t0 = time.perf_counter()
|
| 100 |
+
with torch.autocast(DEVICE, dtype=DTYPE):
|
| 101 |
+
video = vae.decode(latent, return_dict=False)[0]
|
| 102 |
+
torch.cuda.synchronize()
|
| 103 |
+
times_ms.append((time.perf_counter() - t0) * 1000)
|
| 104 |
+
|
| 105 |
+
return video.cpu(), statistics.median(times_ms)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
@torch.inference_mode()
|
| 109 |
+
def generate_latent(prompt: str) -> torch.Tensor:
|
| 110 |
+
"""Run the official 2-stage distilled pipeline and return the final video latent.
|
| 111 |
+
|
| 112 |
+
Same layout as Lightricks DistilledPipeline:
|
| 113 |
+
stage 1 @ half-res → 2× latent upsample → stage 2 @ full-res.
|
| 114 |
+
"""
|
| 115 |
+
half_h, half_w = HEIGHT // 2, WIDTH // 2
|
| 116 |
+
generator = torch.Generator(DEVICE).manual_seed(SEED)
|
| 117 |
+
|
| 118 |
+
pipe = LTX2Pipeline.from_pretrained(DISTILLED_MODEL, torch_dtype=DTYPE)
|
| 119 |
+
pipe.enable_model_cpu_offload(device=DEVICE)
|
| 120 |
+
|
| 121 |
+
# Stage 1 — cheap draft at half resolution.
|
| 122 |
+
print(f"1/3 Stage 1 @ {half_w}×{half_h}")
|
| 123 |
+
video_latent, audio_latent = pipe(
|
| 124 |
+
prompt=prompt,
|
| 125 |
+
negative_prompt=DEFAULT_NEGATIVE_PROMPT,
|
| 126 |
+
height=half_h,
|
| 127 |
+
width=half_w,
|
| 128 |
+
num_frames=NUM_FRAMES,
|
| 129 |
+
frame_rate=FPS,
|
| 130 |
+
num_inference_steps=len(DISTILLED_SIGMA_VALUES),
|
| 131 |
+
sigmas=DISTILLED_SIGMA_VALUES,
|
| 132 |
+
guidance_scale=1.0,
|
| 133 |
+
generator=generator,
|
| 134 |
+
output_type="latent",
|
| 135 |
+
return_dict=False,
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
# Upsample — bring the latent to full resolution before refining.
|
| 139 |
+
print(f"2/3 Upsample → {WIDTH}×{HEIGHT}")
|
| 140 |
+
upsampler = LTX2LatentUpsamplerModel.from_pretrained(
|
| 141 |
+
SPATIAL_UPSAMPLER, subfolder="latent_upsampler", torch_dtype=DTYPE
|
| 142 |
+
)
|
| 143 |
+
upsample_pipe = LTX2LatentUpsamplePipeline(vae=pipe.vae, latent_upsampler=upsampler)
|
| 144 |
+
upsample_pipe.enable_model_cpu_offload(device=DEVICE)
|
| 145 |
+
video_latent = upsample_pipe(
|
| 146 |
+
latents=video_latent[:1],
|
| 147 |
+
height=half_h,
|
| 148 |
+
width=half_w,
|
| 149 |
+
num_frames=NUM_FRAMES,
|
| 150 |
+
output_type="latent",
|
| 151 |
+
return_dict=False,
|
| 152 |
+
)[0]
|
| 153 |
+
del upsample_pipe, upsampler
|
| 154 |
+
torch.cuda.empty_cache()
|
| 155 |
+
|
| 156 |
+
# Stage 2 — refine at full resolution (renoises from STAGE_2 schedule).
|
| 157 |
+
print(f"3/3 Stage 2 @ {WIDTH}×{HEIGHT}")
|
| 158 |
+
video_latent, _ = pipe(
|
| 159 |
+
latents=video_latent,
|
| 160 |
+
audio_latents=audio_latent,
|
| 161 |
+
prompt=prompt,
|
| 162 |
+
negative_prompt=DEFAULT_NEGATIVE_PROMPT,
|
| 163 |
+
height=HEIGHT,
|
| 164 |
+
width=WIDTH,
|
| 165 |
+
num_frames=NUM_FRAMES,
|
| 166 |
+
frame_rate=FPS,
|
| 167 |
+
num_inference_steps=len(STAGE_2_DISTILLED_SIGMA_VALUES),
|
| 168 |
+
noise_scale=STAGE_2_DISTILLED_SIGMA_VALUES[0],
|
| 169 |
+
sigmas=STAGE_2_DISTILLED_SIGMA_VALUES,
|
| 170 |
+
guidance_scale=1.0,
|
| 171 |
+
generator=generator,
|
| 172 |
+
output_type="latent",
|
| 173 |
+
return_dict=False,
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
latent = video_latent.detach().cpu()
|
| 177 |
+
del pipe
|
| 178 |
+
torch.cuda.empty_cache()
|
| 179 |
+
return latent
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def compare_decoders(latent: torch.Tensor, out_dir: Path) -> None:
|
| 183 |
+
"""Decode the same latent with LTX-2.3 and PrunaVAED; print ms and save mp4s."""
|
| 184 |
+
# Needed so Diffusers can build the pruned decoder graph correctly.
|
| 185 |
+
patch_pruna_ltx2_decoder()
|
| 186 |
+
|
| 187 |
+
results = {}
|
| 188 |
+
for name, model_id in (("ltx23", LTX23_VAE), ("prunavaed", str(PRUNED_VAE))):
|
| 189 |
+
print(f"Decoding with {name} …")
|
| 190 |
+
vae = load_vae(model_id)
|
| 191 |
+
video, ms = timed_decode(vae, latent)
|
| 192 |
+
results[name] = ms
|
| 193 |
+
path = out_dir / f"{name}.mp4"
|
| 194 |
+
save_mp4(video, path)
|
| 195 |
+
print(f" {name}: {ms:.1f} ms → {path}")
|
| 196 |
+
del vae, video
|
| 197 |
+
torch.cuda.empty_cache()
|
| 198 |
+
|
| 199 |
+
print(f"Speedup: {results['ltx23'] / results['prunavaed']:.2f}×")
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
# ---------------------------------------------------------------------------
|
| 203 |
+
# Entry point
|
| 204 |
+
# ---------------------------------------------------------------------------
|
| 205 |
+
|
| 206 |
+
def main() -> None:
|
| 207 |
+
if not torch.cuda.is_available():
|
| 208 |
+
raise SystemExit("This demo needs a CUDA GPU.")
|
| 209 |
+
|
| 210 |
+
print(f"Prompt: {PROMPT[:80]}…")
|
| 211 |
+
print(f"Output: {OUTPUT_DIR}")
|
| 212 |
+
|
| 213 |
+
latent = generate_latent(PROMPT)
|
| 214 |
+
print(f"Latent shape: {tuple(latent.shape)}")
|
| 215 |
+
compare_decoders(latent, OUTPUT_DIR)
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
if __name__ == "__main__":
|
| 219 |
+
main()
|