mrfakename commited on
Commit
a4e1031
·
1 Parent(s): eb09060

Localize the H3 conditioner

Browse files
Files changed (5) hide show
  1. README.md +26 -13
  2. app.py +119 -35
  3. h3_local_conditioner.py +174 -0
  4. h3_nvfp4.py +8 -0
  5. h3_split_blocks.py +6 -6
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: MiniMax H3 Ultra
3
  emoji: ⚡
4
  colorFrom: purple
5
  colorTo: indigo
@@ -7,11 +7,11 @@ sdk: gradio
7
  sdk_version: 6.20.0
8
  app_file: app.py
9
  pinned: true
10
- short_description: Blackwell-native NVFP4 video + synchronized audio generation
11
  suggested_hardware: zero-a10g
12
  ---
13
 
14
- # MiniMax-H3 Ultra — pruned NVFP4 on Blackwell
15
 
16
  Joint video and synchronized sound from MiniMax-H3, with the repeatedly executed transformer rebuilt around the
17
  Blackwell-native ComfyUI optimization path:
@@ -22,6 +22,8 @@ Blackwell-native ComfyUI optimization path:
22
  - One fused QKV projection per attention layer.
23
  - One fused in-place Q/K RMSNorm + partial split-half RoPE kernel.
24
  - Native CUDA 13 NVFP4 tensor-core GEMMs through `comfy-kitchen`.
 
 
25
  - A conservative EasyCache-style trajectory estimator skips complete 50-layer denoiser evaluations when successive
26
  latent changes are predictable; it is request-local and is not prompt-to-video output caching.
27
  - Segment-wise in-place AdaLN modulation and gated residual accumulation.
@@ -67,20 +69,23 @@ Blackwell at 864×480, 39 frames. Those numbers are useful implementation eviden
67
  H3 attention grows quadratically with packed sequence length, so resolution, duration and keyframe vision tokens
68
  still dominate large requests.
69
 
70
- ## Split deployment
71
 
72
- The full checkpoint cannot fit under a single Space's 150 GB storage ceiling. This Space remains the denoising half:
 
73
 
74
  | component | where it runs | precision / format |
75
  |---|---|---|
76
- | Qwen3-VL layer-50 conditioner | [`qwen3vl-conditioner`](https://huggingface.co/spaces/multimodalart/qwen3vl-conditioner) | BF16 |
77
  | H3 transformer | this Space | pruned NVFP4 + higher-precision islands |
78
  | video VAE | this Space | full precision checkpoint policy |
79
  | audio VAE | this Space | FP32 |
80
 
81
- The wire format is `prompt_embeds` plus `text_token_tags` in a safetensors file. The conditioner also returns the
82
- resolved canvas, aligned frame count and prompt plan. Keyframes are encoded again by this Space's video VAE so the
83
- conditioning latents exactly match the pixels seen by the conditioner.
 
 
84
 
85
  ## Kernel path
86
 
@@ -105,7 +110,8 @@ not the default.
105
 
106
  ## Quality trade-off
107
 
108
- NVFP4 and adaptive step reuse are approximate. The checkpoint author reports that 4-bit weights can show more
 
109
  mid-motion artifacts and weaker shape retention than the larger INT8 ConvRot checkpoint on difficult 15-second
110
  clips. The comparison was not fully controlled, so treat it as a real caution rather than a quantified quality
111
  score. Adaptive reuse adds another speed/quality trade-off; lower `H3_EASYCACHE_THRESHOLD` for motion-sensitive
@@ -122,7 +128,11 @@ exact original denoiser, set `H3_ENGINE=bf16`; this restores the 61.7 GiB unquan
122
  | `H3_NVFP4_REPO` | `lilcheaty/MiniMax-H3-NVFP4` | Repository containing the pruned Comfy-format transformer. |
123
  | `H3_NVFP4_FILE` | `minimax_h3_fl2va_pruned_nvfp4.safetensors` | FL2VA/T2VA transformer file. |
124
  | `H3_MODEL_REPO` | `MiniMaxAI/MiniMax-H3` | Canonical schedulers and VAE checkpoint. |
125
- | `H3_CONDITIONER` | `multimodalart/qwen3vl-conditioner` | Remote layer-50 conditioner Space. |
 
 
 
 
126
  | `H3_PLACEMENT` | `lazy` (`nvfp4`) | Move the compact transformer and VAEs on the first GPU call, then keep them resident. |
127
  | `H3_ATTENTION` | `_native_cudnn` | Attention backend for both the main stack and text refiner. |
128
  | `H3_EASYCACHE_THRESHOLD` | `0.10` | Maximum accumulated estimated change before a full denoiser evaluation; `0` disables reuse. |
@@ -139,11 +149,14 @@ exact original denoiser, set `H3_ENGINE=bf16`; this restores the 61.7 GiB unquan
139
  - `comfy-kitchen==0.2.26` for the native layouts and fused Q/K kernel.
140
  - The pinned MiniMax-H3 diffusers pull request for the modular schedulers, packing and VAE decode path.
141
 
142
- No secret is required. All model artifacts are public, and `gradio_client` forwards the requesting user's ZeroGPU
143
- identity to the conditioner.
144
 
145
  ## Attribution
146
 
 
 
 
147
  The fused/pruned model structure follows
148
  [`comfy/ldm/minimax/model.py`](https://github.com/Comfy-Org/ComfyUI/blob/master/comfy/ldm/minimax/model.py) from
149
  ComfyUI (Apache-2.0), and the adaptive residual estimator follows ComfyUI's built-in
 
1
  ---
2
+ title: MiniMax-H3 Ultra Fast
3
  emoji: ⚡
4
  colorFrom: purple
5
  colorTo: indigo
 
7
  sdk_version: 6.20.0
8
  app_file: app.py
9
  pinned: true
10
+ short_description: Ultra-fast local NVFP4 video + synchronized audio generation
11
  suggested_hardware: zero-a10g
12
  ---
13
 
14
+ # MiniMax-H3 Ultra Fast local conditioner + pruned NVFP4 on Blackwell
15
 
16
  Joint video and synchronized sound from MiniMax-H3, with the repeatedly executed transformer rebuilt around the
17
  Blackwell-native ComfyUI optimization path:
 
22
  - One fused QKV projection per attention layer.
23
  - One fused in-place Q/K RMSNorm + partial split-half RoPE kernel.
24
  - Native CUDA 13 NVFP4 tensor-core GEMMs through `comfy-kitchen`.
25
+ - A local 15.7 GB Qwen3-VL NVFP4-AWQ conditioner replaces the normal cross-Space API call. It contains exactly the
26
+ first 50 language layers H3 reads, with the unused 14-layer tail and vocabulary head removed.
27
  - A conservative EasyCache-style trajectory estimator skips complete 50-layer denoiser evaluations when successive
28
  latent changes are predictable; it is request-local and is not prompt-to-video output caching.
29
  - Segment-wise in-place AdaLN modulation and gated residual accumulation.
 
69
  H3 attention grows quadratically with packed sequence length, so resolution, duration and keyframe vision tokens
70
  still dominate large requests.
71
 
72
+ ## One-Space deployment
73
 
74
+ The original BF16 deployment had to be split because its 62.14 GiB conditioner plus 61.7 GiB transformer could not
75
+ fit comfortably under one Space's storage and runtime limits. The pruned formats change that calculation:
76
 
77
  | component | where it runs | precision / format |
78
  |---|---|---|
79
+ | Qwen3-VL layer-50 conditioner | this Space | truncated NVFP4-AWQ weights / BF16 GEMMs + BF16 vision tower |
80
  | H3 transformer | this Space | pruned NVFP4 + higher-precision islands |
81
  | video VAE | this Space | full precision checkpoint policy |
82
  | audio VAE | this Space | FP32 |
83
 
84
+ The embeddings stay on the same GPU worker and flow directly into H3: there is no Gradio round trip, second queue,
85
+ safetensors serialization, or user-quota handoff during normal generation. Prompt upsampling still uses the remote
86
+ BF16 conditioner because rewriting requires the language-model head and the 14 decoder layers deliberately removed
87
+ from the local inference-only checkpoint. The remote service also remains an automatic fallback if local loading
88
+ fails.
89
 
90
  ## Kernel path
91
 
 
110
 
111
  ## Quality trade-off
112
 
113
+ The transformer, local conditioner, and adaptive step reuse are approximate. The checkpoint author reports that
114
+ 4-bit weights can show more
115
  mid-motion artifacts and weaker shape retention than the larger INT8 ConvRot checkpoint on difficult 15-second
116
  clips. The comparison was not fully controlled, so treat it as a real caution rather than a quantified quality
117
  score. Adaptive reuse adds another speed/quality trade-off; lower `H3_EASYCACHE_THRESHOLD` for motion-sensitive
 
128
  | `H3_NVFP4_REPO` | `lilcheaty/MiniMax-H3-NVFP4` | Repository containing the pruned Comfy-format transformer. |
129
  | `H3_NVFP4_FILE` | `minimax_h3_fl2va_pruned_nvfp4.safetensors` | FL2VA/T2VA transformer file. |
130
  | `H3_MODEL_REPO` | `MiniMaxAI/MiniMax-H3` | Canonical schedulers and VAE checkpoint. |
131
+ | `H3_CONDITIONER_MODE` | `local` | Use the local truncated conditioner; `remote` restores the split deployment. |
132
+ | `H3_LOCAL_CONDITIONER_REPO` | `Comfy-Org/MiniMax-H3` | Repository containing the truncated conditioner. |
133
+ | `H3_LOCAL_CONDITIONER_FILE` | `text_encoders/qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors` | Local layer-50 conditioner checkpoint. |
134
+ | `H3_CONDITIONER_NATIVE_NVFP4` | `0` | Use native W4A4 conditioner GEMMs for maximum speed; default BF16 GEMMs add less activation error. |
135
+ | `H3_CONDITIONER` | `multimodalart/qwen3vl-conditioner` | Prompt-upsampling and local-load fallback service. |
136
  | `H3_PLACEMENT` | `lazy` (`nvfp4`) | Move the compact transformer and VAEs on the first GPU call, then keep them resident. |
137
  | `H3_ATTENTION` | `_native_cudnn` | Attention backend for both the main stack and text refiner. |
138
  | `H3_EASYCACHE_THRESHOLD` | `0.10` | Maximum accumulated estimated change before a full denoiser evaluation; `0` disables reuse. |
 
149
  - `comfy-kitchen==0.2.26` for the native layouts and fused Q/K kernel.
150
  - The pinned MiniMax-H3 diffusers pull request for the modular schedulers, packing and VAE decode path.
151
 
152
+ No secret is required. All model artifacts are public. Normal requests use one local GPU booking; a prompt-upsampling
153
+ request forwards the requesting user's ZeroGPU identity to the remote rewriter/conditioner.
154
 
155
  ## Attribution
156
 
157
+ This is an optimized derivative of the original
158
+ [`multimodalart/minimax-h3`](https://huggingface.co/spaces/multimodalart/minimax-h3) Space.
159
+
160
  The fused/pruned model structure follows
161
  [`comfy/ldm/minimax/model.py`](https://github.com/Comfy-Org/ComfyUI/blob/master/comfy/ldm/minimax/model.py) from
162
  ComfyUI (Apache-2.0), and the adaptive residual estimator follows ComfyUI's built-in
app.py CHANGED
@@ -1,4 +1,4 @@
1
- """MiniMax-H3 `t2va` / `fl2va`, split deployment — the denoising half."""
2
 
3
  from __future__ import annotations
4
 
@@ -15,6 +15,7 @@ import gradio as gr
15
 
16
  MODEL_REPO = os.environ.get("H3_MODEL_REPO", "MiniMaxAI/MiniMax-H3")
17
  CONDITIONER_SPACE = os.environ.get("H3_CONDITIONER", "multimodalart/qwen3vl-conditioner")
 
18
  # `nvfp4` is the Blackwell-native ultra path; `bf16` preserves the original 33B diffusers transformer as a fallback.
19
  ENGINE = os.environ.get("H3_ENGINE", "nvfp4").lower()
20
  # `pack` places the transformer at startup, `lazy` moves everything on the first GPU call, `offload` hands placement to
@@ -73,6 +74,8 @@ def lower_duration_floor(seconds: float = MIN_UI_DURATION) -> None:
73
 
74
  PIPE = None
75
  MANAGER = None
 
 
76
  LOAD_ERROR: str | None = None
77
  LOADED_IN: float | None = None
78
 
@@ -82,7 +85,7 @@ def status() -> str:
82
  return LOAD_ERROR
83
  if PIPE is None:
84
  payload = (
85
- "pruned NVFP4 transformer + full-precision VAEs (~28 GB)"
86
  if ENGINE == "nvfp4"
87
  else "BF16 transformer + VAEs (77.3 GB)"
88
  )
@@ -95,21 +98,27 @@ def status() -> str:
95
  import h3_aoti
96
 
97
  engine_status = f"BF16, unquantized · {h3_aoti.status()}"
 
 
 
 
 
 
98
  return (
99
  f"Ready · **{engine_status}** · VAEs full precision · placement `{PLACEMENT}` · attention `{ATTENTION}` · "
100
- f"loaded in {LOADED_IN:.0f}s · conditioner `{CONDITIONER_SPACE}`"
101
  )
102
 
103
 
104
  def load_models() -> str | None:
105
- """Load the denoising half at startup.
106
 
107
  `MiniMaxH3GeneratorBlocks` declares `transformer`, `vae`, `audio_vae`, the two schedulers and `video_processor`,
108
  so `load_components` fetches exactly those subfolders — `text_encoder/` and `transformer_ref/` are never touched.
109
  Both autoencoders carry `_keep_in_fp32_modules` over every module and stay float32: a bfloat16 audio VAE decodes
110
  the soundtrack roughly 20 dB too quiet.
111
  """
112
- global PIPE, MANAGER, LOAD_ERROR, LOADED_IN
113
 
114
  if PIPE is not None or LOAD_ERROR is not None:
115
  return LOAD_ERROR
@@ -159,7 +168,28 @@ def load_models() -> str | None:
159
  manager.enable_auto_cpu_offload(device="cuda")
160
  _arm_decode_hooks(pipe)
161
 
162
- PIPE, MANAGER = pipe, manager
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  LOADED_IN = time.time() - started
164
  print(f"[gen] ready in {LOADED_IN:.0f}s", flush=True)
165
  except Exception as error:
@@ -225,31 +255,52 @@ _DECODE_BASE, _DECODE_PER_DEFAULT_CANVAS, _DEFAULT_CANVAS_PIXELS = 15, 15, 960 *
225
  _PLACEMENT_ALLOWANCE, _PAD = 12, 10
226
 
227
 
228
- def get_duration(prompt_embeds, text_token_tags, image, last_image, height, width, num_frames, steps, seed, *a, **k):
229
  height, width, num_frames, steps = int(height), int(width), int(num_frames), int(steps)
230
  latent_frames = (num_frames - LATENTS_PER_CHUNK) // FRAMES_PER_CHUNK * LATENTS_PER_CHUNK + 2
231
  patches = (height // 32) * (width // 32)
232
  rows = latent_frames * patches + (int(image is not None) + int(last_image is not None)) * patches
233
  denoise = steps * (_DUR_B * rows + _DUR_C * rows**2)
234
  decode = _DECODE_BASE + _DECODE_PER_DEFAULT_CANVAS * (height * width * num_frames) / _DEFAULT_CANVAS_PIXELS
235
- return max(60, int(denoise + decode) + _PLACEMENT_ALLOWANCE + _PAD)
 
236
 
237
 
238
  @spaces.GPU(duration=get_duration, size=GPU_SIZE)
239
- def _generate(prompt_embeds, text_token_tags, image, last_image, height, width, num_frames, steps, seed):
240
- """The only thing on GPU time: the packed-sequence denoise loop and the two decoders.
241
 
242
- Only the three generated outputs come back a `@spaces.GPU` return crosses a process boundary by pickling, and
243
- the full `PipelineState` still holds the packed latents, the rotary grid and the row indices on the card.
244
  """
245
  import torch
246
 
 
 
247
  if PLACEMENT == "lazy":
248
  PIPE.to("cuda")
249
  elif PLACEMENT == "pack":
250
  PIPE.vae.to("cuda")
251
  PIPE.audio_vae.to("cuda")
252
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  begin_request = getattr(PIPE.transformer, "begin_request", None)
254
  end_request = getattr(PIPE.transformer, "end_request", None)
255
  if begin_request is not None:
@@ -270,7 +321,13 @@ def _generate(prompt_embeds, text_token_tags, image, last_image, height, width,
270
  finally:
271
  if end_request is not None:
272
  end_request()
273
- return state.get("videos")[0], state.get("audio")[0].cpu(), state.get("sampling_rate")
 
 
 
 
 
 
274
 
275
 
276
  def generate(prompt, image_path=None, last_image_path=None, canvas=DEFAULT_CANVAS, duration=5, steps=28, seed=42, upsample=False, progress=gr.Progress(track_tqdm=True)):
@@ -287,28 +344,49 @@ def generate(prompt, image_path=None, last_image_path=None, canvas=DEFAULT_CANVA
287
  from diffusers.utils import encode_video
288
 
289
  num_frames = snap_frames(duration)
290
-
291
- progress(0.0, desc=f"Upsampling the prompt on {CONDITIONER_SPACE} ..." if upsample else f"Conditioning on {CONDITIONER_SPACE} ...")
292
- conditioned = time.time()
293
- prompt_embeds, text_token_tags, metadata, plan = encode_remote(
294
- prompt, image_path, last_image_path, canvas, num_frames, rewrite_prompt=upsample
295
- )
296
- condition_seconds = time.time() - conditioned
297
- height, width, num_frames = (int(metadata[key]) for key in ("height", "width", "num_frames"))
298
- refined = plan.get("refined_prompt") or ""
299
 
300
  def keyframe(path):
301
- # The conditioning latents encoded here have to be of the image the conditioner looked at, which it prepares
302
- # exactly this way.
303
  return ImageOps.exif_transpose(Image.open(path)).convert("RGB") if path else None
304
 
305
- progress(0.1, desc=f"Denoising {steps} steps at {width}x{height}, {num_frames} frames ...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  started = time.time()
307
- frames, audio, sampling_rate = _generate(
 
308
  prompt_embeds,
309
  text_token_tags,
310
- keyframe(image_path),
311
- keyframe(last_image_path),
312
  height,
313
  width,
314
  num_frames,
@@ -316,6 +394,10 @@ def generate(prompt, image_path=None, last_image_path=None, canvas=DEFAULT_CANVA
316
  seed,
317
  )
318
  generate_seconds = time.time() - started
 
 
 
 
319
 
320
  directory = os.path.join(tempfile.gettempdir(), "h3-outputs")
321
  os.makedirs(directory, exist_ok=True)
@@ -324,9 +406,9 @@ def generate(prompt, image_path=None, last_image_path=None, canvas=DEFAULT_CANVA
324
 
325
  report = (
326
  f"`{width}x{height}`, {num_frames} frames ({num_frames / FPS:.3f} s), {int(steps)} steps · "
327
- f"conditioner {condition_seconds:.0f}s ({plan['num_text_tokens']} tokens"
328
  f"{', upsampled' if refined else ''}) · "
329
- f"denoise + decode {generate_seconds:.0f}s ({generate_seconds / int(steps):.1f} s/step) · seed {int(seed)}"
330
  )
331
  print(f"[gen] {report}", flush=True)
332
  return path, report, refined, gr.update(visible=bool(refined))
@@ -371,17 +453,19 @@ def _fit_keyframe(image_path, current_canvas):
371
 
372
  load_models()
373
 
374
- INTRO = """# MiniMax-H3 Ultra
375
 
376
  <div align="center">
377
  <a href="https://huggingface.co/MiniMaxAI/MiniMax-H3" target="_blank" rel="noopener"><strong>[ model ]</strong></a> &nbsp;
378
  <a href="https://huggingface.co/lilcheaty/MiniMax-H3-NVFP4" target="_blank" rel="noopener"><strong>[ NVFP4 ]</strong></a> &nbsp;
379
  <a href="https://www.minimax.io/blog/minimax-h3" target="_blank" rel="noopener"><strong>[ blog ]</strong></a> &nbsp;
380
- <a href="https://huggingface.co/spaces/multimodalart/minimax-h3-reference" target="_blank" rel="noopener"><strong>[ reference to video ]</strong></a>
381
  </div>
382
 
383
- **MiniMax-H3 Ultra** runs the pruned Blackwell-native NVFP4 transformer with fused QKV, fused Q/K norm + RoPE,
384
- full-precision video/audio decoders, and the original synchronized soundtrack generation.
 
 
385
  """
386
 
387
  CSS = """
@@ -389,7 +473,7 @@ CSS = """
389
  .dark .gradio-container { color: var(--body-text-color); }
390
  """
391
 
392
- with gr.Blocks(title="MiniMax-H3") as demo:
393
  gr.Markdown(INTRO)
394
 
395
  with gr.Row():
 
1
+ """MiniMax-H3 Ultra Fast: local layer-50 conditioning plus `t2va` / `fl2va` generation."""
2
 
3
  from __future__ import annotations
4
 
 
15
 
16
  MODEL_REPO = os.environ.get("H3_MODEL_REPO", "MiniMaxAI/MiniMax-H3")
17
  CONDITIONER_SPACE = os.environ.get("H3_CONDITIONER", "multimodalart/qwen3vl-conditioner")
18
+ CONDITIONER_MODE = os.environ.get("H3_CONDITIONER_MODE", "local").lower()
19
  # `nvfp4` is the Blackwell-native ultra path; `bf16` preserves the original 33B diffusers transformer as a fallback.
20
  ENGINE = os.environ.get("H3_ENGINE", "nvfp4").lower()
21
  # `pack` places the transformer at startup, `lazy` moves everything on the first GPU call, `offload` hands placement to
 
74
 
75
  PIPE = None
76
  MANAGER = None
77
+ COND_PIPE = None
78
+ COND_ERROR: str | None = None
79
  LOAD_ERROR: str | None = None
80
  LOADED_IN: float | None = None
81
 
 
85
  return LOAD_ERROR
86
  if PIPE is None:
87
  payload = (
88
+ "pruned NVFP4 transformer + local NVFP4 conditioner + full-precision VAEs (~44 GB)"
89
  if ENGINE == "nvfp4"
90
  else "BF16 transformer + VAEs (77.3 GB)"
91
  )
 
98
  import h3_aoti
99
 
100
  engine_status = f"BF16, unquantized · {h3_aoti.status()}"
101
+ if COND_PIPE is not None:
102
+ import h3_local_conditioner
103
+
104
+ conditioner_status = h3_local_conditioner.status()
105
+ else:
106
+ conditioner_status = f"remote `{CONDITIONER_SPACE}`" + (" (local fallback)" if COND_ERROR else "")
107
  return (
108
  f"Ready · **{engine_status}** · VAEs full precision · placement `{PLACEMENT}` · attention `{ATTENTION}` · "
109
+ f"loaded in {LOADED_IN:.0f}s · conditioner {conditioner_status}"
110
  )
111
 
112
 
113
  def load_models() -> str | None:
114
+ """Load the compact generator and, by default, its local truncated conditioner at startup.
115
 
116
  `MiniMaxH3GeneratorBlocks` declares `transformer`, `vae`, `audio_vae`, the two schedulers and `video_processor`,
117
  so `load_components` fetches exactly those subfolders — `text_encoder/` and `transformer_ref/` are never touched.
118
  Both autoencoders carry `_keep_in_fp32_modules` over every module and stay float32: a bfloat16 audio VAE decodes
119
  the soundtrack roughly 20 dB too quiet.
120
  """
121
+ global PIPE, MANAGER, COND_PIPE, COND_ERROR, LOAD_ERROR, LOADED_IN
122
 
123
  if PIPE is not None or LOAD_ERROR is not None:
124
  return LOAD_ERROR
 
168
  manager.enable_auto_cpu_offload(device="cuda")
169
  _arm_decode_hooks(pipe)
170
 
171
+ cond_pipe = None
172
+ if CONDITIONER_MODE == "local":
173
+ try:
174
+ from h3_local_conditioner import load_local_conditioner
175
+ from h3_split_blocks import MiniMaxH3ConditionerBlocks
176
+
177
+ print("[cond] loading the local truncated NVFP4-AWQ conditioner ...", flush=True)
178
+ text_encoder, tokenizer, processor = load_local_conditioner()
179
+ cond_pipe = MiniMaxH3ConditionerBlocks().init_pipeline(MODEL_REPO)
180
+ cond_pipe.update_components(
181
+ text_encoder=text_encoder,
182
+ tokenizer=tokenizer,
183
+ processor=processor,
184
+ )
185
+ except Exception as error:
186
+ traceback.print_exc()
187
+ COND_ERROR = f"{type(error).__name__}: {error}"
188
+ print(f"[cond] local load failed ({COND_ERROR}); retaining the remote fallback", flush=True)
189
+ elif CONDITIONER_MODE != "remote":
190
+ raise ValueError(f"H3_CONDITIONER_MODE must be `local` or `remote`, got {CONDITIONER_MODE!r}")
191
+
192
+ PIPE, MANAGER, COND_PIPE = pipe, manager, cond_pipe
193
  LOADED_IN = time.time() - started
194
  print(f"[gen] ready in {LOADED_IN:.0f}s", flush=True)
195
  except Exception as error:
 
255
  _PLACEMENT_ALLOWANCE, _PAD = 12, 10
256
 
257
 
258
+ def get_duration(prompt, prompt_embeds, text_token_tags, image, last_image, height, width, num_frames, steps, seed, *a, **k):
259
  height, width, num_frames, steps = int(height), int(width), int(num_frames), int(steps)
260
  latent_frames = (num_frames - LATENTS_PER_CHUNK) // FRAMES_PER_CHUNK * LATENTS_PER_CHUNK + 2
261
  patches = (height // 32) * (width // 32)
262
  rows = latent_frames * patches + (int(image is not None) + int(last_image is not None)) * patches
263
  denoise = steps * (_DUR_B * rows + _DUR_C * rows**2)
264
  decode = _DECODE_BASE + _DECODE_PER_DEFAULT_CANVAS * (height * width * num_frames) / _DEFAULT_CANVAS_PIXELS
265
+ local_conditioning = 20 if prompt_embeds is None else 0
266
+ return max(60, int(denoise + decode) + local_conditioning + _PLACEMENT_ALLOWANCE + _PAD)
267
 
268
 
269
  @spaces.GPU(duration=get_duration, size=GPU_SIZE)
270
+ def _generate(prompt, prompt_embeds, text_token_tags, image, last_image, height, width, num_frames, steps, seed):
271
+ """The only thing on GPU time: local conditioning, packed denoising and the two decoders.
272
 
273
+ Only generated outputs and two timing scalars come back—a `@spaces.GPU` return crosses a process boundary by
274
+ pickling, and the full `PipelineState` still holds packed latents, the rotary grid and row indices on the card.
275
  """
276
  import torch
277
 
278
+ if COND_PIPE is not None and prompt_embeds is None:
279
+ COND_PIPE.text_encoder.to("cuda")
280
  if PLACEMENT == "lazy":
281
  PIPE.to("cuda")
282
  elif PLACEMENT == "pack":
283
  PIPE.vae.to("cuda")
284
  PIPE.audio_vae.to("cuda")
285
 
286
+ condition_seconds = None
287
+ num_text_tokens = None
288
+ if prompt_embeds is None:
289
+ if COND_PIPE is None:
290
+ raise RuntimeError(f"The local conditioner is unavailable: {COND_ERROR or 'disabled'}")
291
+ conditioned = time.time()
292
+ condition_state = COND_PIPE(
293
+ prompt=prompt,
294
+ image=image,
295
+ last_image=last_image,
296
+ height=int(height),
297
+ width=int(width),
298
+ )
299
+ prompt_embeds = condition_state.get("prompt_embeds")
300
+ text_token_tags = condition_state.get("text_token_tags")
301
+ condition_seconds = time.time() - conditioned
302
+ num_text_tokens = int(prompt_embeds.shape[1])
303
+
304
  begin_request = getattr(PIPE.transformer, "begin_request", None)
305
  end_request = getattr(PIPE.transformer, "end_request", None)
306
  if begin_request is not None:
 
321
  finally:
322
  if end_request is not None:
323
  end_request()
324
+ return (
325
+ state.get("videos")[0],
326
+ state.get("audio")[0].cpu(),
327
+ state.get("sampling_rate"),
328
+ condition_seconds,
329
+ num_text_tokens,
330
+ )
331
 
332
 
333
  def generate(prompt, image_path=None, last_image_path=None, canvas=DEFAULT_CANVAS, duration=5, steps=28, seed=42, upsample=False, progress=gr.Progress(track_tqdm=True)):
 
344
  from diffusers.utils import encode_video
345
 
346
  num_frames = snap_frames(duration)
347
+ height, width = CANVASES[canvas]
 
 
 
 
 
 
 
 
348
 
349
  def keyframe(path):
350
+ # Both local conditioner and denoiser receive the same upright RGB source; their resize blocks then apply the
351
+ # same target canvas independently.
352
  return ImageOps.exif_transpose(Image.open(path)).convert("RGB") if path else None
353
 
354
+ first_frame, final_frame = keyframe(image_path), keyframe(last_image_path)
355
+ prompt_embeds = text_token_tags = None
356
+ condition_seconds = None
357
+ num_text_tokens = None
358
+ refined = ""
359
+
360
+ # Prompt rewriting needs the discarded LM head and decoder tail, so it intentionally retains the remote path.
361
+ # Normal generation—the default—keeps embeddings on this worker and never serializes them through another API.
362
+ if upsample or COND_PIPE is None:
363
+ progress(
364
+ 0.0,
365
+ desc=f"Upsampling and conditioning on {CONDITIONER_SPACE} ..."
366
+ if upsample
367
+ else f"Local conditioner unavailable; using {CONDITIONER_SPACE} ...",
368
+ )
369
+ conditioned = time.time()
370
+ prompt_embeds, text_token_tags, metadata, plan = encode_remote(
371
+ prompt, image_path, last_image_path, canvas, num_frames, rewrite_prompt=upsample
372
+ )
373
+ condition_seconds = time.time() - conditioned
374
+ height, width, num_frames = (int(metadata[key]) for key in ("height", "width", "num_frames"))
375
+ num_text_tokens = int(plan["num_text_tokens"])
376
+ refined = plan.get("refined_prompt") or ""
377
+
378
+ progress(
379
+ 0.1,
380
+ desc=("Local conditioning + " if prompt_embeds is None else "")
381
+ + f"denoising {steps} steps at {width}x{height}, {num_frames} frames ...",
382
+ )
383
  started = time.time()
384
+ frames, audio, sampling_rate, local_condition_seconds, local_num_text_tokens = _generate(
385
+ prompt,
386
  prompt_embeds,
387
  text_token_tags,
388
+ first_frame,
389
+ final_frame,
390
  height,
391
  width,
392
  num_frames,
 
394
  seed,
395
  )
396
  generate_seconds = time.time() - started
397
+ if local_condition_seconds is not None:
398
+ condition_seconds = local_condition_seconds
399
+ num_text_tokens = local_num_text_tokens
400
+ denoise_seconds = generate_seconds - (local_condition_seconds or 0.0)
401
 
402
  directory = os.path.join(tempfile.gettempdir(), "h3-outputs")
403
  os.makedirs(directory, exist_ok=True)
 
406
 
407
  report = (
408
  f"`{width}x{height}`, {num_frames} frames ({num_frames / FPS:.3f} s), {int(steps)} steps · "
409
+ f"conditioner {condition_seconds:.0f}s ({num_text_tokens} tokens"
410
  f"{', upsampled' if refined else ''}) · "
411
+ f"denoise + decode {denoise_seconds:.0f}s ({denoise_seconds / int(steps):.1f} s/step) · seed {int(seed)}"
412
  )
413
  print(f"[gen] {report}", flush=True)
414
  return path, report, refined, gr.update(visible=bool(refined))
 
453
 
454
  load_models()
455
 
456
+ INTRO = """# MiniMax-H3 Ultra Fast
457
 
458
  <div align="center">
459
  <a href="https://huggingface.co/MiniMaxAI/MiniMax-H3" target="_blank" rel="noopener"><strong>[ model ]</strong></a> &nbsp;
460
  <a href="https://huggingface.co/lilcheaty/MiniMax-H3-NVFP4" target="_blank" rel="noopener"><strong>[ NVFP4 ]</strong></a> &nbsp;
461
  <a href="https://www.minimax.io/blog/minimax-h3" target="_blank" rel="noopener"><strong>[ blog ]</strong></a> &nbsp;
462
+ <a href="https://huggingface.co/spaces/multimodalart/minimax-h3" target="_blank" rel="noopener"><strong>[ original Space ]</strong></a>
463
  </div>
464
 
465
+ **MiniMax-H3 Ultra Fast** runs a local truncated Qwen3-VL conditioner and the pruned Blackwell-native NVFP4
466
+ transformer with fused QKV, fused Q/K norm + RoPE, full-precision video/audio decoders, and synchronized sound.
467
+ It is optimized from the original [`multimodalart/minimax-h3`](https://huggingface.co/spaces/multimodalart/minimax-h3)
468
+ Space.
469
  """
470
 
471
  CSS = """
 
473
  .dark .gradio-container { color: var(--body-text-color); }
474
  """
475
 
476
+ with gr.Blocks(title="MiniMax-H3 Ultra Fast") as demo:
477
  gr.Markdown(INTRO)
478
 
479
  with gr.Row():
h3_local_conditioner.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Local, truncated Qwen3-VL conditioner for MiniMax-H3.
2
+
3
+ The canonical diffusers checkpoint stores all 64 language layers plus the LM head in BF16 (66.7 GB), although H3
4
+ only reads the unnormalized state after layer 50. ComfyUI's Apache-2.0 conversion removes the unused tail and head,
5
+ keeps the vision tower in BF16, and stores the 50 language layers as NVFP4-AWQ. This adapter loads that single
6
+ 15.7 GB file directly into Transformers' Qwen3-VL architecture and exposes the tiny contract used by diffusers.
7
+
8
+ No ComfyUI application or server is launched. Preprocessing remains Transformers' canonical Qwen3-VL processor.
9
+ By default the checkpoint's quality-oriented weight-only policy is honored: compact NVFP4-AWQ weights are
10
+ dequantized one layer at a time for BF16 GEMMs. Native W4A4 is available as an aggressive opt-in.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import copy
16
+ import os
17
+ from types import SimpleNamespace
18
+
19
+ import torch
20
+ import torch.nn as nn
21
+
22
+ from h3_nvfp4 import H3Linear
23
+
24
+
25
+ CONDITIONER_REPO = os.environ.get("H3_LOCAL_CONDITIONER_REPO", "Comfy-Org/MiniMax-H3")
26
+ CONDITIONER_FILE = os.environ.get(
27
+ "H3_LOCAL_CONDITIONER_FILE",
28
+ "text_encoders/qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors",
29
+ )
30
+ SOURCE_REPO = os.environ.get("H3_MODEL_REPO", "MiniMaxAI/MiniMax-H3")
31
+ LAYERS = 50
32
+ NATIVE_NVFP4 = os.environ.get("H3_CONDITIONER_NATIVE_NVFP4", "0") == "1"
33
+
34
+
35
+ class QuantizedEmbedding(nn.Module):
36
+ """Row-wise INT8 token lookup without dequantizing the 1.56 GB BF16 vocabulary table."""
37
+
38
+ def __init__(self, handle, prefix: str):
39
+ super().__init__()
40
+ self.register_buffer("weight", handle.get_tensor(f"{prefix}.weight"))
41
+ self.register_buffer("scale", handle.get_tensor(f"{prefix}.weight_scale").float())
42
+
43
+ def forward(self, input_ids: torch.Tensor) -> torch.Tensor:
44
+ flat = input_ids.reshape(-1)
45
+ values = self.weight.index_select(0, flat).reshape(*input_ids.shape, self.weight.shape[1])
46
+ scales = self.scale.index_select(0, flat).reshape(*input_ids.shape, 1)
47
+ return values.to(torch.bfloat16).mul_(scales.to(torch.bfloat16))
48
+
49
+
50
+ class Layer50Backbone(nn.Module):
51
+ """Avoid retaining 50 intermediate tensors merely to satisfy diffusers' hidden-state indexing API."""
52
+
53
+ def __init__(self, core: nn.Module):
54
+ super().__init__()
55
+ self.core = core
56
+
57
+ def forward(self, *args, **kwargs):
58
+ kwargs.pop("output_hidden_states", None)
59
+ kwargs.pop("return_dict", None)
60
+ kwargs["use_cache"] = False
61
+ output = self.core(*args, **kwargs)
62
+ # get_qwen3vl_prompt_embeds asks for hidden_states[50]. The first 50 entries need not be materialized.
63
+ return SimpleNamespace(hidden_states=(None,) * LAYERS + (output.last_hidden_state,))
64
+
65
+
66
+ class LocalH3Conditioner(nn.Module):
67
+ """The subset of Qwen3VLForConditionalGeneration that MiniMax-H3 actually calls."""
68
+
69
+ def __init__(self, core: nn.Module, source_config):
70
+ super().__init__()
71
+ public_config = copy.deepcopy(source_config)
72
+ # Diffusers rejects a nominally 50-layer model because a normal last_hidden_state is post-norm. This adapter
73
+ # removes the final norm and returns the raw 50th-layer state, so advertise index 50 as available explicitly.
74
+ public_config.text_config.num_hidden_layers = LAYERS + 1
75
+ self.config = public_config
76
+ self.model = Layer50Backbone(core)
77
+
78
+ @property
79
+ def dtype(self) -> torch.dtype:
80
+ return torch.bfloat16
81
+
82
+ @property
83
+ def device(self) -> torch.device:
84
+ return self.model.core.visual.patch_embed.proj.weight.device
85
+
86
+
87
+ def _target_name(checkpoint_name: str) -> str:
88
+ if checkpoint_name.startswith("model.layers."):
89
+ return "language_model.layers." + checkpoint_name.removeprefix("model.layers.")
90
+ if checkpoint_name.startswith("visual."):
91
+ return checkpoint_name
92
+ raise KeyError(checkpoint_name)
93
+
94
+
95
+ def _build_core(handle):
96
+ from accelerate import init_empty_weights
97
+ from transformers import Qwen3VLConfig
98
+ from transformers.models.qwen3_vl.modeling_qwen3_vl import Qwen3VLModel
99
+
100
+ config = Qwen3VLConfig.from_pretrained(SOURCE_REPO, subfolder="text_encoder")
101
+ config.text_config.num_hidden_layers = LAYERS
102
+ config.text_config.use_cache = False
103
+ config.text_config._attn_implementation = "sdpa"
104
+ config.vision_config._attn_implementation = "sdpa"
105
+
106
+ with init_empty_weights(include_buffers=False):
107
+ core = Qwen3VLModel(config)
108
+
109
+ keys = set(handle.keys())
110
+ embedding_prefix = "model.embed_tokens"
111
+ core.language_model.embed_tokens = QuantizedEmbedding(handle, embedding_prefix)
112
+ consumed = {
113
+ key for key in keys if key == f"{embedding_prefix}.comfy_quant" or key.startswith(f"{embedding_prefix}.weight")
114
+ }
115
+
116
+ quantized_prefixes = sorted(
117
+ key.removesuffix(".comfy_quant")
118
+ for key in keys
119
+ if key.startswith("model.layers.") and key.endswith(".comfy_quant")
120
+ )
121
+ if len(quantized_prefixes) != LAYERS * 7:
122
+ raise RuntimeError(f"Expected {LAYERS * 7} quantized language linears, found {len(quantized_prefixes)}.")
123
+
124
+ for source_prefix in quantized_prefixes:
125
+ target_prefix = _target_name(source_prefix)
126
+ parent_name, child_name = target_prefix.rsplit(".", 1)
127
+ parent = core.get_submodule(parent_name)
128
+ original = getattr(parent, child_name)
129
+ linear = H3Linear(original.in_features, original.out_features, bias=original.bias is not None)
130
+ linear.load(handle, source_prefix)
131
+ if NATIVE_NVFP4:
132
+ linear.full_precision_mm = False
133
+ setattr(parent, child_name, linear)
134
+ consumed.update(key for key in keys if key.startswith(f"{source_prefix}."))
135
+
136
+ # MiniMax-H3 consumes the raw output of layer 49. The released Comfy checkpoint intentionally has no final norm.
137
+ core.language_model.norm = nn.Identity()
138
+
139
+ plain_state = {}
140
+ for source_name in sorted(keys - consumed):
141
+ if source_name.startswith("visual.") or source_name.startswith("model.layers."):
142
+ plain_state[_target_name(source_name)] = handle.get_tensor(source_name)
143
+ consumed.add(source_name)
144
+
145
+ unknown = keys - consumed
146
+ if unknown:
147
+ raise RuntimeError(f"Unhandled local-conditioner tensors: {sorted(unknown)[:12]}")
148
+
149
+ core.load_state_dict(plain_state, strict=False, assign=True)
150
+ meta = [name for name, value in core.named_parameters() if value.is_meta]
151
+ if meta:
152
+ raise RuntimeError(f"Local conditioner still has uninitialized parameters: {meta[:12]}")
153
+ core.eval()
154
+ return core, config
155
+
156
+
157
+ def load_local_conditioner():
158
+ from huggingface_hub import hf_hub_download
159
+ from safetensors import safe_open
160
+ from transformers import Qwen3VLProcessor
161
+
162
+ path = hf_hub_download(CONDITIONER_REPO, CONDITIONER_FILE)
163
+ with safe_open(path, framework="pt", device="cpu") as handle:
164
+ core, config = _build_core(handle)
165
+
166
+ processor = Qwen3VLProcessor.from_pretrained(SOURCE_REPO, subfolder="text_encoder")
167
+ model = LocalH3Conditioner(core, config).eval()
168
+ print(f"[h3-cond] loaded local layer-50 conditioner {CONDITIONER_REPO}/{CONDITIONER_FILE}", flush=True)
169
+ return model, processor.tokenizer, processor
170
+
171
+
172
+ def status() -> str:
173
+ compute = "native W4A4" if NATIVE_NVFP4 else "BF16 GEMM"
174
+ return f"local layer-50 Qwen3-VL NVFP4-AWQ weights / {compute} · `{CONDITIONER_REPO}`"
h3_nvfp4.py CHANGED
@@ -172,6 +172,7 @@ class H3Linear(nn.Module):
172
  self.register_buffer("input_scale", None)
173
  self.register_buffer("pre_quant_scale", None)
174
  self.quantized = False
 
175
 
176
  def load(self, handle, prefix: str) -> None:
177
  config = _quant_config(handle, prefix)
@@ -197,6 +198,7 @@ class H3Linear(nn.Module):
197
  quantized = QuantizedTensor(weight.to(torch.uint8), "TensorCoreNVFP4Layout", params)
198
  self.weight = nn.Parameter(quantized, requires_grad=False)
199
  self.quantized = True
 
200
  for name in ("input_scale", "pre_quant_scale"):
201
  key = f"{prefix}.{name}"
202
  if key in handle.keys():
@@ -224,6 +226,12 @@ class H3Linear(nn.Module):
224
  self.bias,
225
  )
226
 
 
 
 
 
 
 
227
  from comfy_kitchen.tensor import QuantizedTensor
228
 
229
  shape = hidden_states.shape
 
172
  self.register_buffer("input_scale", None)
173
  self.register_buffer("pre_quant_scale", None)
174
  self.quantized = False
175
+ self.full_precision_mm = False
176
 
177
  def load(self, handle, prefix: str) -> None:
178
  config = _quant_config(handle, prefix)
 
198
  quantized = QuantizedTensor(weight.to(torch.uint8), "TensorCoreNVFP4Layout", params)
199
  self.weight = nn.Parameter(quantized, requires_grad=False)
200
  self.quantized = True
201
+ self.full_precision_mm = bool(config.get("full_precision_matrix_mult", False))
202
  for name in ("input_scale", "pre_quant_scale"):
203
  key = f"{prefix}.{name}"
204
  if key in handle.keys():
 
226
  self.bias,
227
  )
228
 
229
+ if self.full_precision_mm:
230
+ # Some AWQ checkpoints use NVFP4 as a compact weight format but deliberately retain BF16 activations and
231
+ # GEMMs. Dequantization is layer-local, so residency stays compact without adding activation error.
232
+ weight = self.weight.dequantize().to(hidden_states.dtype)
233
+ return F.linear(hidden_states, weight, None if self.bias is None else self.bias.to(hidden_states.dtype))
234
+
235
  from comfy_kitchen.tensor import QuantizedTensor
236
 
237
  shape = hidden_states.shape
h3_split_blocks.py CHANGED
@@ -1,8 +1,8 @@
1
- """The halves of a **split** MiniMax-H3 deployment, for both of its checkpoint partitions.
2
 
3
- MiniMax-H3 is 195.9 GiB in bfloat16 and a ZeroGPU Space is evicted at 150 GB of storage, so `MiniMaxH3Blocks` is cut
4
- at its `text_encoder` step: the 62.14 GiB Qwen3-VL runs in the conditioner Space, everything else in a generator
5
- Space, and `prompt_embeds` + `text_token_tags` is the whole wire format between them.
6
 
7
  `resize` / `setup` run on **both** sides: they own no pretrained component, and each half needs the canvas and the
8
  prepared keyframes or normalized references. Both conditioner halves also return the resolved `height` / `width` /
@@ -84,7 +84,7 @@ class MiniMaxH3GeneratorBlocks(SequentialPipelineBlocks):
84
  return (
85
  "The denoising half of a split MiniMax-H3 deployment: the `t2va` / `fl2va` branch of `MiniMaxH3Blocks` "
86
  "without its text-encoder step, so `prompt_embeds` and `text_token_tags` come in as inputs and the "
87
- "62.14 GiB Qwen3-VL conditioner is never loaded here."
88
  )
89
 
90
  @property
@@ -139,7 +139,7 @@ class MiniMaxH3Ref2VAGeneratorBlocks(SequentialPipelineBlocks):
139
  return (
140
  "The denoising half of a split MiniMax-H3 `ref2va` deployment: the `ref2va` branch of `MiniMaxH3Blocks` "
141
  "without its text-encoder step, so `prompt_embeds` and `text_token_tags` come in as inputs and the "
142
- "62.14 GiB Qwen3-VL conditioner is never loaded here. The transformer is the `transformer_ref` partition."
143
  )
144
 
145
  @property
 
1
+ """Composable conditioner and generator halves of MiniMax-H3, for both checkpoint partitions.
2
 
3
+ The blocks cut `MiniMaxH3Blocks` at its `text_encoder` step. They can run in separate Spaces with `prompt_embeds` and
4
+ `text_token_tags` as a wire format, or sequentially in one GPU worker when a compact local conditioner fits beside
5
+ the generator.
6
 
7
  `resize` / `setup` run on **both** sides: they own no pretrained component, and each half needs the canvas and the
8
  prepared keyframes or normalized references. Both conditioner halves also return the resolved `height` / `width` /
 
84
  return (
85
  "The denoising half of a split MiniMax-H3 deployment: the `t2va` / `fl2va` branch of `MiniMaxH3Blocks` "
86
  "without its text-encoder step, so `prompt_embeds` and `text_token_tags` come in as inputs and the "
87
+ "conditioner is supplied by the caller or by the preceding local conditioner half."
88
  )
89
 
90
  @property
 
139
  return (
140
  "The denoising half of a split MiniMax-H3 `ref2va` deployment: the `ref2va` branch of `MiniMaxH3Blocks` "
141
  "without its text-encoder step, so `prompt_embeds` and `text_token_tags` come in as inputs and the "
142
+ "conditioner is supplied by the caller or preceding local half. The transformer is the `transformer_ref` partition."
143
  )
144
 
145
  @property