Spaces:
Running on Zero
Running on Zero
| """Local, truncated Qwen3-VL conditioner for MiniMax-H3. | |
| The canonical diffusers checkpoint stores all 64 language layers plus the LM head in BF16 (66.7 GB), although H3 | |
| only reads the unnormalized state after layer 50. ComfyUI's Apache-2.0 conversion removes the unused tail and head, | |
| keeps the vision tower in BF16, and stores the 50 language layers as NVFP4-AWQ. This adapter loads that single | |
| 15.7 GB file directly into Transformers' Qwen3-VL architecture and exposes the tiny contract used by diffusers. | |
| No ComfyUI application or server is launched. Preprocessing remains Transformers' canonical Qwen3-VL processor. | |
| By default the checkpoint's quality-oriented weight-only policy is honored: compact NVFP4-AWQ weights are | |
| dequantized one layer at a time for BF16 GEMMs. Native W4A4 is available as an aggressive opt-in. | |
| """ | |
| from __future__ import annotations | |
| import copy | |
| import os | |
| from types import SimpleNamespace | |
| import torch | |
| import torch.nn as nn | |
| from h3_nvfp4 import H3Linear | |
| CONDITIONER_REPO = os.environ.get("H3_LOCAL_CONDITIONER_REPO", "Comfy-Org/MiniMax-H3") | |
| CONDITIONER_FILE = os.environ.get( | |
| "H3_LOCAL_CONDITIONER_FILE", | |
| "text_encoders/qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors", | |
| ) | |
| SOURCE_REPO = os.environ.get("H3_MODEL_REPO", "MiniMaxAI/MiniMax-H3") | |
| LAYERS = 50 | |
| NATIVE_NVFP4 = os.environ.get("H3_CONDITIONER_NATIVE_NVFP4", "0") == "1" | |
| class QuantizedEmbedding(nn.Module): | |
| """Row-wise INT8 token lookup without dequantizing the 1.56 GB BF16 vocabulary table.""" | |
| def __init__(self, handle, prefix: str): | |
| super().__init__() | |
| self.register_buffer("weight", handle.get_tensor(f"{prefix}.weight")) | |
| self.register_buffer("scale", handle.get_tensor(f"{prefix}.weight_scale").float()) | |
| def forward(self, input_ids: torch.Tensor) -> torch.Tensor: | |
| flat = input_ids.reshape(-1) | |
| values = self.weight.index_select(0, flat).reshape(*input_ids.shape, self.weight.shape[1]) | |
| scales = self.scale.index_select(0, flat).reshape(*input_ids.shape, 1) | |
| return values.to(torch.bfloat16).mul_(scales.to(torch.bfloat16)) | |
| class Layer50Backbone(nn.Module): | |
| """Avoid retaining 50 intermediate tensors merely to satisfy diffusers' hidden-state indexing API.""" | |
| def __init__(self, core: nn.Module): | |
| super().__init__() | |
| self.core = core | |
| def forward(self, *args, **kwargs): | |
| kwargs.pop("output_hidden_states", None) | |
| kwargs.pop("return_dict", None) | |
| kwargs["use_cache"] = False | |
| output = self.core(*args, **kwargs) | |
| # get_qwen3vl_prompt_embeds asks for hidden_states[50]. The first 50 entries need not be materialized. | |
| return SimpleNamespace(hidden_states=(None,) * LAYERS + (output.last_hidden_state,)) | |
| class LocalH3Conditioner(nn.Module): | |
| """The subset of Qwen3VLForConditionalGeneration that MiniMax-H3 actually calls.""" | |
| def __init__(self, core: nn.Module, source_config): | |
| super().__init__() | |
| public_config = copy.deepcopy(source_config) | |
| # Diffusers rejects a nominally 50-layer model because a normal last_hidden_state is post-norm. This adapter | |
| # removes the final norm and returns the raw 50th-layer state, so advertise index 50 as available explicitly. | |
| public_config.text_config.num_hidden_layers = LAYERS + 1 | |
| self.config = public_config | |
| self.model = Layer50Backbone(core) | |
| def dtype(self) -> torch.dtype: | |
| return torch.bfloat16 | |
| def device(self) -> torch.device: | |
| return self.model.core.visual.patch_embed.proj.weight.device | |
| def _target_name(checkpoint_name: str) -> str: | |
| if checkpoint_name.startswith("model.layers."): | |
| return "language_model.layers." + checkpoint_name.removeprefix("model.layers.") | |
| if checkpoint_name.startswith("visual."): | |
| return checkpoint_name | |
| raise KeyError(checkpoint_name) | |
| def _build_core(handle): | |
| from accelerate import init_empty_weights | |
| from transformers import Qwen3VLConfig | |
| from transformers.models.qwen3_vl.modeling_qwen3_vl import Qwen3VLModel | |
| config = Qwen3VLConfig.from_pretrained(SOURCE_REPO, subfolder="text_encoder") | |
| config.text_config.num_hidden_layers = LAYERS | |
| config.text_config.use_cache = False | |
| config.text_config._attn_implementation = "sdpa" | |
| config.vision_config._attn_implementation = "sdpa" | |
| with init_empty_weights(include_buffers=False): | |
| core = Qwen3VLModel(config) | |
| keys = set(handle.keys()) | |
| embedding_prefix = "model.embed_tokens" | |
| core.language_model.embed_tokens = QuantizedEmbedding(handle, embedding_prefix) | |
| consumed = { | |
| key for key in keys if key == f"{embedding_prefix}.comfy_quant" or key.startswith(f"{embedding_prefix}.weight") | |
| } | |
| quantized_prefixes = sorted( | |
| key.removesuffix(".comfy_quant") | |
| for key in keys | |
| if key.startswith("model.layers.") and key.endswith(".comfy_quant") | |
| ) | |
| if len(quantized_prefixes) != LAYERS * 7: | |
| raise RuntimeError(f"Expected {LAYERS * 7} quantized language linears, found {len(quantized_prefixes)}.") | |
| for source_prefix in quantized_prefixes: | |
| target_prefix = _target_name(source_prefix) | |
| parent_name, child_name = target_prefix.rsplit(".", 1) | |
| parent = core.get_submodule(parent_name) | |
| original = getattr(parent, child_name) | |
| linear = H3Linear(original.in_features, original.out_features, bias=original.bias is not None) | |
| linear.load(handle, source_prefix) | |
| if NATIVE_NVFP4: | |
| linear.full_precision_mm = False | |
| setattr(parent, child_name, linear) | |
| consumed.update(key for key in keys if key.startswith(f"{source_prefix}.")) | |
| # MiniMax-H3 consumes the raw output of layer 49. The released Comfy checkpoint intentionally has no final norm. | |
| core.language_model.norm = nn.Identity() | |
| plain_state = {} | |
| for source_name in sorted(keys - consumed): | |
| if source_name.startswith("visual.") or source_name.startswith("model.layers."): | |
| plain_state[_target_name(source_name)] = handle.get_tensor(source_name) | |
| consumed.add(source_name) | |
| unknown = keys - consumed | |
| if unknown: | |
| raise RuntimeError(f"Unhandled local-conditioner tensors: {sorted(unknown)[:12]}") | |
| core.load_state_dict(plain_state, strict=False, assign=True) | |
| meta = [name for name, value in core.named_parameters() if value.is_meta] | |
| if meta: | |
| raise RuntimeError(f"Local conditioner still has uninitialized parameters: {meta[:12]}") | |
| core.eval() | |
| return core, config | |
| def load_local_conditioner(): | |
| from huggingface_hub import hf_hub_download | |
| from safetensors import safe_open | |
| from transformers import Qwen3VLProcessor | |
| path = hf_hub_download(CONDITIONER_REPO, CONDITIONER_FILE) | |
| with safe_open(path, framework="pt", device="cpu") as handle: | |
| core, config = _build_core(handle) | |
| processor = Qwen3VLProcessor.from_pretrained(SOURCE_REPO, subfolder="text_encoder") | |
| model = LocalH3Conditioner(core, config).eval() | |
| print(f"[h3-cond] loaded local layer-50 conditioner {CONDITIONER_REPO}/{CONDITIONER_FILE}", flush=True) | |
| return model, processor.tokenizer, processor | |
| def status() -> str: | |
| compute = "native W4A4" if NATIVE_NVFP4 else "BF16 GEMM" | |
| return f"local layer-50 Qwen3-VL NVFP4-AWQ weights / {compute} · `{CONDITIONER_REPO}`" | |