mpasila commited on
Commit
49c5b9b
·
verified ·
1 Parent(s): b3babf9

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.

Files changed (2) hide show
  1. README.md +219 -219
  2. app.py +8 -2
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: Unquantized MiniMax-H3 from image, audio, video refs
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/minimax_h3_turbo_v4_step600.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_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,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