Spaces:
Running on Zero
Running on Zero
Upload h3_local_conditioner.py with huggingface_hub
Browse files- h3_local_conditioner.py +174 -0
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}`"
|