Add LoRA loading support for MiniMax-H3 - #14408
Conversation
|
this could be entirely avoided if #14410 would be addressed by the team for future model releases. all of the conversions here are simply counter-productive;
|
sayakpaul
left a comment
There was a problem hiding this comment.
Thanks! Have we tried any of the LoRAs mentioned and inferred with them?
|
|
||
| ## LoRA | ||
|
|
||
| `pipe.load_lora_weights` accepts the diffusers/PEFT format and the two formats real MiniMax-H3 LoRAs actually ship in — [ostris/ai-toolkit](https://github.com/ostris/ai-toolkit)'s `diffusion_model.`-prefixed output and unprefixed original-checkpoint keys — converting the latter two onto the transformer's module names, splitting the fused `attn.qkv_proj` into `to_q` / `to_k` / `to_v` and swapping the two halves of the fused SwiGLU projection. |
There was a problem hiding this comment.
We don't accept the PEFT format though (which includes adapter_config.json.
| pipe.load_components(dtype=torch.bfloat16) | ||
| pipe.to("cuda") | ||
|
|
||
| pipe.load_lora_weights("some-user/some-minimax-h3-lora", weight_name="lora.safetensors", adapter_name="style") |
There was a problem hiding this comment.
Specifying weight_name isn't needed.
| ) | ||
| ``` | ||
|
|
||
| ## LoRA |
| if metadata is None and not any(k.endswith(".alpha") for k in state_dict): | ||
| metadata = {} | ||
| for prefix in (cls.transformer_name, cls.transformer_ref_name): | ||
| component_state_dict = { | ||
| k.removeprefix(f"{prefix}."): v for k, v in state_dict.items() if k.startswith(f"{prefix}.") | ||
| } | ||
| # `^` anchors each pattern to a full module name, as `load_lora_adapter` does for the ranks it derives. | ||
| rank = {f"^{k}": v.shape[1] for k, v in component_state_dict.items() if "lora_B" in k and v.ndim > 1} | ||
| if not rank: | ||
| continue | ||
| lora_config_kwargs = get_peft_kwargs( | ||
| rank, network_alpha_dict=None, peft_state_dict=component_state_dict, is_unet=False | ||
| ) | ||
| # The same fix-up `PeftAdapterMixin.load_lora_adapter` applies to SAI control LoRAs. | ||
| lora_config_kwargs["lora_alpha"] = lora_config_kwargs["r"] | ||
| lora_config_kwargs["alpha_pattern"] = lora_config_kwargs["rank_pattern"] | ||
| metadata.update(_pack_dict_with_prefix(lora_config_kwargs, prefix)) |
There was a problem hiding this comment.
We should be able to infer all of this here:
diffusers/src/diffusers/loaders/peft.py
Line 80 in 9c6a68c
Does it not suffice?
There was a problem hiding this comment.
Nevermind. It's needed to do the alpha-related shenanigans.
|
|
||
| _lora_loadable_modules = ["transformer", "transformer_ref"] | ||
| transformer_name = TRANSFORMER_NAME | ||
| transformer_ref_name = MINIMAX_H3_TRANSFORMER_REF_NAME |
There was a problem hiding this comment.
Do we have a LoRA with this? If not, do we want to remove it?
| ) | ||
|
|
||
| @classmethod | ||
| def load_lora_into_transformer( |
There was a problem hiding this comment.
Could use a "Copied from ..." here?
| if is_peft_available(): | ||
| from peft.utils import get_peft_model_state_dict | ||
|
|
There was a problem hiding this comment.
We can remove this for now. Because LoRA tests should generally be shipped to all the modular pipelines that support LoRAs.
| # and never re-derives it, so a mixed-rank adapter — which the public turbo LoRA is, rank 64 for attention | ||
| # and FFN against rank 16 for the AdaLN projections — has one of its two rank groups silently scaled by |
There was a problem hiding this comment.
Is it rank 64 for attention and FFN and rank 16 for AdaLN? If so, I would clarify this comment.
| if is_non_diffusers_format: | ||
| state_dict = _convert_non_diffusers_minimax_h3_lora_to_diffusers(state_dict) | ||
|
|
||
| # Every published MiniMax-H3 LoRA is alpha-less and applies as `W + lora_B @ lora_A`, i.e. at an effective |
There was a problem hiding this comment.
It's also interesting to see how pipeline-specific this is. I don't think there's a way to faithfully derive this info just from a canonical state dict. @BenjaminBossan thoughts?
There was a problem hiding this comment.
I don't see how this info could possibly derived from the state_dict. The only way to know this is to already know ahead of time that the checkpoint was trained with r == alpha (or that alpha was multiplied into the weights).
If it helps, we could think about allowing lora_alpha=None in PEFT, in which case we assume that it must be equal to r (it's not quite as easy, e.g. what to do about rsLoRA?). That way, there would no longer be the need to set lora_alpha=r everywhere.

Adds
MiniMaxH3LoraLoaderMixinsoMiniMaxH3ModularPipelineloads LoRAs through the standardload_lora_weightspath, plus a load time converter for the non diffusers formats in circulation.What loads
diffusion_model.prefixed, fused projections).larryvrh/MiniMax-H3-Turbo-Lora, the 4 step turbo LoRA, in its original unprefixed layout.num_inference_steps=5matches upstream--steps 4; the two scheduler design already covers its dual schedule sampler, so no custom sampler is needed.InstantX/MiniMax-H3-Turbo-Lora-Diffusers, the pre converted mirror, without the manualnetwork_alphasworkaround its card currently requires.Conversion
The converter renames onto
MiniMaxH3Transformer3DModel, splits the fused QKV LoRA (sharedlora_A, row splitlora_B), and swaps the SwiGLUfc1halves from[gate; value]to the converted base's[value; gate]. Verified against ComfyUI's owncalculate_weightand ai-toolkit's ownmerge_outon real base weights: converted factor matrices are bitwise identical after the layout mapping, and effective weights agree to float accumulation noise (4.4e-7 abs over 3.2e9 elements). The output is also key for key identical to InstantX's independent conversion.Because these files carry mixed ranks (64 for attention/FFN, 16 for AdaLN) and no alpha keys, the loader synthesizes
alpha == rankmetadata whenever a state dict brings no alpha information of its own, per component, so every module applies at the intended scale 1.0. Without it the global alpha default mis-scales such adapters (rank 64 modules at 0.25x here); that underlying issue inget_peft_kwargsis general and is fixed separately in #14409.Both transformer partitions are supported and routed by prefix (
transformer,transformer_ref), including pipelines loaded withworkflow="ref2va"where onlytransformer_refexists.Notes
fuse_lora()into bfloat16 is lossy for adapters this small relative to the base weights; the docs note to prefer the default unfused path.