File size: 40,943 Bytes
d4ceaf5 b477f24 d4ceaf5 93f5879 d4ceaf5 dd8b2a6 d4ceaf5 dd8b2a6 d4ceaf5 16aa819 d4ceaf5 93f5879 d4ceaf5 93f5879 d4ceaf5 b477f24 d4ceaf5 b477f24 d4ceaf5 50089e0 d4ceaf5 93f5879 d4ceaf5 93f5879 d4ceaf5 93f5879 d4ceaf5 d401c91 f122b41 d401c91 13b6445 d401c91 492f937 d401c91 492f937 d401c91 2ade38a d401c91 d4ceaf5 93f5879 d4ceaf5 93f5879 d4ceaf5 93f5879 d4ceaf5 d401c91 d4ceaf5 d401c91 d4ceaf5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 | """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
# Before anything that could initialize CUDA: `import spaces` patches `torch.cuda` so the 75 GiB load can happen at
# startup rather than on GPU time.
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")
# `lazy` moves all weights onto the card on the first GPU call and leaves them there; `offload` hands placement to
# `ComponentsManager.enable_auto_cpu_offload`. Packing at startup is not an option here: `spaces` writes every
# startup-resident CUDA tensor to a second on-disk copy, and this checkpoint's transformer (70.1 GB on disk) would
# bust the 150 GB quota.
PLACEMENT = os.environ.get("H3_PLACEMENT", "lazy").lower()
# `vsa` is the trained route (see the module docstring). `dense` is the escape hatch: it runs the released dense
# `diffusers` path with cuDNN's fused kernel, which is off-distribution for this student but useful to bisect against.
ATTENTION = os.environ.get("H3_ATTENTION", "vsa").lower()
# The checkpoint's own `vsa_sparsity`. Only read when `H3_ATTENTION=vsa`.
VSA_SPARSITY = float(os.environ.get("H3_VSA_SPARSITY", "0.9"))
# 75.7 GiB of weights plus activations does not fit a `large` (48 GiB) allocation.
GPU_SIZE = os.environ.get("H3_GPU_SIZE", "xlarge")
# The distilled ladder, as sigma grid points. 5 points -> 4 transformer forwards at t = 1000, 750, 500, 250.
SIGMA_GRID_POINTS = 5
NUM_FORWARDS = SIGMA_GRID_POINTS - 1
# Must stay identical to the conditioner's table: the *label* goes over the wire, so a canvas that half does not know
# is rejected there and surfaces as a failure here.
CANVASES = {
# 16:9
"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),
# 9:16
"544x960 · 9:16 fast": (960, 544),
"640x1152 · 9:16": (1152, 640),
"768x1344 · 9:16 full": (1344, 768),
# 1:1
"544x544 · 1:1 fast": (544, 544),
"768x768 · 1:1 full": (768, 768),
"1024x1024 · 1:1 max": (1024, 1024),
# 4:3 / 3:4
"768x576 · 4:3 fast": (576, 768),
"1024x768 · 4:3 full": (768, 1024),
"576x768 · 3:4 fast": (768, 576),
"768x1024 · 3:4 full": (1024, 768),
# 21:9
"1152x512 · 21:9 fast": (512, 1152),
"1536x672 · 21:9 full": (672, 1536),
}
# The distillation's own operating point: 768x1344, 124 frames, 24 fps.
DEFAULT_CANVAS = "1344x768 · 16:9 full"
DEFAULT_DURATION = 5
FPS, FRAMES_PER_CHUNK, LATENTS_PER_CHUNK = 24, 17, 5
# The ceiling holds for the *snapped* frame count. 75.74 GiB of weights are resident on a 95.0 GiB card, and the
# sparse path's own working set grows with the packed sequence, so this is a memory ceiling, not a policy one.
MIN_UI_DURATION, MAX_UI_DURATION = 2, 8
# The frozen benchmark request. One fixed prompt, one fixed seed, the trained 4-forward schedule, the distilled
# operating point (1344x768, 124 frames, 24 fps), audio on (the pipeline always emits it). `bench/bench_client.py`
# duplicates this table; keep the two in sync.
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 # the prompt is already in the trained format
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
# Wall seconds of the last request's transformer forwards (reset by every `_generate` call).
FORWARD_TIMES: list[float] = []
# Wall seconds of the last request's two VAE decodes, appended by the wrappers `_arm_decode_timers` arms.
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: # noqa: BLE001 - no GPU visible at status time
capability = "nocuda"
return f"torch {torch.__version__} (sm_{capability})"
def triton_version() -> str:
try:
import triton
return triton.__version__
except Exception: # noqa: BLE001
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)
# Resolve the sparse kernel now (CPU-side import only) so the log shows the outcome at startup.
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
# Seconds of GPU one request needs, from the packed rows it is about to denoise. Block-sparse attention keeps a fixed
# 10% of the tiles, so the cost is linear in the rows and the quadratic term a dense path needs is gone. Measured
# warm on this Space: 37 296 rows in 59-72 s and 57 456 rows in 87 s, i.e. 1.51e-3 to 1.93e-3 s/row depending on how
# fast a slice of the pool the request lands on. Fitted to the *slow* end so a slow slice is not aborted mid-video.
_DUR_A, _DUR_BASE = 1.95e-3, 3.0
# The one-time costs a cold worker pays inside its first request: 75.7 GiB across PCIe (~11 s) plus the Triton JIT of
# the vendored block-sparse kernels (~57 s), measured at 74 s against 6 s warm for the same request. Booking that on
# *every* request would burn 70 s of each visitor's quota for nothing, so it is only booked while this process has
# not yet seen a request come back.
_COLD_ALLOWANCE, _WARM_ALLOWANCE = 75, 8
_WARM = False
# Booked over the estimate. Keep it small: an inflated duration burns the visitor's quota and drops queue priority.
_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":
# One packed-sequence request: let every forward reuse the first forward's VSA geometry instead of
# re-deriving it from `token_tags` with a host sync each time.
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) # warm (JIT/compile)
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):
# eager sparse_attention takes the gate as [B, S, H, D] and transposes internally
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: # noqa: BLE001 - surfaced verbatim for the log
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
# A real packed layout, just a small one: 300 text rows, 5 latent frames of 8x12 video, its soundtrack.
_, 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).
"""
# Both long examples are the MiniMax-H3 authors' own published T2VA prompts: the first is Case 1 of the prompt writing
# guide, the second is the reproducible 768p T2VA case from the model card
# (`scripts/readme/reproducible-768p-t2va-request.sh`). Both are documentation of the Apache-2.0 base repo.
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")
# No UI, API only: the sparse-attention equivalence check, so the kernel can be verified on this pool without
# spending a full generation; and the AOTI Phase A validation.
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__":
# Gradio 6 moved `theme` and `css` off the `Blocks` constructor onto `launch`.
demo.launch(theme=gr.themes.Citrus(), css=CSS, show_error=True, max_threads=1000, mcp_server=True)
|