-
Notifications
You must be signed in to change notification settings - Fork 15.4k
Minimax h3 controlnet as a model patch instead of a controlnet. #15975
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| """MiniMax H3 Fun ControlNet-Union model patch.""" | ||
|
|
||
| import torch | ||
| import torch.nn as nn | ||
|
|
||
| import comfy.ldm.common_dit | ||
| from .model import DiTBlock, patchify_video | ||
|
|
||
|
|
||
| class ControlDiTBlock(DiTBlock): | ||
| def __init__(self, hidden, heads, head_dim, ffn, t_dim, eps, qk_eps, first_block=False, | ||
| apply_silu=True, adaln_dtype=None, dtype=None, device=None, operations=None): | ||
| super().__init__(hidden, heads, head_dim, ffn, t_dim, eps, qk_eps, apply_silu=apply_silu, | ||
| adaln_dtype=adaln_dtype, dtype=dtype, device=device, operations=operations) | ||
| if first_block: | ||
| self.before_proj = operations.Linear(hidden, hidden, bias=True, dtype=dtype, device=device) | ||
| self.after_proj = operations.Linear(hidden, hidden, bias=True, dtype=dtype, device=device) | ||
|
|
||
|
|
||
| class MiniMaxH3FunControl(torch.nn.Module): | ||
| def __init__(self, control_in_dim=49, injection_layers=(0, 10, 20, 30, 40), hidden_size=5376, | ||
| num_attention_heads=56, attention_head_dim=128, ffn_hidden_size=14336, | ||
| time_embed_dim=2688, patch_size=(1, 2, 2), norm_eps=1e-5, qk_norm_eps=1e-5, | ||
| use_adaln_curves=False, dtype=None, device=None, operations=None): | ||
| super().__init__() | ||
| self.dtype = dtype | ||
| self.patch_size = tuple(patch_size) | ||
| self.injection_layers = tuple(injection_layers) | ||
| if not self.injection_layers or self.injection_layers[0] != 0: | ||
| raise ValueError("MiniMax H3 Fun control injection layers must start at layer 0") | ||
| if self.injection_layers != tuple(sorted(set(self.injection_layers))): | ||
| raise ValueError("MiniMax H3 Fun control injection layers must be unique and increasing") | ||
| self.control_in_dim = control_in_dim | ||
| patch_dim = control_in_dim * self.patch_size[0] * self.patch_size[1] * self.patch_size[2] | ||
| self.control_proj_in = operations.Linear(patch_dim, hidden_size, bias=True, dtype=torch.float32, device=device) | ||
| self.control_blocks = nn.ModuleList([ | ||
| ControlDiTBlock(hidden_size, num_attention_heads, attention_head_dim, ffn_hidden_size, | ||
| time_embed_dim, norm_eps, qk_norm_eps, first_block=(i == 0), | ||
| apply_silu=not use_adaln_curves, | ||
| adaln_dtype=torch.float32 if use_adaln_curves else dtype, | ||
| dtype=dtype, device=device, operations=operations) | ||
| for i in range(len(self.injection_layers))]) | ||
|
|
||
| def init_stream(self, h, control_latent, layout, t_emb): | ||
| if any(kind not in ("text", "audio", "video") for _, _, kind in layout.segments): | ||
| raise ValueError("MiniMax H3 Fun ControlNet does not support keyframe or reference conditioning") | ||
| adaln_in = self.control_blocks[0].adaln_proj.linear.in_features | ||
| if t_emb.shape[-1] != adaln_in: | ||
| raise RuntimeError( | ||
| "MiniMax H3 controlnet adaln width {} does not match the base model's timestep embedding width {}: " | ||
| "the controlnet and base checkpoint use different adaln forms (curve basis vs full), " | ||
| "convert the controlnet to match the base model.".format(adaln_in, t_emb.shape[-1])) | ||
|
|
||
| patch_dim = self.control_in_dim * self.patch_size[0] * self.patch_size[1] * self.patch_size[2] | ||
| control_latent = comfy.ldm.common_dit.pad_to_patch_size(control_latent.to(torch.float32), self.patch_size) | ||
| target_rows = patchify_video(control_latent, self.patch_size) | ||
| if target_rows.shape[1] < patch_dim: | ||
| target_rows = torch.nn.functional.pad(target_rows, (0, patch_dim - target_rows.shape[1])) | ||
| elif target_rows.shape[1] > patch_dim: | ||
| raise ValueError("MiniMax H3 control input has {} columns but the model patch expects {}".format(target_rows.shape[1], patch_dim)) | ||
|
|
||
| c = h.clone() | ||
| c[layout.img_pos.to(h.device)] = self.control_proj_in(target_rows).to(h.dtype) | ||
| return self.control_blocks[0].before_proj(c).add_(h) | ||
|
|
||
| def step(self, index, c, t_emb, mod_segments, rope_freqs, transformer_options): | ||
| block = self.control_blocks[index] | ||
| c = DiTBlock.forward(block, c, t_emb, mod_segments, rope_freqs, transformer_options=transformer_options) | ||
| return c, block.after_proj(c) | ||
|
|
||
|
|
||
| def is_minimax_h3_fun_state_dict(state_dict): | ||
| required = ( | ||
| "control_proj_in.weight", | ||
| "control_blocks.0.adaln_proj.linear.weight", | ||
| "control_blocks.0.after_proj.weight", | ||
| "control_blocks.0.before_proj.weight", | ||
| "control_blocks.0.attn.qkv_proj.weight", | ||
| "control_blocks.0.attn.q_norm.weight", | ||
| "control_blocks.0.mlp.fc1.weight", | ||
| ) | ||
| return all(key in state_dict for key in required) | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,3 +1,5 @@ | ||||||||||
| import json | ||||||||||
|
|
||||||||||
| import torch | ||||||||||
| from torch import nn | ||||||||||
| import folder_paths | ||||||||||
|
|
@@ -9,6 +11,7 @@ | |||||||||
| import comfy.ldm.lumina.controlnet | ||||||||||
| import comfy.ldm.supir.supir_modules | ||||||||||
| import comfy.ldm.anima.lllite | ||||||||||
| import comfy.ldm.minimax.controlnet | ||||||||||
| import comfy.ldm.wan.uni3c | ||||||||||
| import comfy.ldm.lightricks.duration_head | ||||||||||
| from comfy.ldm.wan.model_multitalk import WanMultiTalkAttentionBlock, MultiTalkAudioProjModel | ||||||||||
|
|
@@ -266,6 +269,48 @@ def load_model_patch(self, name): | |||||||||
| if torch.count_nonzero(ref_weight) == 0: | ||||||||||
| config['broken'] = True | ||||||||||
| model = comfy.ldm.lumina.controlnet.ZImage_Control(device=comfy.model_management.unet_offload_device(), dtype=dtype, operations=comfy.ops.manual_cast, **config) | ||||||||||
| elif comfy.ldm.minimax.controlnet.is_minimax_h3_fun_state_dict(sd): | ||||||||||
| load_device = comfy.model_management.get_torch_device() | ||||||||||
| quant = comfy.utils.detect_layer_quantization(sd, "") | ||||||||||
| if quant is not None: | ||||||||||
| dtype = torch.bfloat16 | ||||||||||
| operations = comfy.ops.mixed_precision_ops(quant, dtype) | ||||||||||
| else: | ||||||||||
| dtype = comfy.model_management.unet_dtype( | ||||||||||
| model_params=-1, | ||||||||||
| supported_dtypes=[torch.bfloat16, torch.float32], | ||||||||||
| weight_dtype=comfy.utils.weight_dtype(sd), | ||||||||||
| ) | ||||||||||
| manual_cast_dtype = comfy.model_management.unet_manual_cast( | ||||||||||
| dtype, load_device, supported_dtypes=[torch.bfloat16, torch.float32]) | ||||||||||
| operations = comfy.ops.pick_operations(dtype, manual_cast_dtype) | ||||||||||
|
|
||||||||||
| num_blocks = 0 | ||||||||||
| while "control_blocks.{}.after_proj.weight".format(num_blocks) in sd: | ||||||||||
| num_blocks += 1 | ||||||||||
| injection_layers = tuple(range(0, num_blocks * 10, 10)) | ||||||||||
| if metadata is not None and "control_blocks_places" in metadata: | ||||||||||
| injection_layers = tuple(json.loads(metadata["control_blocks_places"])) | ||||||||||
| if len(injection_layers) != num_blocks: | ||||||||||
| raise ValueError("MiniMax H3 Fun control_blocks_places metadata does not match the checkpoint") | ||||||||||
| qkv = sd["control_blocks.0.attn.qkv_proj.weight"] | ||||||||||
| head_dim = sd["control_blocks.0.attn.q_norm.weight"].shape[0] | ||||||||||
| use_adaln_curves = metadata is not None and metadata.get("minimax_h3_fun_controlnet") == "adaln_basis" | ||||||||||
| time_embed_dim = 8 if use_adaln_curves else 2688 | ||||||||||
|
Comment on lines
+298
to
+299
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win Derive Every other dimension in this branch comes from the weights. The adaln input width is available directly as ♻️ Proposed refactor use_adaln_curves = metadata is not None and metadata.get("minimax_h3_fun_controlnet") == "adaln_basis"
- time_embed_dim = 8 if use_adaln_curves else 2688
+ time_embed_dim = sd["control_blocks.0.adaln_proj.linear.weight"].shape[1]📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| model = comfy.ldm.minimax.controlnet.MiniMaxH3FunControl( | ||||||||||
| control_in_dim=49, | ||||||||||
| injection_layers=injection_layers, | ||||||||||
| hidden_size=sd["control_proj_in.weight"].shape[0], | ||||||||||
| num_attention_heads=qkv.shape[0] // (3 * head_dim), | ||||||||||
| attention_head_dim=head_dim, | ||||||||||
| ffn_hidden_size=sd["control_blocks.0.mlp.fc1.weight"].shape[0] // 2, | ||||||||||
| time_embed_dim=time_embed_dim, | ||||||||||
| use_adaln_curves=use_adaln_curves, | ||||||||||
| operations=operations, | ||||||||||
| device=comfy.model_management.unet_offload_device(), | ||||||||||
| dtype=dtype, | ||||||||||
| ) | ||||||||||
| model.requires_grad_(False) | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Remove the AGENTS.md forbids adding inference-mode freeze logic to model loading. No other branch in As per coding guidelines: "Do not add 🧹 Proposed fix )
- model.requires_grad_(False)
elif 'controlnet_patch_embedding.weight' in sd: # Uni3C controlnet for Wan📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSources: Coding guidelines, Path instructions |
||||||||||
| elif 'controlnet_patch_embedding.weight' in sd: # Uni3C controlnet for Wan | ||||||||||
| attn_key_replace = {".self_attn.to_q.": ".self_attn.q.", | ||||||||||
| ".self_attn.to_k.": ".self_attn.k.", | ||||||||||
|
|
||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Keep Diffusers detection and conversion aligned with the loader.
At Lines 73-82, the detector accepts only native
qkv_proj,q_norm, andmlp.fc1keys. The model-patch loader calls this detector and then reads only those native keys. Diffusers checkpoints therefore skip this branch and cannot load through the new model-patch path. Restore the Diffusers key alternative and conversion, or remove that format from the supported scope and documentation.🤖 Prompt for AI Agents