Spaces:
Running on Zero
Running on Zero
multimodalart HF Staff
Size @spaces.GPU duration to the measured ~15.5s GPU time (was 300s)
9d56281 verified Download app.py from hugging-apps/distillalign: direct link, hf CLI and curl.
- Browser
- Download file 17.9 kB
-
https://huggingface.co/spaces/hugging-apps/distillalign/resolve/main/app.py
- Command line
-
hf download hf://spaces/hugging-apps/distillalign/app.py
-
curl -L -o app.py https://huggingface.co/spaces/hugging-apps/distillalign/resolve/main/app.py
17.9 kB
| import os | |
| # Must be set before torch initialises its caching allocator: the causal VAE | |
| # decode of 81 frames at 480x832 fragments the pool badly otherwise. | |
| os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") | |
| import spaces # noqa: F401 -- MUST come before any torch / CUDA-touching import | |
| import random | |
| import tempfile | |
| import time | |
| import uuid | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| import gradio as gr | |
| from omegaconf import OmegaConf | |
| from huggingface_hub import snapshot_download | |
| import av | |
| from demo_utils.constant import ZERO_VAE_CACHE | |
| from demo_utils.vae_block3 import VAEDecoderWrapper | |
| from pipeline import CausalInferencePipeline | |
| from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder | |
| # --------------------------------------------------------------------------- | |
| # Checkpoints | |
| # --------------------------------------------------------------------------- | |
| BASE_MODEL_ID = "Wan-AI/Wan2.1-T2V-1.3B" | |
| STUDENT_MODEL_ID = "LiJiaxing/DistillAlign" | |
| STUDENT_CKPT = "checkpoints/distillalign_distill_1p3b_teacher.pt" | |
| MODEL_ROOT = "wan_models" | |
| MODEL_NAME = "Wan2.1-T2V-1.3B" | |
| MODEL_DIR = f"{MODEL_ROOT}/{MODEL_NAME}" | |
| torch.backends.cuda.matmul.allow_tf32 = True | |
| torch.backends.cudnn.allow_tf32 = True | |
| torch.set_grad_enabled(False) | |
| print("== downloading Wan2.1-T2V-1.3B VAE + UMT5-XXL text encoder ==", flush=True) | |
| snapshot_download( | |
| repo_id=BASE_MODEL_ID, | |
| local_dir=MODEL_DIR, | |
| repo_type="model", | |
| allow_patterns=[ | |
| "Wan2.1_VAE.pth", | |
| "models_t5_umt5-xxl-enc-bf16.pth", | |
| # CausalWanModel.from_pretrained() builds the backbone from this file's | |
| # weights before the DistillAlign student state dict replaces them, so | |
| # the base transformer has to be on disk even though it is discarded. | |
| "diffusion_pytorch_model.safetensors", | |
| "google/umt5-xxl/*", | |
| "config.json", | |
| ], | |
| ) | |
| print("== downloading the DistillAlign 1.3B distilled student ==", flush=True) | |
| snapshot_download( | |
| repo_id=STUDENT_MODEL_ID, | |
| local_dir=".", | |
| repo_type="model", | |
| allow_patterns=[STUDENT_CKPT, "config.json"], | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Config + models (module scope, eagerly on the GPU -- ZeroGPU rule #2) | |
| # --------------------------------------------------------------------------- | |
| config = OmegaConf.merge( | |
| OmegaConf.load("configs/default_config.yaml"), | |
| OmegaConf.load("configs/self_forcing_dmd.yaml"), | |
| ) | |
| config.model_kwargs.model_root = MODEL_ROOT | |
| config.model_kwargs.model_name = MODEL_NAME | |
| def _load_checkpoint(path: Path, state_key: str = "auto"): | |
| """Mirror of ``distribution_eval.sampling._load_checkpoint`` (Apache-2.0).""" | |
| try: | |
| obj = torch.load(path, mmap=True, map_location="cpu", weights_only=True) | |
| except TypeError: | |
| obj = torch.load(path, map_location="cpu", weights_only=True) | |
| if not isinstance(obj, dict): | |
| raise TypeError(f"checkpoint must contain a state dict, got {type(obj)}") | |
| selected = state_key | |
| if state_key == "auto": | |
| if "generator_ema" in obj: | |
| selected = "generator_ema" | |
| elif "generator" in obj: | |
| selected = "generator" | |
| else: | |
| selected = "root" | |
| state = obj if selected == "root" else obj.get(selected) | |
| if not isinstance(state, dict): | |
| raise KeyError(f"checkpoint has no state dict named {selected!r}") | |
| cleaned = {} | |
| for key, value in state.items(): | |
| if key.startswith("model._fsdp_wrapped_module."): | |
| key = key.replace("model._fsdp_wrapped_module.", "model.", 1) | |
| elif key.startswith("_fsdp_wrapped_module."): | |
| key = key.replace("_fsdp_wrapped_module.", "", 1) | |
| cleaned[key] = value | |
| return cleaned, selected | |
| print("== loading UMT5-XXL text encoder ==", flush=True) | |
| text_encoder = WanTextEncoder(model_name=MODEL_NAME, model_root=MODEL_ROOT) | |
| text_encoder.eval().requires_grad_(False) | |
| text_encoder.to(device="cuda", dtype=torch.bfloat16) | |
| print("== loading the causal Wan2.1-1.3B student ==", flush=True) | |
| transformer = WanDiffusionWrapper( | |
| model_name=MODEL_NAME, | |
| model_root=MODEL_ROOT, | |
| timestep_shift=float(config.timestep_shift), | |
| is_causal=True, | |
| ).eval() | |
| _state_dict, _state_key = _load_checkpoint(Path(STUDENT_CKPT), "auto") | |
| print(f"== loading DistillAlign state dict '{_state_key}' ==", flush=True) | |
| transformer.load_state_dict(_state_dict, strict=True) | |
| del _state_dict | |
| transformer.requires_grad_(False) | |
| transformer.to(dtype=torch.bfloat16).to("cuda") | |
| print("== loading streaming (causal) VAE decoder ==", flush=True) | |
| vae = VAEDecoderWrapper().eval() | |
| _vae_sd = torch.load(f"{MODEL_DIR}/Wan2.1_VAE.pth", map_location="cpu", weights_only=True) | |
| vae.load_state_dict({k: v for k, v in _vae_sd.items() if "decoder." in k or "conv2" in k}) | |
| del _vae_sd | |
| vae.requires_grad_(False) | |
| vae.to(dtype=torch.bfloat16).to("cuda") | |
| pipeline = CausalInferencePipeline( | |
| config, | |
| device="cuda", | |
| generator=transformer, | |
| text_encoder=text_encoder, | |
| vae=vae, | |
| ).to(dtype=torch.bfloat16) | |
| LATENT_SHAPE = (1, 21, 16, 60, 104) # 21 latent frames -> 81 pixel frames @ 480x832 | |
| NUM_BLOCKS = LATENT_SHAPE[1] // pipeline.num_frame_per_block | |
| FPS = 16 | |
| torch.cuda.empty_cache() | |
| print( | |
| f"== ready: {NUM_BLOCKS} blocks x {pipeline.num_frame_per_block} latent frames, " | |
| f"denoising steps {pipeline.denoising_step_list.tolist()}, " | |
| f"{torch.cuda.memory_allocated() / 1024 ** 3:.1f} GB resident ==", | |
| flush=True, | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Video encoding | |
| # --------------------------------------------------------------------------- | |
| def frames_to_mp4(frames, filepath, fps=FPS): | |
| """frames: list of HWC uint8 RGB arrays -> h264 mp4.""" | |
| height, width = frames[0].shape[:2] | |
| container = av.open(filepath, mode="w", format="mp4") | |
| stream = container.add_stream("h264", rate=fps) | |
| stream.width = width | |
| stream.height = height | |
| stream.pix_fmt = "yuv420p" | |
| stream.options = {"crf": "18", "preset": "veryfast"} | |
| try: | |
| for frame_np in frames: | |
| frame = av.VideoFrame.from_ndarray(np.ascontiguousarray(frame_np), format="rgb24") | |
| for packet in stream.encode(frame): | |
| container.mux(packet) | |
| for packet in stream.encode(): | |
| container.mux(packet) | |
| finally: | |
| container.close() | |
| return filepath | |
| # --------------------------------------------------------------------------- | |
| # Inference | |
| # --------------------------------------------------------------------------- | |
| # Measured on a ZeroGPU (zero-a10g / RTX PRO 6000) worker: ~15.5 s of GPU time | |
| # end-to-end (7 causal blocks x 4 DMD steps + streaming causal VAE decode + mp4 | |
| # mux), so 20 s is the observed time plus the usual ~15% margin. Keeping this | |
| # tight matters: ZeroGPU compares the requested duration against the visitor's | |
| # remaining quota and a large request also lowers the demo's queue priority. | |
| def generate_video(prompt: str, seed: int = 0) -> tuple[str, str]: | |
| """Generate a 5-second 480x832 video from a text prompt. | |
| Args: | |
| prompt: Text description of the video to generate. | |
| seed: Random seed. Use 0 for a random seed. | |
| Returns: | |
| The path to the generated mp4, plus a short status line. | |
| """ | |
| if not prompt or not prompt.strip(): | |
| raise gr.Error("Please enter a text prompt.") | |
| prompt = prompt.strip() | |
| if seed is None: | |
| seed = 0 | |
| seed = int(seed) | |
| if seed == 0: | |
| seed = random.randint(1, 2**31 - 1) | |
| t0 = time.perf_counter() | |
| device = "cuda" | |
| conditional_dict = text_encoder(text_prompts=[prompt]) | |
| for key, value in conditional_dict.items(): | |
| conditional_dict[key] = value.to(dtype=torch.bfloat16) | |
| rnd = torch.Generator(device=device).manual_seed(seed) | |
| noise = torch.randn(LATENT_SHAPE, device=device, dtype=torch.bfloat16, generator=rnd) | |
| pipeline._initialize_kv_cache(1, torch.bfloat16, device=device) | |
| pipeline._initialize_crossattn_cache(1, torch.bfloat16, device=device) | |
| vae_cache = [c.to(device=device, dtype=torch.bfloat16) for c in ZERO_VAE_CACHE] | |
| denoising_step_list = pipeline.denoising_step_list | |
| all_frames = [] | |
| current_start_frame = 0 | |
| for block_index in range(NUM_BLOCKS): | |
| current_num_frames = pipeline.num_frame_per_block | |
| noisy_input = noise[:, current_start_frame:current_start_frame + current_num_frames] | |
| for step_index, current_timestep in enumerate(denoising_step_list): | |
| timestep = torch.ones( | |
| [1, current_num_frames], device=device, dtype=torch.int64 | |
| ) * current_timestep | |
| _, denoised_pred = pipeline.generator( | |
| noisy_image_or_video=noisy_input, | |
| conditional_dict=conditional_dict, | |
| timestep=timestep, | |
| kv_cache=pipeline.kv_cache1, | |
| crossattn_cache=pipeline.crossattn_cache, | |
| current_start=current_start_frame * pipeline.frame_seq_length, | |
| ) | |
| if step_index < len(denoising_step_list) - 1: | |
| next_timestep = denoising_step_list[step_index + 1] | |
| noisy_input = pipeline.scheduler.add_noise( | |
| denoised_pred.flatten(0, 1), | |
| torch.randn_like(denoised_pred.flatten(0, 1)), | |
| next_timestep * torch.ones( | |
| [1 * current_num_frames], device=device, dtype=torch.long | |
| ), | |
| ).unflatten(0, denoised_pred.shape[:2]) | |
| # Re-run with clean context so the KV cache holds denoised latents for | |
| # the next block (skipped on the last block -- the cache is discarded). | |
| if block_index < NUM_BLOCKS - 1: | |
| pipeline.generator( | |
| noisy_image_or_video=denoised_pred, | |
| conditional_dict=conditional_dict, | |
| timestep=torch.zeros_like(timestep), | |
| kv_cache=pipeline.kv_cache1, | |
| crossattn_cache=pipeline.crossattn_cache, | |
| current_start=current_start_frame * pipeline.frame_seq_length, | |
| ) | |
| # Stream the VAE decode one block at a time to bound peak memory. The | |
| # causal decoder emits 4 pixel frames per latent frame; the very first | |
| # block carries 3 warm-up frames from the causal padding, so drop them | |
| # (9 + 6*12 = 81 pixel frames, matching the reference decode()). | |
| pixels, vae_cache = pipeline.vae(denoised_pred.to(torch.bfloat16), *vae_cache) | |
| if block_index == 0: | |
| pixels = pixels[:, 3:] | |
| pixels = pixels.float() | |
| for frame in pixels[0]: | |
| arr = torch.clamp(frame, -1.0, 1.0).add(1.0).mul(127.5) | |
| all_frames.append(arr.to(torch.uint8).permute(1, 2, 0).cpu().numpy()) | |
| current_start_frame += current_num_frames | |
| print( | |
| f"block {block_index + 1}/{NUM_BLOCKS} decoded, " | |
| f"{len(all_frames)} frames, {time.perf_counter() - t0:.1f}s", | |
| flush=True, | |
| ) | |
| out_path = os.path.join(tempfile.gettempdir(), f"distillalign-{uuid.uuid4().hex}.mp4") | |
| frames_to_mp4(all_frames, out_path, fps=FPS) | |
| elapsed = time.perf_counter() - t0 | |
| status = ( | |
| f"Generated {len(all_frames)} frames at {FPS} fps " | |
| f"({len(all_frames) / FPS:.1f}s of video) in {elapsed:.1f}s. Seed: {seed}." | |
| ) | |
| print(status, flush=True) | |
| return out_path, status | |
| # --------------------------------------------------------------------------- | |
| # UI | |
| # --------------------------------------------------------------------------- | |
| EXAMPLE_PROMPTS = [ | |
| "Four teenage girls, with windswept hair and determined expressions, ride powerful waves in " | |
| "frigid, crystal-clear coldwater. Their vibrant surfboards cut through frothy whitecaps as they " | |
| "lean into turns with athletic grace. Dynamic camera angles—low drone sweeps, close-up shoulder " | |
| "shots, and overhead tracking—capture their synchronized movements. Each girl wears snug " | |
| "wetsuits, laughing and shouting as they paddle, carve, and ride the swell. Motion is fluid and " | |
| "energetic, emphasizing their youthful spirit against the vast, chilly ocean backdrop.", | |
| "A young couple stands side by side under a vast, starry night sky, gazing upward in wonder as " | |
| "distant stars flicker with soft, ethereal light. Their faces glow with awe, hands lightly " | |
| "touching, shoulders relaxed. The camera slowly pans around them, tilting upward to capture the " | |
| "infinite cosmos, then gently zooms in on their eyes reflecting the stars. Shot in cinematic 4K " | |
| "HD, with deep blues and warm golden hues, the scene pulses with quiet romance and cosmic wonder " | |
| "as gentle wind rustles their hair.", | |
| "A cinematic, slow-motion sequence capturing every chess move in vivid detail, filmed in 16:9 " | |
| "aspect ratio with dynamic camera tracking following each piece's movement across the board. The " | |
| "chess pieces are intricately designed, with dramatic lighting casting sharp shadows. The camera " | |
| "pans smoothly between moves, zooms in for critical captures, and tilts slightly to emphasize " | |
| "strategic tension. Each piece glides with purpose — pawns advance, knights leap, bishops slash " | |
| "diagonally — all rendered with fluid motion and cinematic grace.", | |
| "Hyper realistic cinematic shot of vibrant orange fish gracefully leaping and floating on " | |
| "shimmering water surface, capturing dynamic ripples and vivid reflections. Fish vary in size, " | |
| "displaying lively expressions as they dart through the frame. Camera glides smoothly at low " | |
| "angle, panning across the water's surface to emphasize motion and depth. Rich, saturated colors " | |
| "contrast with soft ambient lighting. Immersive and fluid, evoking a dreamlike aquatic ballet " | |
| "with natural, organic movement.", | |
| "A man with long, flowing curly black hair stands alone on a windswept beach cliff, gazing at " | |
| "the sun setting over the endless ocean — captured from behind in a stunning anime-style digital " | |
| "illustration by Makoto Shinkai. His hollow cheeks and contemplative posture evoke melancholy " | |
| "beauty, as if frozen in his last moment of solitude. The scene glows with warm, cinematic hues, " | |
| "enhanced by soft atmospheric lighting and gentle waves crashing below. Camera slowly pans right, " | |
| "then tilts up to frame the sky, blending romanticism with aestheticism. Motion: slow, " | |
| "reflective, emotionally resonant.", | |
| "A dynamic slow-motion shot of a figure skater gliding across an ice rink, arms elegantly " | |
| "extended, showcasing fluid spins and graceful landings. His athletic build, focused expression, " | |
| "and crisp costume details are highlighted under bright arena lights. Camera circles him " | |
| "mid-spin, then tracks his glide with a low-angle follow shot. Emphasize the shimmering ice, " | |
| "crisp blades, and swirling motion trails. Cinematic, high-definition, with dramatic lighting and " | |
| "smooth camera transitions capturing the intensity and artistry of competitive figure skating.", | |
| ] | |
| CSS = """ | |
| #col-container { max-width: 1100px; margin: 0 auto; } | |
| .dark .gradio-container { color: var(--body-text-color); } | |
| """ | |
| with gr.Blocks(title="DistillAlign", theme=gr.themes.Citrus(), css=CSS) as demo: | |
| gr.Markdown( | |
| """ | |
| # 🎬 DistillAlign — Autoregressive Video Distillation | |
| **Coordinating Mode Covering and Mode Seeking in Autoregressive Video Distillation** | |
| A **Wan2.1-T2V-1.3B** student distilled for few-step *causal* (autoregressive) text-to-video | |
| generation. It samples 21 latent frames in 7 causal blocks with a 4-step DMD schedule | |
| (`1000 → 750 → 500 → 250`) and a KV cache, then streams them through the causal Wan VAE — | |
| **81 frames / 480×832 / 16 fps ≈ 5 s** in a single forward pass per block. | |
| ⏱️ Generation takes roughly a minute on an RTX PRO 6000 (ZeroGPU). Pick one of the example | |
| prompts below or write your own. | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| prompt = gr.Textbox( | |
| label="Prompt", | |
| placeholder="Describe the video you want to generate...", | |
| lines=6, | |
| max_lines=10, | |
| ) | |
| generate_btn = gr.Button("Generate video", variant="primary", size="lg") | |
| with gr.Accordion("Advanced options", open=False): | |
| seed = gr.Number( | |
| label="Seed (0 = random)", value=0, precision=0, minimum=0 | |
| ) | |
| with gr.Column(scale=1): | |
| video = gr.Video(label="Generated video", autoplay=True, height=420) | |
| status = gr.Markdown() | |
| generate_btn.click( | |
| fn=generate_video, | |
| inputs=[prompt, seed], | |
| outputs=[video, status], | |
| api_name="generate", | |
| ) | |
| gr.Examples( | |
| examples=[[p] for p in EXAMPLE_PROMPTS], | |
| inputs=[prompt], | |
| outputs=[video, status], | |
| fn=generate_video, | |
| cache_examples=True, | |
| cache_mode="lazy", | |
| label="Example prompts from the DistillAlign paper", | |
| ) | |
| gr.Markdown( | |
| """ | |
| --- | |
| **Model**: [LiJiaxing/DistillAlign](https://huggingface.co/LiJiaxing/DistillAlign) · | |
| **Code**: [LiJiaxing0213/DistillAlign](https://github.com/LiJiaxing0213/DistillAlign) · | |
| **Paper**: [arXiv 2607.26811](https://huggingface.co/papers/2607.26811) | |
| Built on Wan2.1-T2V-1.3B ([Wan-AI](https://huggingface.co/Wan-AI/Wan2.1-T2V-1.3B)). | |
| The example prompts above are the authors' own evaluation prompts | |
| (`prompts/distribution_eval_16.txt`, Apache-2.0). | |
| """ | |
| ) | |
| if __name__ == "__main__": | |
| demo.queue().launch(mcp_server=True) | |