Spaces:
Running on Zero
Running on Zero
feat: add Turbo LoRA presets and ComfyUI support
Browse files- Update Turbo LoRA presets to use EMA versions for better quality.
- Add support for ComfyUI-trained LoRAs via on-the-fly module remapping.
- Update README and app introduction to highlight 4-8 step Turbo LoRA speedups.
README.md
CHANGED
|
@@ -1,220 +1,220 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: MiniMax-H3 Reference Custom Lora
|
| 3 |
-
emoji: 🎭
|
| 4 |
-
colorFrom: pink
|
| 5 |
-
colorTo: purple
|
| 6 |
-
sdk: gradio
|
| 7 |
-
sdk_version: 6.20.0
|
| 8 |
-
app_file: app.py
|
| 9 |
-
pinned: true
|
| 10 |
-
short_description:
|
| 11 |
-
suggested_hardware: zero-a10g
|
| 12 |
-
---
|
| 13 |
-
|
| 14 |
-
# MiniMax-H3 — omni-references, unquantized, split across two Spaces
|
| 15 |
-
|
| 16 |
-
Joint video **and** soundtrack out of a single denoising pass, conditioned on an ordered list of image, video and
|
| 17 |
-
audio references, at **bfloat16 with no quantization anywhere**.
|
| 18 |
-
|
| 19 |
-
This Space is the denoising half of the `ref2va` task: the 61.73 GiB `transformer_ref` partition and the two
|
| 20 |
-
autoencoders. The 62.14 GiB Qwen3-VL conditioner runs in
|
| 21 |
-
[`qwen3vl-conditioner`](https://huggingface.co/spaces/multimodalart/qwen3vl-conditioner), which this
|
| 22 |
-
Space calls over the gradio API for every request — the same conditioner Space, and the same resident weights, that
|
| 23 |
-
the keyframe half [`minimax-h3`](https://huggingface.co/spaces/multimodalart/minimax-h3)
|
| 24 |
-
uses.
|
| 25 |
-
|
| 26 |
-
## Why split
|
| 27 |
-
|
| 28 |
-
MiniMax-H3 is 195.9 GiB in bfloat16 and a ZeroGPU Space is evicted at **150 GB of storage**. An unquantized single
|
| 29 |
-
Space is therefore impossible. Cut the `ref2va` branch of `MiniMaxH3Blocks` at its `text_encoder` step and both
|
| 30 |
-
halves fit unquantized:
|
| 31 |
-
|
| 32 |
-
| Space | Subfolders | Download | Resident |
|
| 33 |
-
|---|---|---|---|
|
| 34 |
-
| [`qwen3vl-conditioner`](https://huggingface.co/spaces/multimodalart/qwen3vl-conditioner) | `text_encoder/` + `tokenizer/` + `processor/` | 66.7 GB | 62.15 GiB bf16 |
|
| 35 |
-
| this one | `transformer_ref/` + `vae/` + `audio_vae/` | 77.3 GB | 61.73 GiB bf16 + 10.43 GiB float32 |
|
| 36 |
-
|
| 37 |
-
## References
|
| 38 |
-
|
| 39 |
-
A request carries up to **12** references — at most 9 images, 3 videos and 3 audio clips — **in the order the model
|
| 40 |
-
reads them**. The order is semantic: it numbers the labels of MiniMax-H3's prompt presentation (`<Picture 1>`,
|
| 41 |
-
`<Video 1>`, `<Audio 1>`) and it advances the shared audio/video rotary clock, so the same references in a different
|
| 42 |
-
order are a different request. This demo lays the slots out as one tab per modality in reading order — images, then
|
| 43 |
-
audio, then video — and assembles the request that way. The Images tab opens with two slots and **+ Add another
|
| 44 |
-
image** reveals the rest, up to the model's own nine; the audio and video tabs hold one each. A reference left in a
|
| 45 |
-
tab that is not the open one is still part of the request; the tabs lay the slots out, they do not choose between
|
| 46 |
-
them.
|
| 47 |
-
|
| 48 |
-
Rules the model imposes, enforced here before anything is uploaded:
|
| 49 |
-
|
| 50 |
-
* an audio reference cannot be the only one; it needs an image or a video alongside it,
|
| 51 |
-
* a reference video runs 2 to 15 seconds, and brings its own soundtrack with it,
|
| 52 |
-
* the generated duration may be left to the references, but only when exactly one of them carries a soundtrack —
|
| 53 |
-
which is why the duration slider disappears when a single reference can set it, and comes back when two can or
|
| 54 |
-
when the one that could is out of range.
|
| 55 |
-
|
| 56 |
-
## Example assets
|
| 57 |
-
|
| 58 |
-
`examples/subject.png` is [a studio portrait by Oliver
|
| 59 |
-
Dohrn](https://www.pexels.com/photo/casual-portrait-of-smiling-woman-in-studio-31747504/) cropped to head and
|
| 60 |
-
shoulders — Pexels licence, free to use. `examples/motion.mp4` is a synthetic clip from the parity fixtures, and
|
| 61 |
-
`examples/voice.wav` is utterance `1462-170145-0022` of
|
| 62 |
-
[LibriSpeech](https://www.openslr.org/12) `dev-clean` — CC BY 4.0, read from a public-domain LibriVox recording. It
|
| 63 |
-
is 16 kHz mono on purpose: the audio VAE wants 32 kHz, so the example exercises the `torchaudio` resample the
|
| 64 |
-
`ref2va` path needs.
|
| 65 |
-
|
| 66 |
-
## How the split is expressed
|
| 67 |
-
|
| 68 |
-
`MiniMaxH3Blocks` is one `SequentialPipelineBlocks` whose branches are picked per request — and per `workflow=` —
|
| 69 |
-
from the inputs, `ref2va` being the branch `references` selects:
|
| 70 |
-
|
| 71 |
-
```
|
| 72 |
-
setup -> text_encoder -> reference_encoder -> denoise -> after_denoise -> decode
|
| 73 |
-
```
|
| 74 |
-
|
| 75 |
-
where `denoise` is itself `prepare_layout -> prepare_latents -> set_timesteps -> denoise`, against the
|
| 76 |
-
`transformer_ref` partition.
|
| 77 |
-
|
| 78 |
-
`h3_split_blocks.py` subclasses it with the `text_encoder` step removed. Dropping the step drops the three
|
| 79 |
-
components it declares, so `load_components` resolves `transformer_ref` / `vae` / `audio_vae` / the two schedulers
|
| 80 |
-
out of the shared `modular_model_index.json` and never fetches the conditioner — and `prompt_embeds` and
|
| 81 |
-
`text_token_tags` become ordinary required inputs of the pipeline call:
|
| 82 |
-
|
| 83 |
-
```py
|
| 84 |
-
pipe = MiniMaxH3Ref2VAGeneratorBlocks().init_pipeline("MiniMaxAI/MiniMax-H3")
|
| 85 |
-
pipe.load_components(dtype=torch.bfloat16)
|
| 86 |
-
state = pipe(prompt_embeds=..., text_token_tags=..., references=[...], height=544, width=960, num_frames=124,
|
| 87 |
-
num_inference_steps=28)
|
| 88 |
-
```
|
| 89 |
-
|
| 90 |
-
Only **text** encoding is remote. `reference_encoder` is the `ref2va` branch's own encoder step — it runs the video
|
| 91 |
-
VAE over the image and video references and the audio VAE over the soundtracks, and it is where the references'
|
| 92 |
-
latent geometry is resolved — so it stays on this side, next to the autoencoders the conditioner Space does not hold.
|
| 93 |
-
|
| 94 |
-
The wire format is the same two tensors as the keyframe half: `(1, num_text_tokens, 5120)` bfloat16 and
|
| 95 |
-
`(num_text_tokens,)` int64, carried as one safetensors file with the resolved `height` / `width` / `num_frames` in
|
| 96 |
-
its metadata header. What differs is only what the conditioner is shown, so the references travel to it as files:
|
| 97 |
-
`ref2va`'s presentation puts a vision block in front of the prompt for every image and every merged video frame
|
| 98 |
-
pair. An audio reference contributes its `"<Audio j>: "` label and nothing else — a waveform never reaches the
|
| 99 |
-
conditioner — but it still goes over, because a single audio-bearing reference is what resolves `num_frames` when
|
| 100 |
-
the request leaves it open.
|
| 101 |
-
|
| 102 |
-
The `setup` step runs on **both** halves. It owns no component (PIL, PyAV-decoded media and arithmetic) and it
|
| 103 |
-
resolves the canvas, the `17 * n + 5` frame count and the references prepared at their own resolutions. It is
|
| 104 |
-
deterministic over the same files, and the conditioner returns the plan it resolved so this Space pins the same
|
| 105 |
-
canvas and frame count rather than re-deriving them.
|
| 106 |
-
|
| 107 |
-
## AoTI-compiled blocks
|
| 108 |
-
|
| 109 |
-
With `H3_AOTI=1` the 50 repeated transformer blocks run from a compiled package,
|
| 110 |
-
[`multimodalart/minimax-h3-aoti`](https://huggingface.co/multimodalart/minimax-h3-aoti)`:bf16/torch2.11/sm120/dynamic` — a single dynamic-sequence artifact that serves
|
| 111 |
-
every canvas, duration, reference set and prompt length.
|
| 112 |
-
|
| 113 |
-
It is the **same package the `transformer/` partition runs**, and nothing about it is partition-specific. The two
|
| 114 |
-
`config.json` files are identical field for field, and the package carries no weights at all: `LazyAOTIModel` binds
|
| 115 |
-
each block's own live `state_dict()` by name on its first forward. Patching it in is startup CPU work and costs no
|
| 116 |
-
GPU time.
|
| 117 |
-
|
| 118 |
-
It removes a near-constant ~0.5 s/step — 50 blocks' worth of kernel-launch overhead plus the norm / rotary / AdaLN
|
| 119 |
-
epilogues around the matmuls — and cannot touch the matmuls themselves, so it pays best where the block is not
|
| 120 |
-
compute bound. `ref2va` packs the reference rows in front of the generated ones, which makes the sequence longer
|
| 121 |
-
than a keyframe request at the same canvas and moves it further toward compute bound.
|
| 122 |
-
|
| 123 |
-
## Nothing is paid for with GPU time
|
| 124 |
-
|
| 125 |
-
The 77.3 GB download and the load happen at **startup**: `import spaces` at module top patches `torch.cuda` before
|
| 126 |
-
any GPU is attached, so nothing about the load needs a card. The conditioner round trip is a network call on this
|
| 127 |
-
Space's CPU. A `@spaces.GPU` call is therefore only the placement (once), the two reference encoders, the denoise
|
| 128 |
-
loop and the two decoders.
|
| 129 |
-
|
| 130 |
-
One thing does *not* happen at startup: the move onto the card. `spaces`' startup `torch.pack()` writes every
|
| 131 |
-
startup-resident CUDA tensor to a second copy on disk before deleting the downloaded originals, and 77.3 GB of
|
| 132 |
-
weights plus a 77.3 GB pack is 154.6 GB against a 150 GB quota — the Space is evicted mid-pack with `OSError:
|
| 133 |
-
[Errno 28] No space left on device`. Placement therefore happens on the **first GPU call**, `PIPE.to("cuda")` at the
|
| 134 |
-
top of the `@spaces.GPU` function: about 10 s of PCIe once, then a no-op walk, and the denoise loop runs with
|
| 135 |
-
everything resident and no offloading at all.
|
| 136 |
-
|
| 137 |
-
The references are decoded inside that call too, from their paths rather than as decoded media. A `@spaces.GPU`
|
| 138 |
-
argument crosses a process boundary by pickling, and a 5 s 1344x768 reference video is 370 MB of frames once PyAV
|
| 139 |
-
has expanded it.
|
| 140 |
-
|
| 141 |
-
## Generation constraints
|
| 142 |
-
|
| 143 |
-
Fixed by the checkpoint: 24 fps, a 768 pixel short edge, 5 to 15 s, `num_frames` snapped up to the next `17 * n + 5`,
|
| 144 |
-
no CFG and no negative prompt (it is guidance-distilled, so every step is one forward pass). The duration slider
|
| 145 |
-
stops at 14 s because it is the *snapped* count that has to hold for the ceiling: 15 s is 360 frames, which rounds
|
| 146 |
-
up to 362, i.e. 15.083 s, and is refused.
|
| 147 |
-
|
| 148 |
-
## GPU time is reserved per request, not per Space
|
| 149 |
-
|
| 150 |
-
MiniMax-H3 attends over one packed sequence, so what a step costs is a function of that sequence's length alone —
|
| 151 |
-
and on this half the references dominate it. A single 1344x768 image reference is ~7168 conditioning rows plus the
|
| 152 |
-
vision block it puts in front of the prompt; a 2.5 s video reference is another ~17000. The same 960x544, 124-frame
|
| 153 |
-
request runs 2.4 s/step with no references and 16 s/step with an image and a video.
|
| 154 |
-
|
| 155 |
-
`get_duration` prices that before the call instead of reserving a flat ceiling for everything. It takes the
|
| 156 |
-
arguments of the `@spaces.GPU` function, so it has the conditioner's own `text_token_tags` (exact) and the reference
|
| 157 |
-
files (measured from metadata, no decode), and evaluates
|
| 158 |
-
|
| 159 |
-
```
|
| 160 |
-
S = text rows + reference rows + target rows
|
| 161 |
-
seconds = placement + reference encode + steps * (LINEAR * S + QUADRATIC * S**2) * SAFETY + decode + pad
|
| 162 |
-
```
|
| 163 |
-
|
| 164 |
-
fitted on the `t2va` half and checked against live `ref2va` requests to about 10%. It matters beyond tidiness: the
|
| 165 |
-
pool reserves whatever number it is given, and a flat 900 s is what makes a busy account fail admission with *"You
|
| 166 |
-
have too many ZeroGPU credits allocated to running tasks."* A typical single-image request now reserves ~460 s.
|
| 167 |
-
|
| 168 |
-
Every request carries the full placement allowance, because nothing on this side knows whether the worker it lands
|
| 169 |
-
on is cold and a cold one pays the lazy 72.16 GiB `PIPE.to("cuda")` inside its first GPU call.
|
| 170 |
-
|
| 171 |
-
## Space variables
|
| 172 |
-
|
| 173 |
-
| Variable | Default | Meaning |
|
| 174 |
-
|---|---|---|
|
| 175 |
-
| `H3_CONDITIONER` | `multimodalart/qwen3vl-conditioner` | The public Space this one asks for embeddings; the client passes no token, so the call runs on the caller's own quota. |
|
| 176 |
-
| `H3_MODEL_REPO` | `MiniMaxAI/MiniMax-H3` | The diffusers-layout checkpoint. Public. |
|
| 177 |
-
| `H3_AOTI` | `0` | `1` loads the compiled block package. |
|
| 178 |
-
| `H3_PLACEMENT` | `lazy` | `lazy` moves all 72.16 GiB onto the card on the first GPU call and leaves it there; `offload` hands placement to `ComponentsManager.enable_auto_cpu_offload` instead. |
|
| 179 |
-
| `H3_ATTENTION` | `_native_cudnn` | cuDNN's fused kernel, 10–20% faster than the SDPA default and needs nothing installed. flash-attention 3 is sm90-only and this pool is sm120. The two VAEs are pinned to torch SDPA instead: they are float32, which cuDNN has no kernel for. |
|
| 180 |
-
| `H3_GPU_DURATION_MIN` / `_MAX` | `120` / `1500` | Bounds on what `get_duration` may reserve. |
|
| 181 |
-
| `H3_PLACEMENT_ALLOWANCE` | `90` | Seconds of the reservation set aside for a cold worker's placement. |
|
| 182 |
-
| `H3_GPU_SIZE` | `xlarge` | ZeroGPU allocation size. `large` does not fit. |
|
| 183 |
-
|
| 184 |
-
## Whose GPU quota pays
|
| 185 |
-
|
| 186 |
-
Two cards are booked per request — this Space's denoise loop and the conditioner's forward — and both are billed to the
|
| 187 |
-
requesting user, with nothing here arranging it: `gradio_client` attaches the caller's own `x-ip-token` to every
|
| 188 |
-
outgoing call, reading it off gradio's `LocalContext` inside the event listener (`Client.send_data` ->
|
| 189 |
-
`add_zero_gpu_headers`), and ZeroGPU charges the booking to whatever that token identifies.
|
| 190 |
-
|
| 191 |
-
A caller with no token to forward — a `gradio_client` script rather than a browser — leaves the conditioner's booking
|
| 192 |
-
attributed to this Space's pod IP and its small shared quota. An unattributed caller may book at most 120 credits at a
|
| 193 |
-
time and an `xlarge` booking costs twice its seconds, so the conditioner books the encode (45 s) and a prompt upsample
|
| 194 |
-
(60 s) as two separate calls, each within that ceiling.
|
| 195 |
-
|
| 196 |
-
## Secrets
|
| 197 |
-
|
| 198 |
-
None are required. Everything this Space downloads is public — the
|
| 199 |
-
[`MiniMaxAI/MiniMax-H3`](https://huggingface.co/MiniMaxAI/MiniMax-H3) checkpoint and the
|
| 200 |
-
[`multimodalart/minimax-h3-aoti`](https://huggingface.co/multimodalart/minimax-h3-aoti) packages — and the conditioner
|
| 201 |
-
is a public Space called on the requesting user's own ZeroGPU token, never on an org token.
|
| 202 |
-
|
| 203 |
-
## Where diffusers comes from
|
| 204 |
-
|
| 205 |
-
MiniMax-H3 is modular-only and not in a released `diffusers`, so `requirements.txt` installs it from the canonical
|
| 206 |
-
pull request, [huggingface/diffusers#14371](https://github.com/huggingface/diffusers/pull/14371), pinned to the commit
|
| 207 |
-
`665f5782` (`refs/pull/14371/head`) rather than to the moving `minimax-h3-refactor` branch.
|
| 208 |
-
|
| 209 |
-
That PR is a WIP, so it needs re-pinning whenever it updates, and `h3_split_blocks.py` — which subclasses its block
|
| 210 |
-
classes to cut the pipeline in two — has to be re-checked against the new head at the same time.
|
| 211 |
-
|
| 212 |
-
Two of those are `ref2va`-only and easy to miss. PyAV decodes a reference video or audio file as the reference is
|
| 213 |
-
built, and **`torchaudio`** resamples a soundtrack that is not already at the audio VAE's 32 kHz — a 32 kHz
|
| 214 |
-
reference skips the resample entirely, so the dependency only shows up once someone brings audio at another rate:
|
| 215 |
-
|
| 216 |
-
```
|
| 217 |
-
ImportError: Resampling a MiniMax-H3 reference soundtrack from 24000 Hz to 32000 Hz needs `torchaudio`.
|
| 218 |
-
```
|
| 219 |
-
|
| 220 |
The conditioner Space needs it as well: its `setup` step normalizes the very same waveforms this one does.
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: MiniMax-H3 Reference Custom Lora
|
| 3 |
+
emoji: 🎭
|
| 4 |
+
colorFrom: pink
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 6.20.0
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: true
|
| 10 |
+
short_description: MiniMax-H3 with 4-8 step Turbo LoRAs, ComfyUI LoRA support
|
| 11 |
+
suggested_hardware: zero-a10g
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
# MiniMax-H3 — omni-references, unquantized, split across two Spaces
|
| 15 |
+
|
| 16 |
+
Joint video **and** soundtrack out of a single denoising pass, conditioned on an ordered list of image, video and
|
| 17 |
+
audio references, at **bfloat16 with no quantization anywhere**.
|
| 18 |
+
|
| 19 |
+
This Space is the denoising half of the `ref2va` task: the 61.73 GiB `transformer_ref` partition and the two
|
| 20 |
+
autoencoders. The 62.14 GiB Qwen3-VL conditioner runs in
|
| 21 |
+
[`qwen3vl-conditioner`](https://huggingface.co/spaces/multimodalart/qwen3vl-conditioner), which this
|
| 22 |
+
Space calls over the gradio API for every request — the same conditioner Space, and the same resident weights, that
|
| 23 |
+
the keyframe half [`minimax-h3`](https://huggingface.co/spaces/multimodalart/minimax-h3)
|
| 24 |
+
uses.
|
| 25 |
+
|
| 26 |
+
## Why split
|
| 27 |
+
|
| 28 |
+
MiniMax-H3 is 195.9 GiB in bfloat16 and a ZeroGPU Space is evicted at **150 GB of storage**. An unquantized single
|
| 29 |
+
Space is therefore impossible. Cut the `ref2va` branch of `MiniMaxH3Blocks` at its `text_encoder` step and both
|
| 30 |
+
halves fit unquantized:
|
| 31 |
+
|
| 32 |
+
| Space | Subfolders | Download | Resident |
|
| 33 |
+
|---|---|---|---|
|
| 34 |
+
| [`qwen3vl-conditioner`](https://huggingface.co/spaces/multimodalart/qwen3vl-conditioner) | `text_encoder/` + `tokenizer/` + `processor/` | 66.7 GB | 62.15 GiB bf16 |
|
| 35 |
+
| this one | `transformer_ref/` + `vae/` + `audio_vae/` | 77.3 GB | 61.73 GiB bf16 + 10.43 GiB float32 |
|
| 36 |
+
|
| 37 |
+
## References
|
| 38 |
+
|
| 39 |
+
A request carries up to **12** references — at most 9 images, 3 videos and 3 audio clips — **in the order the model
|
| 40 |
+
reads them**. The order is semantic: it numbers the labels of MiniMax-H3's prompt presentation (`<Picture 1>`,
|
| 41 |
+
`<Video 1>`, `<Audio 1>`) and it advances the shared audio/video rotary clock, so the same references in a different
|
| 42 |
+
order are a different request. This demo lays the slots out as one tab per modality in reading order — images, then
|
| 43 |
+
audio, then video — and assembles the request that way. The Images tab opens with two slots and **+ Add another
|
| 44 |
+
image** reveals the rest, up to the model's own nine; the audio and video tabs hold one each. A reference left in a
|
| 45 |
+
tab that is not the open one is still part of the request; the tabs lay the slots out, they do not choose between
|
| 46 |
+
them.
|
| 47 |
+
|
| 48 |
+
Rules the model imposes, enforced here before anything is uploaded:
|
| 49 |
+
|
| 50 |
+
* an audio reference cannot be the only one; it needs an image or a video alongside it,
|
| 51 |
+
* a reference video runs 2 to 15 seconds, and brings its own soundtrack with it,
|
| 52 |
+
* the generated duration may be left to the references, but only when exactly one of them carries a soundtrack —
|
| 53 |
+
which is why the duration slider disappears when a single reference can set it, and comes back when two can or
|
| 54 |
+
when the one that could is out of range.
|
| 55 |
+
|
| 56 |
+
## Example assets
|
| 57 |
+
|
| 58 |
+
`examples/subject.png` is [a studio portrait by Oliver
|
| 59 |
+
Dohrn](https://www.pexels.com/photo/casual-portrait-of-smiling-woman-in-studio-31747504/) cropped to head and
|
| 60 |
+
shoulders — Pexels licence, free to use. `examples/motion.mp4` is a synthetic clip from the parity fixtures, and
|
| 61 |
+
`examples/voice.wav` is utterance `1462-170145-0022` of
|
| 62 |
+
[LibriSpeech](https://www.openslr.org/12) `dev-clean` — CC BY 4.0, read from a public-domain LibriVox recording. It
|
| 63 |
+
is 16 kHz mono on purpose: the audio VAE wants 32 kHz, so the example exercises the `torchaudio` resample the
|
| 64 |
+
`ref2va` path needs.
|
| 65 |
+
|
| 66 |
+
## How the split is expressed
|
| 67 |
+
|
| 68 |
+
`MiniMaxH3Blocks` is one `SequentialPipelineBlocks` whose branches are picked per request — and per `workflow=` —
|
| 69 |
+
from the inputs, `ref2va` being the branch `references` selects:
|
| 70 |
+
|
| 71 |
+
```
|
| 72 |
+
setup -> text_encoder -> reference_encoder -> denoise -> after_denoise -> decode
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
where `denoise` is itself `prepare_layout -> prepare_latents -> set_timesteps -> denoise`, against the
|
| 76 |
+
`transformer_ref` partition.
|
| 77 |
+
|
| 78 |
+
`h3_split_blocks.py` subclasses it with the `text_encoder` step removed. Dropping the step drops the three
|
| 79 |
+
components it declares, so `load_components` resolves `transformer_ref` / `vae` / `audio_vae` / the two schedulers
|
| 80 |
+
out of the shared `modular_model_index.json` and never fetches the conditioner — and `prompt_embeds` and
|
| 81 |
+
`text_token_tags` become ordinary required inputs of the pipeline call:
|
| 82 |
+
|
| 83 |
+
```py
|
| 84 |
+
pipe = MiniMaxH3Ref2VAGeneratorBlocks().init_pipeline("MiniMaxAI/MiniMax-H3")
|
| 85 |
+
pipe.load_components(dtype=torch.bfloat16)
|
| 86 |
+
state = pipe(prompt_embeds=..., text_token_tags=..., references=[...], height=544, width=960, num_frames=124,
|
| 87 |
+
num_inference_steps=28)
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
Only **text** encoding is remote. `reference_encoder` is the `ref2va` branch's own encoder step — it runs the video
|
| 91 |
+
VAE over the image and video references and the audio VAE over the soundtracks, and it is where the references'
|
| 92 |
+
latent geometry is resolved — so it stays on this side, next to the autoencoders the conditioner Space does not hold.
|
| 93 |
+
|
| 94 |
+
The wire format is the same two tensors as the keyframe half: `(1, num_text_tokens, 5120)` bfloat16 and
|
| 95 |
+
`(num_text_tokens,)` int64, carried as one safetensors file with the resolved `height` / `width` / `num_frames` in
|
| 96 |
+
its metadata header. What differs is only what the conditioner is shown, so the references travel to it as files:
|
| 97 |
+
`ref2va`'s presentation puts a vision block in front of the prompt for every image and every merged video frame
|
| 98 |
+
pair. An audio reference contributes its `"<Audio j>: "` label and nothing else — a waveform never reaches the
|
| 99 |
+
conditioner — but it still goes over, because a single audio-bearing reference is what resolves `num_frames` when
|
| 100 |
+
the request leaves it open.
|
| 101 |
+
|
| 102 |
+
The `setup` step runs on **both** halves. It owns no component (PIL, PyAV-decoded media and arithmetic) and it
|
| 103 |
+
resolves the canvas, the `17 * n + 5` frame count and the references prepared at their own resolutions. It is
|
| 104 |
+
deterministic over the same files, and the conditioner returns the plan it resolved so this Space pins the same
|
| 105 |
+
canvas and frame count rather than re-deriving them.
|
| 106 |
+
|
| 107 |
+
## AoTI-compiled blocks
|
| 108 |
+
|
| 109 |
+
With `H3_AOTI=1` the 50 repeated transformer blocks run from a compiled package,
|
| 110 |
+
[`multimodalart/minimax-h3-aoti`](https://huggingface.co/multimodalart/minimax-h3-aoti)`:bf16/torch2.11/sm120/dynamic` — a single dynamic-sequence artifact that serves
|
| 111 |
+
every canvas, duration, reference set and prompt length.
|
| 112 |
+
|
| 113 |
+
It is the **same package the `transformer/` partition runs**, and nothing about it is partition-specific. The two
|
| 114 |
+
`config.json` files are identical field for field, and the package carries no weights at all: `LazyAOTIModel` binds
|
| 115 |
+
each block's own live `state_dict()` by name on its first forward. Patching it in is startup CPU work and costs no
|
| 116 |
+
GPU time.
|
| 117 |
+
|
| 118 |
+
It removes a near-constant ~0.5 s/step — 50 blocks' worth of kernel-launch overhead plus the norm / rotary / AdaLN
|
| 119 |
+
epilogues around the matmuls — and cannot touch the matmuls themselves, so it pays best where the block is not
|
| 120 |
+
compute bound. `ref2va` packs the reference rows in front of the generated ones, which makes the sequence longer
|
| 121 |
+
than a keyframe request at the same canvas and moves it further toward compute bound.
|
| 122 |
+
|
| 123 |
+
## Nothing is paid for with GPU time
|
| 124 |
+
|
| 125 |
+
The 77.3 GB download and the load happen at **startup**: `import spaces` at module top patches `torch.cuda` before
|
| 126 |
+
any GPU is attached, so nothing about the load needs a card. The conditioner round trip is a network call on this
|
| 127 |
+
Space's CPU. A `@spaces.GPU` call is therefore only the placement (once), the two reference encoders, the denoise
|
| 128 |
+
loop and the two decoders.
|
| 129 |
+
|
| 130 |
+
One thing does *not* happen at startup: the move onto the card. `spaces`' startup `torch.pack()` writes every
|
| 131 |
+
startup-resident CUDA tensor to a second copy on disk before deleting the downloaded originals, and 77.3 GB of
|
| 132 |
+
weights plus a 77.3 GB pack is 154.6 GB against a 150 GB quota — the Space is evicted mid-pack with `OSError:
|
| 133 |
+
[Errno 28] No space left on device`. Placement therefore happens on the **first GPU call**, `PIPE.to("cuda")` at the
|
| 134 |
+
top of the `@spaces.GPU` function: about 10 s of PCIe once, then a no-op walk, and the denoise loop runs with
|
| 135 |
+
everything resident and no offloading at all.
|
| 136 |
+
|
| 137 |
+
The references are decoded inside that call too, from their paths rather than as decoded media. A `@spaces.GPU`
|
| 138 |
+
argument crosses a process boundary by pickling, and a 5 s 1344x768 reference video is 370 MB of frames once PyAV
|
| 139 |
+
has expanded it.
|
| 140 |
+
|
| 141 |
+
## Generation constraints
|
| 142 |
+
|
| 143 |
+
Fixed by the checkpoint: 24 fps, a 768 pixel short edge, 5 to 15 s, `num_frames` snapped up to the next `17 * n + 5`,
|
| 144 |
+
no CFG and no negative prompt (it is guidance-distilled, so every step is one forward pass). The duration slider
|
| 145 |
+
stops at 14 s because it is the *snapped* count that has to hold for the ceiling: 15 s is 360 frames, which rounds
|
| 146 |
+
up to 362, i.e. 15.083 s, and is refused.
|
| 147 |
+
|
| 148 |
+
## GPU time is reserved per request, not per Space
|
| 149 |
+
|
| 150 |
+
MiniMax-H3 attends over one packed sequence, so what a step costs is a function of that sequence's length alone —
|
| 151 |
+
and on this half the references dominate it. A single 1344x768 image reference is ~7168 conditioning rows plus the
|
| 152 |
+
vision block it puts in front of the prompt; a 2.5 s video reference is another ~17000. The same 960x544, 124-frame
|
| 153 |
+
request runs 2.4 s/step with no references and 16 s/step with an image and a video.
|
| 154 |
+
|
| 155 |
+
`get_duration` prices that before the call instead of reserving a flat ceiling for everything. It takes the
|
| 156 |
+
arguments of the `@spaces.GPU` function, so it has the conditioner's own `text_token_tags` (exact) and the reference
|
| 157 |
+
files (measured from metadata, no decode), and evaluates
|
| 158 |
+
|
| 159 |
+
```
|
| 160 |
+
S = text rows + reference rows + target rows
|
| 161 |
+
seconds = placement + reference encode + steps * (LINEAR * S + QUADRATIC * S**2) * SAFETY + decode + pad
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
fitted on the `t2va` half and checked against live `ref2va` requests to about 10%. It matters beyond tidiness: the
|
| 165 |
+
pool reserves whatever number it is given, and a flat 900 s is what makes a busy account fail admission with *"You
|
| 166 |
+
have too many ZeroGPU credits allocated to running tasks."* A typical single-image request now reserves ~460 s.
|
| 167 |
+
|
| 168 |
+
Every request carries the full placement allowance, because nothing on this side knows whether the worker it lands
|
| 169 |
+
on is cold and a cold one pays the lazy 72.16 GiB `PIPE.to("cuda")` inside its first GPU call.
|
| 170 |
+
|
| 171 |
+
## Space variables
|
| 172 |
+
|
| 173 |
+
| Variable | Default | Meaning |
|
| 174 |
+
|---|---|---|
|
| 175 |
+
| `H3_CONDITIONER` | `multimodalart/qwen3vl-conditioner` | The public Space this one asks for embeddings; the client passes no token, so the call runs on the caller's own quota. |
|
| 176 |
+
| `H3_MODEL_REPO` | `MiniMaxAI/MiniMax-H3` | The diffusers-layout checkpoint. Public. |
|
| 177 |
+
| `H3_AOTI` | `0` | `1` loads the compiled block package. |
|
| 178 |
+
| `H3_PLACEMENT` | `lazy` | `lazy` moves all 72.16 GiB onto the card on the first GPU call and leaves it there; `offload` hands placement to `ComponentsManager.enable_auto_cpu_offload` instead. |
|
| 179 |
+
| `H3_ATTENTION` | `_native_cudnn` | cuDNN's fused kernel, 10–20% faster than the SDPA default and needs nothing installed. flash-attention 3 is sm90-only and this pool is sm120. The two VAEs are pinned to torch SDPA instead: they are float32, which cuDNN has no kernel for. |
|
| 180 |
+
| `H3_GPU_DURATION_MIN` / `_MAX` | `120` / `1500` | Bounds on what `get_duration` may reserve. |
|
| 181 |
+
| `H3_PLACEMENT_ALLOWANCE` | `90` | Seconds of the reservation set aside for a cold worker's placement. |
|
| 182 |
+
| `H3_GPU_SIZE` | `xlarge` | ZeroGPU allocation size. `large` does not fit. |
|
| 183 |
+
|
| 184 |
+
## Whose GPU quota pays
|
| 185 |
+
|
| 186 |
+
Two cards are booked per request — this Space's denoise loop and the conditioner's forward — and both are billed to the
|
| 187 |
+
requesting user, with nothing here arranging it: `gradio_client` attaches the caller's own `x-ip-token` to every
|
| 188 |
+
outgoing call, reading it off gradio's `LocalContext` inside the event listener (`Client.send_data` ->
|
| 189 |
+
`add_zero_gpu_headers`), and ZeroGPU charges the booking to whatever that token identifies.
|
| 190 |
+
|
| 191 |
+
A caller with no token to forward — a `gradio_client` script rather than a browser — leaves the conditioner's booking
|
| 192 |
+
attributed to this Space's pod IP and its small shared quota. An unattributed caller may book at most 120 credits at a
|
| 193 |
+
time and an `xlarge` booking costs twice its seconds, so the conditioner books the encode (45 s) and a prompt upsample
|
| 194 |
+
(60 s) as two separate calls, each within that ceiling.
|
| 195 |
+
|
| 196 |
+
## Secrets
|
| 197 |
+
|
| 198 |
+
None are required. Everything this Space downloads is public — the
|
| 199 |
+
[`MiniMaxAI/MiniMax-H3`](https://huggingface.co/MiniMaxAI/MiniMax-H3) checkpoint and the
|
| 200 |
+
[`multimodalart/minimax-h3-aoti`](https://huggingface.co/multimodalart/minimax-h3-aoti) packages — and the conditioner
|
| 201 |
+
is a public Space called on the requesting user's own ZeroGPU token, never on an org token.
|
| 202 |
+
|
| 203 |
+
## Where diffusers comes from
|
| 204 |
+
|
| 205 |
+
MiniMax-H3 is modular-only and not in a released `diffusers`, so `requirements.txt` installs it from the canonical
|
| 206 |
+
pull request, [huggingface/diffusers#14371](https://github.com/huggingface/diffusers/pull/14371), pinned to the commit
|
| 207 |
+
`665f5782` (`refs/pull/14371/head`) rather than to the moving `minimax-h3-refactor` branch.
|
| 208 |
+
|
| 209 |
+
That PR is a WIP, so it needs re-pinning whenever it updates, and `h3_split_blocks.py` — which subclasses its block
|
| 210 |
+
classes to cut the pipeline in two — has to be re-checked against the new head at the same time.
|
| 211 |
+
|
| 212 |
+
Two of those are `ref2va`-only and easy to miss. PyAV decodes a reference video or audio file as the reference is
|
| 213 |
+
built, and **`torchaudio`** resamples a soundtrack that is not already at the audio VAE's 32 kHz — a 32 kHz
|
| 214 |
+
reference skips the resample entirely, so the dependency only shows up once someone brings audio at another rate:
|
| 215 |
+
|
| 216 |
+
```
|
| 217 |
+
ImportError: Resampling a MiniMax-H3 reference soundtrack from 24000 Hz to 32000 Hz needs `torchaudio`.
|
| 218 |
+
```
|
| 219 |
+
|
| 220 |
The conditioner Space needs it as well: its `setup` step normalizes the very same waveforms this one does.
|
app.py
CHANGED
|
@@ -80,13 +80,13 @@ LORA_MIN_SCALE, LORA_MAX_SCALE = -2.0, 2.0
|
|
| 80 |
# cached by `huggingface_hub` thereafter — nothing is bundled in this Space.
|
| 81 |
LORA_PRESETS = {
|
| 82 |
"Turbo v4 (step 600) · 6–8 steps": (
|
| 83 |
-
"larryvrh/MiniMax-H3-Turbo-Lora/
|
| 84 |
8,
|
| 85 |
"Recommended for most work. Strong static / small-motion, good micro-detail, no over-sharpening. "
|
| 86 |
"Use 6–8 steps; 4 steps can smear on heavy motion.",
|
| 87 |
),
|
| 88 |
"Turbo v1 (ckpt 850) · 4 steps": (
|
| 89 |
-
"larryvrh/MiniMax-H3-Turbo-Lora/
|
| 90 |
4,
|
| 91 |
"The friendlier pick for 4-step heavy / fast motion, where v4 can trail. Over-sharpens at higher step counts, "
|
| 92 |
"so keep it at 4 steps.",
|
|
@@ -809,6 +809,12 @@ INTRO = """# MiniMax-H3 Reference Custom Lora
|
|
| 809 |
**MiniMax-H3** is a 33B parameter state of the art video generation model that produces video and a
|
| 810 |
fully synchronized soundtrack (ambience, foley, speech). Bring your own subject, voice or camera move as a
|
| 811 |
reference.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 812 |
"""
|
| 813 |
|
| 814 |
LORA_HELP = """Each slot takes a Hugging Face repo (`owner/repo`), a file inside one
|
|
|
|
| 80 |
# cached by `huggingface_hub` thereafter — nothing is bundled in this Space.
|
| 81 |
LORA_PRESETS = {
|
| 82 |
"Turbo v4 (step 600) · 6–8 steps": (
|
| 83 |
+
"larryvrh/MiniMax-H3-Turbo-Lora/minimax_h3_turbo_v4_step600_ema.safetensors",
|
| 84 |
8,
|
| 85 |
"Recommended for most work. Strong static / small-motion, good micro-detail, no over-sharpening. "
|
| 86 |
"Use 6–8 steps; 4 steps can smear on heavy motion.",
|
| 87 |
),
|
| 88 |
"Turbo v1 (ckpt 850) · 4 steps": (
|
| 89 |
+
"larryvrh/MiniMax-H3-Turbo-Lora/minimax_h3_turbo_4step_ema_ckpt850.safetensors",
|
| 90 |
4,
|
| 91 |
"The friendlier pick for 4-step heavy / fast motion, where v4 can trail. Over-sharpens at higher step counts, "
|
| 92 |
"so keep it at 4 steps.",
|
|
|
|
| 809 |
**MiniMax-H3** is a 33B parameter state of the art video generation model that produces video and a
|
| 810 |
fully synchronized soundtrack (ambience, foley, speech). Bring your own subject, voice or camera move as a
|
| 811 |
reference.
|
| 812 |
+
|
| 813 |
+
This Space ships with **pre-wired Turbo LoRA presets** from
|
| 814 |
+
[`larryvrh/MiniMax-H3-Turbo-Lora`](https://huggingface.co/larryvrh/MiniMax-H3-Turbo-Lora) that render joint video +
|
| 815 |
+
soundtrack in **4–8 steps** instead of the usual ~20 (a ~5× speedup). It also accepts **ComfyUI-trained LoRAs** —
|
| 816 |
+
adapters in the ComfyUI checkpoint naming are remapped to diffusers' module names on the fly, so a `.safetensors`
|
| 817 |
+
trained against the ComfyUI base works without a separate conversion step.
|
| 818 |
"""
|
| 819 |
|
| 820 |
LORA_HELP = """Each slot takes a Hugging Face repo (`owner/repo`), a file inside one
|