| """FastH3 v1 (VSA) — the 4-step DMD2 distillation of MiniMax-H3, text to video + synchronized audio. |
| |
| `FastVideo/FastVideo-FastH3-4-step-Preview-v1-VSA-DataFree` replaces only the `transformer/` of the MiniMax-H3 release |
| with a data-free DMD2 student. Everything else in the repo (Qwen3-VL conditioner, both autoencoders, both schedulers) |
| is an unmodified copy of the base checkpoint, so it runs on the released `diffusers` modular pipeline — the two |
| differences at inference time are the step count and the **attention backend**. |
| |
| **The sampling contract.** `num_inference_steps` counts sigma *grid points*, and `N` points drive `N - 1` transformer |
| forwards. The checkpoint's own `fastvideo_inference.json` states it exactly: `num_inference_steps: 5`, |
| `transformer_forwards: 4`, `dmd_denoising_steps: [999, 749, 500, 250]`, `guidance_scale: 1.0`. It is fixed here. |
| |
| **VSA is not optional.** That same file records `attention_backend: VIDEO_SPARSE_ATTN_H3`, `vsa_tile_size: 64`, |
| `vsa_sparsity: 0.9`. This student was distilled *under* block-sparse attention and ships 50 trained |
| `attn.to_gate_compress` tensors that only the sparse path reads, so `vsa_h3.py` ports FastVideo's VSA-H3 backend onto |
| `MiniMaxH3Attention` and runs it, on FastVideo's own Triton kernels (vendored under `vsa_kernel/`). The checkpoint's |
| `vsa_kernel: sm100a` is the GB200-only fast path for the same mask semantics; this pool is sm120. |
| |
| **Why the Space is split.** MiniMax-H3 is ~196 GiB in bfloat16 and a ZeroGPU Space is evicted at 150 GB of storage. |
| This half holds the distilled transformer and the two autoencoders (81 GB); the 62.15 GiB Qwen3-VL conditioner runs in |
| `multimodalart/qwen3vl-conditioner`, which this Space calls over the gradio API for every request. FastH3 ships the |
| base release's conditioner verbatim, so that Space encodes this checkpoint exactly. Nothing is quantized anywhere. |
| |
| **Latency instrumentation.** The `@spaces.GPU` function times the pipeline call alone with `time.perf_counter()` and |
| returns `gen_s` next to the video, together with per-forward wall times and the peak CUDA allocation, so the |
| autoresearch loop can see where a request spends its seconds. A per-request id in the report proves each artifact is |
| freshly generated (a cached replay would repeat the id). |
| """ |
|
|
| from __future__ import annotations |
|
|
| import functools |
| import os |
| import tempfile |
| import time |
| import traceback |
| import uuid |
| from functools import cache |
|
|
| |
| |
| import spaces |
| import gradio as gr |
|
|
| MODEL_REPO = os.environ.get("H3_MODEL_REPO", "FastVideo/FastVideo-FastH3-4-step-Preview-v1-VSA-DataFree") |
| BASE_REPO = "MiniMaxAI/MiniMax-H3" |
| CONDITIONER_SPACE = os.environ.get("H3_CONDITIONER", "multimodalart/qwen3vl-conditioner") |
| |
| |
| |
| |
| PLACEMENT = os.environ.get("H3_PLACEMENT", "lazy").lower() |
| |
| |
| ATTENTION = os.environ.get("H3_ATTENTION", "vsa").lower() |
| |
| VSA_SPARSITY = float(os.environ.get("H3_VSA_SPARSITY", "0.9")) |
| |
| GPU_SIZE = os.environ.get("H3_GPU_SIZE", "xlarge") |
|
|
| |
| SIGMA_GRID_POINTS = 5 |
| NUM_FORWARDS = SIGMA_GRID_POINTS - 1 |
|
|
| |
| |
| CANVASES = { |
| |
| "960x544 · 16:9 fast": (544, 960), |
| "1024x576 · 16:9 fast": (576, 1024), |
| "1152x640 · 16:9": (640, 1152), |
| "1280x704 · 16:9": (704, 1280), |
| "1344x768 · 16:9 full": (768, 1344), |
| |
| "544x960 · 9:16 fast": (960, 544), |
| "640x1152 · 9:16": (1152, 640), |
| "768x1344 · 9:16 full": (1344, 768), |
| |
| "544x544 · 1:1 fast": (544, 544), |
| "768x768 · 1:1 full": (768, 768), |
| "1024x1024 · 1:1 max": (1024, 1024), |
| |
| "768x576 · 4:3 fast": (576, 768), |
| "1024x768 · 4:3 full": (768, 1024), |
| "576x768 · 3:4 fast": (768, 576), |
| "768x1024 · 3:4 full": (1024, 768), |
| |
| "1152x512 · 21:9 fast": (512, 1152), |
| "1536x672 · 21:9 full": (672, 1536), |
| } |
| |
| DEFAULT_CANVAS = "1344x768 · 16:9 full" |
| DEFAULT_DURATION = 5 |
| FPS, FRAMES_PER_CHUNK, LATENTS_PER_CHUNK = 24, 17, 5 |
| |
| |
| MIN_UI_DURATION, MAX_UI_DURATION = 2, 8 |
|
|
|
|
| |
| |
| |
| BENCH_PROMPT = ( |
| "integrated_multimodal_description: [Shot 1] Live-action, cinematic, a medium-wide shot frames a baker opening " |
| "the shutters of a small street bakery before sunrise. The camera pushes in with small amplitude at slow speed " |
| "as the middle-aged baker with a calm, slightly raspy voice (S1) places a fresh loaf on the wooden counter and " |
| "says: <d>[English] First batch of the morning.</d> [Shot 2] At 00:05.000, the camera cuts to a close-up of " |
| "steam rising from the sliced bread while the baker's final words carry over from the previous shot.\n\n" |
| "overall_soundscape: Wooden shutters scrape open over a quiet street as trays clink softly inside the bakery. " |
| "The doorbell rings once, followed by light footsteps and the crisp sound of bread being sliced.\n\n" |
| "non_diegetic_music: A soft acoustic-guitar pattern at a moderate tempo, joined by sparse upright-bass notes and " |
| "a gentle fade at the end." |
| ) |
| BENCH_CANVAS = "1344x768 · 16:9 full" |
| BENCH_DURATION = 5 |
| BENCH_SEED = 42 |
| BENCH_UPSAMPLE = False |
|
|
|
|
| def snap_frames(seconds: float) -> int: |
| """The frame count MiniMax-H3's video VAE can decode: the next `17 * n + 5` at 24 fps.""" |
| frames = max(1, round(float(seconds) * FPS)) |
| while frames % FRAMES_PER_CHUNK != LATENTS_PER_CHUNK: |
| frames += 1 |
| return frames |
|
|
|
|
| def lower_duration_floor(seconds: float = MIN_UI_DURATION) -> None: |
| """Let the pipeline generate below its 5 s floor. 56 frames (2.33 s) is fine on the released checkpoint.""" |
| from diffusers.modular_pipelines.minimax_h3.modular_pipeline import MiniMaxH3ModularPipeline |
|
|
| MiniMaxH3ModularPipeline.min_duration = property(lambda self: float(seconds)) |
|
|
|
|
| PIPE = None |
| MANAGER = None |
| LOAD_ERROR: str | None = None |
| LOADED_IN: float | None = None |
| VSA_BLOCKS = 0 |
| VSA_GATES = 0 |
| |
| FORWARD_TIMES: list[float] = [] |
| |
| DECODE_TIMES: dict[str, float] = {} |
|
|
|
|
| def status() -> str: |
| if LOAD_ERROR: |
| return LOAD_ERROR |
| if PIPE is None: |
| return f"Loading `{MODEL_REPO}` (transformer + VAEs, 81 GB). Watch the Space logs." |
| if ATTENTION == "vsa": |
| attention = ( |
| f"**VSA-H3** block-sparse, tile 64 / sparsity {VSA_SPARSITY:g} on {VSA_BLOCKS} blocks " |
| f"({VSA_GATES} trained compression gates live)" |
| ) |
| else: |
| attention = f"dense `{ATTENTION}` (off-distribution for this student)" |
| return ( |
| f"Ready · distilled transformer + VAEs **bfloat16, unquantized** · {NUM_FORWARDS} transformer forwards " |
| f"({SIGMA_GRID_POINTS}-point sigma grid) · attention {attention} · placement `{PLACEMENT}` · " |
| f"loaded in {LOADED_IN:.0f}s · conditioner `{CONDITIONER_SPACE}` · " |
| f"`{torch_version()}` / triton `{triton_version()}`" |
| ) |
|
|
|
|
| def torch_version() -> str: |
| import torch |
|
|
| try: |
| capability = ".".join(map(str, torch.cuda.get_device_capability(0))) |
| except Exception: |
| capability = "nocuda" |
| return f"torch {torch.__version__} (sm_{capability})" |
|
|
|
|
| def triton_version() -> str: |
| try: |
| import triton |
|
|
| return triton.__version__ |
| except Exception: |
| return "?" |
|
|
|
|
| def load_models() -> str | None: |
| """Load the denoising half at startup. |
| |
| `MiniMaxH3GeneratorBlocks` declares `transformer`, `vae`, `audio_vae`, the two schedulers and `video_processor`, |
| so `load_components` fetches exactly those subfolders — `text_encoder/` and `transformer_ref/` are never touched. |
| Both autoencoders carry `_keep_in_fp32_modules` over every module and stay float32: a bfloat16 audio VAE decodes |
| the soundtrack roughly 20 dB too quiet. |
| |
| `add_gate_compress_modules()` has to run *before* the transformer is instantiated. The checkpoint carries 50 |
| `transformer_blocks.*.attn.to_gate_compress.weight` tensors — the trained VSA compression gate — and stock |
| `MiniMaxH3Attention` does not declare the module, so `from_pretrained` would report them as unexpected and drop |
| them. Declaring it first is what makes them load. |
| """ |
| global PIPE, MANAGER, LOAD_ERROR, LOADED_IN, VSA_BLOCKS, VSA_GATES |
|
|
| if PIPE is not None or LOAD_ERROR is not None: |
| return LOAD_ERROR |
|
|
| started = time.time() |
| try: |
| import torch |
| from diffusers import ComponentsManager |
|
|
| from h3_split_blocks import MiniMaxH3GeneratorBlocks |
|
|
| lower_duration_floor() |
| if ATTENTION == "vsa": |
| import vsa_h3 |
|
|
| vsa_h3.add_gate_compress_modules() |
|
|
| manager = ComponentsManager() |
| blocks = MiniMaxH3GeneratorBlocks() |
| print(f"[gen] loading {[c.name for c in blocks.expected_components]} from {MODEL_REPO} ...", flush=True) |
| pipe = blocks.init_pipeline(MODEL_REPO, components_manager=manager, collection="fasth3") |
| pipe.load_components(dtype=torch.bfloat16) |
|
|
| if ATTENTION == "vsa": |
| VSA_BLOCKS, VSA_GATES = vsa_h3.install(pipe.transformer, sparsity=VSA_SPARSITY) |
| print(f"[gen] VSA-H3 on {VSA_BLOCKS} blocks, {VSA_GATES} trained gates", flush=True) |
| |
| vsa_h3._resolve_cuda_sparse_op() |
| else: |
| pipe.transformer.set_attention_backend(ATTENTION) |
|
|
| _install_forward_timing(pipe) |
| _arm_decode_timers(pipe) |
|
|
| if PLACEMENT == "offload": |
| manager.enable_auto_cpu_offload(device="cuda") |
| _arm_decode_hooks(pipe) |
|
|
| PIPE, MANAGER = pipe, manager |
| LOADED_IN = time.time() - started |
| print(f"[gen] ready in {LOADED_IN:.0f}s", flush=True) |
| except Exception as error: |
| traceback.print_exc() |
| LOAD_ERROR = ( |
| f"**Loading `{MODEL_REPO}` failed** after {time.time() - started:.0f}s: " |
| f"`{type(error).__name__}: {error}`" |
| ) |
| return LOAD_ERROR |
|
|
|
|
| def _arm_decode_timers(pipe) -> None: |
| """Time each VAE's `decode` call, so a request can report the video/audio decode split. |
| |
| Instance-attribute wrapper, like `_arm_decode_hooks`; only meaningful with `PLACEMENT=lazy` (no offload hook in |
| front). The video VAE decode runs tiled under fp16 autocast over fp32 weights inside the pipeline's decode |
| block; this just measures it. |
| """ |
| for name in ("vae", "audio_vae"): |
| module = getattr(pipe, name) |
| inner = module.decode |
|
|
| def timed(*args, _name=name, _decode=inner, **kwargs): |
| started = time.perf_counter() |
| try: |
| return _decode(*args, **kwargs) |
| finally: |
| DECODE_TIMES[_name] = DECODE_TIMES.get(_name, 0.0) + time.perf_counter() - started |
|
|
| module.decode = timed |
|
|
|
|
| def _install_forward_timing(pipe) -> None: |
| """Record the wall time of every transformer forward, so each request can report where its seconds went. |
| |
| Runs *after* `vsa_h3.install`, so the timing wrapper sits outside the layout-publishing wrapper. `functools.wraps` |
| is load-bearing: the denoise block filters its `token_tags` / `position_ids` / ... kwargs by |
| `inspect.signature(transformer.forward).parameters`, and an unwrapped `(*args, **kwargs)` signature would empty |
| that set and drop the packed-sequence layout from every forward. |
| """ |
| original_forward = pipe.transformer.forward |
|
|
| @functools.wraps(original_forward) |
| def timed_forward(*args, **kwargs): |
| started = time.perf_counter() |
| try: |
| return original_forward(*args, **kwargs) |
| finally: |
| FORWARD_TIMES.append(time.perf_counter() - started) |
|
|
| pipe.transformer.forward = timed_forward |
|
|
|
|
| def _arm_decode_hooks(pipe): |
| """Make the offload hooks fire for the two VAEs. |
| |
| `enable_auto_cpu_offload` wraps `forward`, and the decode blocks call `vae.decode(...)` directly, so the hook |
| never runs and the VAE is still on the host when the latents arrive on the card. |
| """ |
| for name in ("vae", "audio_vae"): |
| module = getattr(pipe, name) |
| inner = module.decode |
|
|
| def armed(*args, _module=module, _decode=inner, **kwargs): |
| hook = getattr(_module, "_hf_hook", None) |
| if hook is not None: |
| hook.pre_forward(_module) |
| return _decode(*args, **kwargs) |
|
|
| module.decode = armed |
|
|
|
|
| @cache |
| def conditioner(): |
| """The other half, over the gradio API. `gradio_client` attaches the caller's own ZeroGPU token per call, so the |
| conditioner's booking is billed to whoever asked for the video.""" |
| from gradio_client import Client |
|
|
| return Client(CONDITIONER_SPACE) |
|
|
|
|
| def encode_remote(prompt: str, canvas: str, num_frames: int, rewrite_prompt: bool = False): |
| """`/encode` on the conditioner Space: a safetensors file holding `prompt_embeds` + `text_token_tags`, with the |
| resolved `height` / `width` / `num_frames` in its metadata, plus the plan. `canvas` is the label.""" |
| from safetensors import safe_open |
|
|
| path, plan = conditioner().predict( |
| prompt=prompt, |
| image_path=None, |
| last_image_path=None, |
| canvas=canvas, |
| num_frames=num_frames, |
| rewrite_prompt=bool(rewrite_prompt), |
| api_name="/encode", |
| ) |
| with safe_open(path, framework="pt") as handle: |
| return handle.get_tensor("prompt_embeds"), handle.get_tensor("text_token_tags"), handle.metadata(), plan |
|
|
|
|
| |
| |
| |
| |
| _DUR_A, _DUR_BASE = 1.95e-3, 3.0 |
| |
| |
| |
| |
| _COLD_ALLOWANCE, _WARM_ALLOWANCE = 75, 8 |
| _WARM = False |
| |
| _MARGIN = 1.15 |
|
|
|
|
| def get_duration(prompt_embeds, text_token_tags, height, width, num_frames, seed, *a, **k): |
| height, width, num_frames = int(height), int(width), int(num_frames) |
| latent_frames = (num_frames - LATENTS_PER_CHUNK) // FRAMES_PER_CHUNK * LATENTS_PER_CHUNK + 2 |
| rows = latent_frames * (height // 32) * (width // 32) |
| allowance = _WARM_ALLOWANCE if _WARM else _COLD_ALLOWANCE |
| return max(60, int((_DUR_A * rows + _DUR_BASE + allowance) * _MARGIN) + 2) |
|
|
|
|
| @spaces.GPU(duration=get_duration, size=GPU_SIZE) |
| def _generate(prompt_embeds, text_token_tags, height: int, width: int, num_frames: int, seed: int): |
| """The only thing on GPU time: the four-forward packed-sequence denoise loop and the two decoders. |
| |
| `gen_s` wraps the pipeline call alone — the four transformer forwards plus the two decoders — measured inside the |
| GPU worker with `time.perf_counter()`, and travels back to the client next to the video. The per-forward wall |
| times and the peak CUDA allocation ride along for profiling. |
| |
| Only the generated outputs come back — a `@spaces.GPU` return crosses a process boundary by pickling, and the |
| full `PipelineState` still holds the packed latents, the rotary grid and the row indices on the card. |
| """ |
| import torch |
|
|
| if PLACEMENT == "lazy": |
| PIPE.to("cuda") |
|
|
| if ATTENTION == "vsa": |
| |
| |
| import vsa_h3 |
|
|
| vsa_h3.begin_request() |
|
|
| torch.cuda.reset_peak_memory_stats() |
| FORWARD_TIMES.clear() |
| DECODE_TIMES.clear() |
| started = time.perf_counter() |
| if os.environ.get("H3_PROFILE") == "1": |
| from torch.profiler import ProfilerActivity, profile |
|
|
| with profile(activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA]) as prof: |
| state = PIPE( |
| prompt_embeds=prompt_embeds.to("cuda"), |
| text_token_tags=text_token_tags, |
| height=height, |
| width=width, |
| num_frames=num_frames, |
| num_inference_steps=SIGMA_GRID_POINTS, |
| generator=torch.Generator("cpu").manual_seed(int(seed)), |
| ) |
| print( |
| prof.key_averages().table(sort_by="cuda_time_total", row_limit=40, max_name_column_width=60), |
| flush=True, |
| ) |
| else: |
| state = PIPE( |
| prompt_embeds=prompt_embeds.to("cuda"), |
| text_token_tags=text_token_tags, |
| height=height, |
| width=width, |
| num_frames=num_frames, |
| num_inference_steps=SIGMA_GRID_POINTS, |
| generator=torch.Generator("cpu").manual_seed(int(seed)), |
| ) |
| gen_s = time.perf_counter() - started |
| peak_gib = torch.cuda.max_memory_allocated() / 1024**3 |
| forwards = [round(t, 2) for t in FORWARD_TIMES] |
| decodes = {name: round(t, 2) for name, t in DECODE_TIMES.items()} |
| print(f"[gen] pipeline {gen_s:.2f}s · forwards {forwards} · decodes {decodes} · peak {peak_gib:.2f} GiB", flush=True) |
| return ( |
| state.get("videos")[0], |
| state.get("audio")[0].cpu(), |
| state.get("sampling_rate"), |
| gen_s, |
| peak_gib, |
| forwards, |
| decodes, |
| ) |
|
|
|
|
| @spaces.GPU(duration=300, size=GPU_SIZE) |
| def aoti_diag() -> str: |
| """Phase A of the AOTI experiment: validate the traceable functional reimplementation of the VSA |
| sparse-attention soup, and (when `H3_AOTI=1`) export + AOTI-compile it for this GPU and check the |
| artifact against eager, with timings. |
| |
| Uses the *real bench geometry* (1344x768, 124 frames, 203 text + 414 audio + 37 296 video rows), |
| synthesized directly — no generation, no weights. |
| """ |
| import torch |
|
|
| import aoti_attention |
|
|
| device = torch.device("cuda") |
| heads, dim = 56, 128 |
| text_rows, audio_rows = 203, 414 |
| grid_t, grid_h, grid_w = 37, 24, 42 |
| n_video = grid_t * grid_h * grid_w |
| seq_len = text_rows + audio_rows + n_video |
|
|
| tags = torch.tensor([1] * text_rows + [2] * audio_rows + [0] * n_video, dtype=torch.long) |
| position_ids = torch.zeros(seq_len, 3, dtype=torch.float64) |
| video_start = text_rows + audio_rows |
| frame = torch.cartesian_prod(torch.arange(float(grid_h)), torch.arange(float(grid_w))) |
| position_ids[video_start:, 0] = torch.arange(float(grid_t)).repeat_interleave(grid_h * grid_w) |
| position_ids[video_start:, 1:] = frame.repeat(grid_t, 1) |
|
|
| import vsa_h3 |
|
|
| geometry = vsa_h3.geometry_from_layout(tags.to(device), position_ids.to(device), 0.9) |
| if geometry is None: |
| return "**FAILED**: synthetic bench layout did not yield VSA geometry." |
|
|
| def timed(fn, *args, repeats=3): |
| fn(*args) |
| torch.cuda.synchronize() |
| import time as _t |
|
|
| started = _t.perf_counter() |
| for _ in range(repeats): |
| fn(*args) |
| torch.cuda.synchronize() |
| return (_t.perf_counter() - started) / repeats |
|
|
| generator = torch.Generator(device=device).manual_seed(0) |
| shape = (1, seq_len, heads, dim) |
| q, k, v = (torch.randn(shape, generator=generator, device=device, dtype=torch.bfloat16) for _ in range(3)) |
| gate = torch.randn(shape, generator=generator, device=device, dtype=torch.bfloat16) * 0.05 |
|
|
| lines = [f"`{torch.cuda.get_device_name()}` · seq {seq_len}, padded {geometry.padded_len}, " |
| f"tiles {geometry.n_tiles}, topk {geometry.topk}", ""] |
|
|
| def eager(gate_h): |
| |
| return vsa_h3.sparse_attention(q, k, v, gate_h, geometry) |
|
|
| def functional(gate_h): |
| gate_t = None if gate_h is None else gate_h.transpose(1, 2) |
| return aoti_attention.sparse_attention_functional( |
| q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2), gate_t, |
| geometry.untile_index, geometry.variable_block_sizes, geometry.tile_divisor, |
| geometry.topk, geometry.num_prefix_tiles, |
| ) |
|
|
| for label, gate_h in (("gate=None", None), ("gate=rand*0.05", gate)): |
| ref, got = eager(gate_h), functional(gate_h) |
| error = (ref.float() - got.float()).abs().max().item() |
| scale = ref.float().abs().max().item() |
| cosine = torch.nn.functional.cosine_similarity( |
| ref.float().flatten(), got.float().flatten(), dim=0 |
| ).item() |
| t_eager = timed(eager, gate_h) |
| t_fn = timed(functional, gate_h) |
| lines.append( |
| f"| {label} | eager {t_eager * 1000:.0f} ms | functional {t_fn * 1000:.0f} ms | " |
| f"rel err {error / scale:.2e} | cosine {cosine:.6f} |" |
| ) |
|
|
| report = ( |
| "VSA soup: eager vs functional (op-for-op reimplementation)\n\n" |
| "| case | eager | functional | rel err | cosine |\n|---|---|---|---|---|\n" + "\n".join(lines[2:]) |
| ) |
|
|
| if os.environ.get("H3_AOTI") == "1": |
| try: |
| from torch._inductor import aoti_compile_and_package, aoti_load_package |
|
|
| with torch.no_grad(): |
| exported = torch.export.export( |
| aoti_attention.sparse_module(geometry.topk, geometry.num_prefix_tiles), |
| args=( |
| q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2), |
| gate.transpose(1, 2), geometry.untile_index, |
| geometry.variable_block_sizes, geometry.tile_divisor, |
| ), |
| ) |
| package = "/tmp/vsa_sparse_aoti.pt2" |
| aoti_compile_and_package(exported, package_path=package) |
| compiled = aoti_load_package(package) |
| got = compiled( |
| q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2), gate.transpose(1, 2), |
| geometry.untile_index, geometry.variable_block_sizes, geometry.tile_divisor, |
| ) |
| ref = eager(gate) |
| error = (ref.float() - got.float()).abs().max().item() |
| scale = ref.float().abs().max().item() |
| cosine = torch.nn.functional.cosine_similarity( |
| ref.float().flatten(), got.float().flatten(), dim=0 |
| ).item() |
| t_eager = timed(eager, gate) |
| t_compiled = timed(lambda *a: compiled(*a), ( |
| q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2), gate.transpose(1, 2), |
| geometry.untile_index, geometry.variable_block_sizes, geometry.tile_divisor, |
| )) |
| report += ( |
| f"\n\nAOTI artifact: `{package}`\n\n| eager | compiled | rel err | cosine |\n|---|---|---|---|\n" |
| f"| {t_eager * 1000:.0f} ms | {t_compiled * 1000:.0f} ms | {error / scale:.2e} | {cosine:.6f} |" |
| ) |
| except Exception as error: |
| report += f"\n\n**AOTI FAILED**: `{type(error).__name__}: {error}`" |
|
|
| return report |
|
|
|
|
| @spaces.GPU(duration=120, size=GPU_SIZE) |
| def selftest() -> str: |
| """Check the vendored VSA-H3 kernels against dense attention on this GPU. |
| |
| At `sparsity = 0` the block map is all-true, so VSA-H3 has to reproduce full attention exactly (up to the tile |
| padding and the fp32 pooled selection, which cannot change an all-true mask). That is the one assertion that |
| catches a wrong tile order, a wrong `variable_block_sizes`, or a mis-transposed buffer — all of which would |
| otherwise show up only as a subtly wrong video. Runs on random tensors; no weights are touched. |
| |
| Returns: |
| A markdown report: the dense-equivalence error, and how much of the dense output the 90%-sparse path keeps. |
| """ |
| import torch |
| import torch.nn.functional as F |
|
|
| from diffusers.modular_pipelines.minimax_h3.before_denoise import MiniMaxH3PrepareLayoutStep |
|
|
| import vsa_h3 |
|
|
| device = torch.device("cuda") |
| heads, dim = 8, 128 |
| |
| _, token_tags, *_ = MiniMaxH3PrepareLayoutStep.build_packed_sequence( |
| torch.ones(300, dtype=torch.long), 5, 16, 24, 50, (1, 2, 2), 2, 2, 0, () |
| ) |
| position_ids = torch.zeros(token_tags.numel(), 3, dtype=torch.float64) |
| video_start = int((token_tags == 0).nonzero()[0]) |
| frame = torch.cartesian_prod(torch.arange(8.0), torch.arange(12.0)) |
| position_ids[video_start:, 0] = torch.arange(5.0).repeat_interleave(96) |
| position_ids[video_start:, 1:] = frame.repeat(5, 1) |
| token_tags, position_ids = token_tags.to(device), position_ids.to(device) |
|
|
| lines = [] |
| generator = torch.Generator(device=device).manual_seed(0) |
| shape = (1, token_tags.numel(), heads, dim) |
| query, key, value = ( |
| torch.randn(shape, generator=generator, device=device, dtype=torch.bfloat16) for _ in range(3) |
| ) |
| reference = F.scaled_dot_product_attention( |
| query.transpose(1, 2), key.transpose(1, 2), value.transpose(1, 2) |
| ).transpose(1, 2) |
|
|
| for sparsity in (0.0, 0.9): |
| vsa_h3.reset_tile_buffers() |
| geometry = vsa_h3.geometry_from_layout(token_tags, position_ids, sparsity) |
| if geometry is None: |
| return "**FAILED**: `geometry_from_layout` did not recognize the standard packed layout." |
| out = vsa_h3.sparse_attention(query, key, value, None, geometry) |
| error = (out.float() - reference.float()).abs().max().item() |
| scale = reference.float().abs().max().item() |
| similarity = F.cosine_similarity(out.float().flatten(), reference.float().flatten(), dim=0).item() |
| lines.append( |
| f"| {sparsity:g} | {geometry.topk}/{geometry.num_video_tiles} | {error:.4f} | " |
| f"{error / scale:.2e} | {similarity:.6f} |" |
| ) |
| if sparsity == 0.0 and error / scale > 0.02: |
| lines.append(f"\n**FAILED**: dense-equivalent VSA differs from SDPA by {error / scale:.3f} relative.") |
|
|
| return ( |
| f"VSA-H3 on `{torch.cuda.get_device_name()}`, {token_tags.numel()} packed rows, " |
| f"{heads} heads x {dim}.\n\n" |
| "| sparsity | tiles kept | max abs err | relative | cosine |\n|---|---|---|---|---|\n" + "\n".join(lines) |
| ) |
|
|
|
|
| def generate( |
| prompt: str, |
| canvas: str = DEFAULT_CANVAS, |
| duration: float = DEFAULT_DURATION, |
| upsample: bool = True, |
| seed: int = 42, |
| progress=gr.Progress(track_tqdm=True), |
| ): |
| """Generate a video with a synchronized soundtrack from a text prompt, in four transformer forwards. |
| |
| Args: |
| prompt: The request. MiniMax-H3 was trained on a structured format |
| (`integrated_multimodal_description: ... overall_soundscape: ... non_diegetic_music: ...`); leave |
| `upsample` on to have the conditioner rewrite a plain sentence into it first. |
| canvas: One of the released canvases, as a `WIDTHxHEIGHT · ratio` label. The distillation's own operating |
| point is `1344x768 · 16:9 full`. |
| duration: Length in seconds, rounded up to the next frame count the video VAE can decode (`17 * n + 5`). |
| upsample: Rewrite the prompt into MiniMax-H3's trained format before encoding it. |
| seed: Random seed. |
| |
| Returns: |
| The path of an mp4 holding h264 video and AAC audio, a one-line report (with a per-request id proving the |
| artifact is fresh), the measured `gen_s` of the pipeline call, and the rewritten prompt when there was one. |
| """ |
| if LOAD_ERROR: |
| raise gr.Error(LOAD_ERROR) |
| if PIPE is None: |
| raise gr.Error("The denoiser is still loading.") |
| if not prompt or not prompt.strip(): |
| raise gr.Error("MiniMax-H3 always takes a prompt.") |
|
|
| from diffusers.utils import encode_video |
|
|
| num_frames = snap_frames(duration) |
|
|
| progress(0.0, desc=f"{'Rewriting the prompt' if upsample else 'Conditioning'} on {CONDITIONER_SPACE} ...") |
| conditioned = time.time() |
| prompt_embeds, text_token_tags, metadata, plan = encode_remote( |
| prompt, canvas, num_frames, rewrite_prompt=bool(upsample) |
| ) |
| condition_seconds = time.time() - conditioned |
| height, width, num_frames = (int(metadata[key]) for key in ("height", "width", "num_frames")) |
| refined = plan.get("refined_prompt") or "" |
|
|
| progress(0.2, desc=f"{NUM_FORWARDS} transformer forwards at {width}x{height}, {num_frames} frames ...") |
| frames, audio, sampling_rate, gen_s, peak_gib, forwards, decodes = _generate( |
| prompt_embeds, text_token_tags, height, width, num_frames, seed |
| ) |
|
|
| directory = os.path.join(tempfile.gettempdir(), "fasth3-outputs") |
| os.makedirs(directory, exist_ok=True) |
| request_id = uuid.uuid4().hex[:12] |
| path = os.path.join(directory, f"fasth3-{request_id}.mp4") |
| encode_video(frames, fps=FPS, output_path=path, audio=audio, audio_sample_rate=sampling_rate) |
|
|
| forward_total = sum(forwards) if forwards else float("nan") |
| forward_detail = "+".join(f"{t:.1f}" for t in forwards) if forwards else "n/a" |
| decode_detail = " + ".join(f"{k} {v:.1f}s" for k, v in decodes.items()) if decodes else "n/a" |
| report = ( |
| f"`{width}x{height}`, {num_frames} frames @ {FPS} fps, {NUM_FORWARDS} transformer forwards · " |
| f"pipeline **{gen_s:.1f}s** (forwards {forward_detail} = {forward_total:.1f}s, {decode_detail}, " |
| f"other {gen_s - forward_total - sum(decodes.values()):.1f}s) · peak {peak_gib:.1f} GiB · " |
| f"conditioner {condition_seconds:.0f}s ({plan['num_text_tokens']} tokens" |
| f"{', rewritten' if refined else ''}) · seed {int(seed)} · req {request_id}" |
| ) |
| print(f"[gen] {report}", flush=True) |
| return path, report, round(gen_s, 2), refined |
|
|
|
|
| load_models() |
|
|
| INTRO = f"""# FastH3 v1 (VSA) — MiniMax-H3 in 4 steps, sparse |
| |
| <div> |
| <a href="https://huggingface.co/{MODEL_REPO}" target="_blank" rel="noopener"><strong>[ model ]</strong></a> |
| <a href="https://github.com/hao-ai-lab/FastVideo" target="_blank" rel="noopener"><strong>[ FastVideo ]</strong></a> |
| <a href="https://huggingface.co/{BASE_REPO}" target="_blank" rel="noopener"><strong>[ base model ]</strong></a> |
| </div> |
| |
| [`{MODEL_REPO}`](https://huggingface.co/{MODEL_REPO}) is a **data-free DMD2 distillation** of |
| [MiniMax-H3](https://huggingface.co/{BASE_REPO}), the 33B dual-modality transformer that generates video **and** a |
| fully synchronized soundtrack (ambience, foley, speech) in one denoising pass. The base model samples in 50 steps; |
| this student walks a trained 4-step ladder — `t = 999, 749, 500, 250` — for **{NUM_FORWARDS} transformer forwards** |
| per video. |
| |
| It is also distilled **under Video Sparse Attention**: 64-token tiles at 90% sparsity, with a trained per-head |
| compression gate. This Space runs that sparse path, on FastVideo's own Triton kernels — not a dense substitute. |
| """ |
|
|
| FORMAT_NOTE = """MiniMax-H3 was trained on a structured prompt, not a caption: |
| |
| ```text |
| integrated_multimodal_description: [Shot 1] ... <d>[English] spoken line.</d> [Shot 2] At 00:04.500, ... |
| overall_soundscape: ... |
| non_diegetic_music: ... |
| ``` |
| |
| **Expand prompt** (on by default) sends a plain sentence through the Qwen3-VL conditioner's own language-model head |
| first, which writes that format with the same weights that are about to encode it. Turn it off when the prompt is |
| already written out — as the last two examples below are. See the base model's |
| [prompt writing guide](https://huggingface.co/MiniMaxAI/MiniMax-H3/blob/main/docs/VIDEO_PROMPT_WRITING_GUIDE_base_en.md). |
| """ |
|
|
| |
| |
| |
| GUIDE_CASE_1 = BENCH_PROMPT |
| OFFICIAL_T2VA = ( |
| "integrated_multimodal_description: [Shot 1] Cinematic, medium wide shot, pushing in slowly. In the cavernous, " |
| "dimly lit bridge of a starship, sleek metallic consoles with glowing amber displays flank a massive, curved " |
| "observation window. A female captain, in her late 40s with an athletic build and short silver-streaked black " |
| "hair, stands in the center midground. She wears a structured, high-collared dark navy military tunic with " |
| "silver chest insignias. Her back is to the camera, silhouetted against the cool, ambient starlight pouring " |
| "through the thick glass. She stands perfectly still with her hands clasped tightly behind her back. Outside the " |
| "window, a massive armada of jagged, dark grey dreadnoughts hovers in tight formation against a deep purple " |
| "space nebula. The fleet's massive rear thrusters begin to glow with an intense, escalating bright blue light. " |
| "[Shot 2] At 00:04.500, the camera cuts to a close-up of the captain's face and shakes strongly. The brilliant " |
| "blue-white light from the fleet's gathering energy reflects vividly in her dark eyes. Suddenly, a blinding " |
| "white flash floods through the window, completely washing out the background as the fleet jumps to hyperspace. " |
| "The sheer spatial force violently jolts the bridge, causing the captain from Shot 1 to stagger slightly " |
| "forward, her shoulders tensing as she visibly braces herself against the physical tremors. As the intense " |
| "white light fades abruptly, leaving only the dim, empty expanse of the purple nebula reflected in her starkly " |
| "lit skin, her jaw clenches, and she slowly closes her eyes in the newly emptied space.\n" |
| "overall_soundscape: A low, resonant hum of the ship's ambient life support systems serves as the baseline, soon " |
| "drowned out by an audible, escalating, high-pitched electronic whine as the fleet outside charges its " |
| "hyperdrives. A massive, deafening, bass-heavy boom and sharp crackle erupts during the blinding flash, " |
| "accompanied by the loud metallic creaking, rattling, and deep thuds of the bridge's bulkheads vibrating under " |
| "immense physical stress. The intense roaring impact then cuts abruptly back to a hollow, echoing room tone, " |
| "leaving only the faint, steady hum of the isolated bridge.\n" |
| "non_diegetic_music: Cinematic space-opera orchestral score, slow tempo, featuring a solitary, mournful French " |
| "horn melody over deep, sustained string dissonances that build rapidly in volume and intensity, swelling to a " |
| "massive orchestral peak before snapping immediately into silence right after the jump." |
| ) |
|
|
| CSS = """ |
| .main.fillable {max-width: 1250px !important} |
| .dark .gradio-container { color: var(--body-text-color); } |
| """ |
|
|
| with gr.Blocks(title="FastH3 v1 (VSA)") as demo: |
| gr.Markdown(INTRO) |
|
|
| with gr.Row(): |
| with gr.Column(): |
| prompt = gr.Textbox( |
| label="Prompt", |
| lines=5, |
| placeholder="A red fox trotting through a snowy pine forest at dawn, snow crunching underfoot", |
| value="A red fox trotting through a snowy pine forest at dawn, snow crunching underfoot", |
| ) |
| upsample = gr.Checkbox( |
| label="Expand prompt into MiniMax-H3's trained format", |
| value=True, |
| info="Runs on the conditioner Space before encoding. Turn off for a prompt already in that format.", |
| ) |
| run = gr.Button("Generate", variant="primary") |
| with gr.Accordion("Advanced options", open=False): |
| canvas = gr.Dropdown(label="Canvas", choices=list(CANVASES), value=DEFAULT_CANVAS) |
| duration = gr.Slider( |
| label="Duration (s)", |
| minimum=MIN_UI_DURATION, |
| maximum=MAX_UI_DURATION, |
| step=1, |
| value=DEFAULT_DURATION, |
| ) |
| seed = gr.Number(label="Seed", value=42, precision=0) |
| gr.Markdown( |
| f"Steps are fixed at the trained ladder — a {SIGMA_GRID_POINTS}-point sigma grid, " |
| f"{NUM_FORWARDS} transformer forwards, exactly what the checkpoint's own " |
| "`fastvideo_inference.json` specifies. There is no guidance scale and no negative prompt: the " |
| "base model is guidance-distilled." |
| ) |
|
|
| with gr.Column(): |
| video = gr.Video(label="Video + soundtrack") |
| report = gr.Markdown() |
| gen_s = gr.Number(label="gen_s — server-side pipeline time (s)", precision=2, interactive=False) |
| with gr.Accordion("Expanded prompt", open=False): |
| upsampled = gr.Textbox(show_label=False, lines=10, interactive=False) |
|
|
| with gr.Accordion("Prompt format", open=False): |
| gr.Markdown(FORMAT_NOTE) |
| banner = gr.Markdown() |
|
|
| gr.Examples( |
| examples=[ |
| [ |
| "A red fox trotting through a snowy pine forest at dawn, snow crunching underfoot", |
| "960x544 · 16:9 fast", |
| 5, |
| True, |
| ], |
| [ |
| "A cellist playing a slow, low melody alone in an empty concert hall", |
| "1344x768 · 16:9 full", |
| 5, |
| True, |
| ], |
| [GUIDE_CASE_1, "1344x768 · 16:9 full", 5, False], |
| [OFFICIAL_T2VA, "1344x768 · 16:9 full", 5, False], |
| ], |
| inputs=[prompt, canvas, duration, upsample], |
| outputs=[video, report, gen_s, upsampled], |
| fn=generate, |
| cache_examples=True, |
| cache_mode="lazy", |
| label="Examples — the last two are the MiniMax-H3 authors' own published T2VA prompts", |
| ) |
|
|
| run.click( |
| generate, |
| [prompt, canvas, duration, upsample, seed], |
| [video, report, gen_s, upsampled], |
| api_name="generate", |
| ) |
| demo.load(status, None, banner, api_name="status") |
|
|
| |
| |
| diagnose = gr.Button(visible=False) |
| diagnose.click(selftest, None, gr.Markdown(visible=False), api_name="selftest") |
| diagnose2 = gr.Button(visible=False) |
| diagnose2.click(aoti_diag, None, gr.Markdown(visible=False), api_name="aoti_diag") |
|
|
|
|
| if __name__ == "__main__": |
| |
| demo.launch(theme=gr.themes.Citrus(), css=CSS, show_error=True, max_threads=1000, mcp_server=True) |
|
|