multimodalart HF Staff commited on
Commit
670ba6b
·
verified ·
1 Parent(s): dd73dc8

Upload h3_local_conditioner.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. 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}`"