Spaces:
Running on Zero
Running on Zero
Commit ·
a4e1031
1
Parent(s): eb09060
Localize the H3 conditioner
Browse files- README.md +26 -13
- app.py +119 -35
- h3_local_conditioner.py +174 -0
- h3_nvfp4.py +8 -0
- h3_split_blocks.py +6 -6
README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
---
|
| 2 |
-
title: MiniMax
|
| 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:
|
| 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 |
-
##
|
| 71 |
|
| 72 |
-
The
|
|
|
|
| 73 |
|
| 74 |
| component | where it runs | precision / format |
|
| 75 |
|---|---|---|
|
| 76 |
-
| Qwen3-VL layer-50 conditioner |
|
| 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
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
| 84 |
|
| 85 |
## Kernel path
|
| 86 |
|
|
@@ -105,7 +110,8 @@ not the default.
|
|
| 105 |
|
| 106 |
## Quality trade-off
|
| 107 |
|
| 108 |
-
|
|
|
|
| 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 |
-
| `
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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`
|
| 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 (~
|
| 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
|
| 101 |
)
|
| 102 |
|
| 103 |
|
| 104 |
def load_models() -> str | None:
|
| 105 |
-
"""Load the
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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:
|
| 241 |
|
| 242 |
-
Only
|
| 243 |
-
the full `PipelineState` still holds
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 302 |
-
#
|
| 303 |
return ImageOps.exif_transpose(Image.open(path)).convert("RGB") if path else None
|
| 304 |
|
| 305 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
started = time.time()
|
| 307 |
-
frames, audio, sampling_rate = _generate(
|
|
|
|
| 308 |
prompt_embeds,
|
| 309 |
text_token_tags,
|
| 310 |
-
|
| 311 |
-
|
| 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 ({
|
| 328 |
f"{', upsampled' if refined else ''}) · "
|
| 329 |
-
f"denoise + decode {
|
| 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>
|
| 378 |
<a href="https://huggingface.co/lilcheaty/MiniMax-H3-NVFP4" target="_blank" rel="noopener"><strong>[ NVFP4 ]</strong></a>
|
| 379 |
<a href="https://www.minimax.io/blog/minimax-h3" target="_blank" rel="noopener"><strong>[ blog ]</strong></a>
|
| 380 |
-
<a href="https://huggingface.co/spaces/multimodalart/minimax-h3
|
| 381 |
</div>
|
| 382 |
|
| 383 |
-
**MiniMax-H3 Ultra** runs
|
| 384 |
-
full-precision video/audio decoders, and
|
|
|
|
|
|
|
| 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>
|
| 460 |
<a href="https://huggingface.co/lilcheaty/MiniMax-H3-NVFP4" target="_blank" rel="noopener"><strong>[ NVFP4 ]</strong></a>
|
| 461 |
<a href="https://www.minimax.io/blog/minimax-h3" target="_blank" rel="noopener"><strong>[ blog ]</strong></a>
|
| 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 |
-
"""
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 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 |
-
"
|
| 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 |
-
"
|
| 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
|