multimodalart HF Staff commited on
Commit
d3ff0e3
·
verified ·
1 Parent(s): 81fb7f5

Project released-checkpoint AdaLN LoRAs onto the pruned timestep coordinates

Browse files
README.md CHANGED
@@ -21,7 +21,7 @@ tags:
21
 
22
  [MiniMaxAI/MiniMax-H3](https://huggingface.co/MiniMaxAI/MiniMax-H3) in diffusers format with the AdaLN input
23
  projections reduced to their reachable rank. **52 GB smaller** across the two DiT partitions, no visible change in
24
- output, and pruned-trained LoRAs load natively.
25
 
26
  | | released | this repo |
27
  |---|---|---|
@@ -131,9 +131,13 @@ be paged in and out per step. On a card with room for all of it, expect the two
131
 
132
  ## LoRAs
133
 
134
- **Pruned-trained LoRAs load natively here.** ai-toolkit trains against a pruned checkpoint by default, so its H3
135
- LoRAs carry `adaln_proj.linear.lora_A` of shape `[rank, 8]`. That is a size mismatch against the released
136
- `Linear(2688, ...)` and an exact fit here.
 
 
 
 
137
 
138
  This works because the coordinates are **not ours**. A rank-8 basis is unique only up to sign and rotation, so a
139
  pruned-trained LoRA is portable only between checkpoints that share the coordinate convention. Rather than derive a
@@ -154,11 +158,71 @@ The last row is why the table is copied and not recomputed: a re-derived basis s
154
  accurate, and would still turn every existing LoRA into noise. Both files load with all 50 AdaLN projections wrapped
155
  by PEFT at scale exactly 1.0, 362 modules in total.
156
 
157
- The reverse does not hold: LoRAs trained against the **released** checkpoint's 2688-wide AdaLN projections, such as
158
- the official 4-step turbo LoRA, do **not** load here. Their `adaln_proj` updates live in the full timestep-embedding
159
- space and cannot be mapped onto the 8-wide one without the training-time activations. Use those against
160
- `MiniMaxAI/MiniMax-H3`. Everything outside `adaln_proj` - attention, feed-forward, the token refiner - is untouched
161
- by the pruning and loads either way.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
 
163
  LoRA loading for MiniMax-H3 landed in diffusers via
164
  [huggingface/diffusers#14408](https://github.com/huggingface/diffusers/pull/14408). This repo declares the stock
@@ -288,7 +352,12 @@ ComfyUI's `*_int8_convrot` files quantize, arrived at from their tensor inventor
288
  `MiniMaxH3ConvRotLinear` is a plain `nn.Linear` subclass, so PEFT wraps it as a `base_layer` and a LoRA's own
289
  branch reads the **unrotated** input in bfloat16. A LoRA trained against the ordinary checkpoint is therefore
290
  correct on top of a rotated, quantized base - and `adaln_proj`, which is where pruned-trained LoRAs put most of
291
- their weight, is never rotated or quantized at all.
 
 
 
 
 
292
 
293
  The order matters, and there is one thing not to do:
294
 
 
21
 
22
  [MiniMaxAI/MiniMax-H3](https://huggingface.co/MiniMaxAI/MiniMax-H3) in diffusers format with the AdaLN input
23
  projections reduced to their reachable rank. **52 GB smaller** across the two DiT partitions, no visible change in
24
+ output, and every published H3 LoRA loads - pruned-trained ones natively, released-trained ones projected.
25
 
26
  | | released | this repo |
27
  |---|---|---|
 
131
 
132
  ## LoRAs
133
 
134
+ **Every published H3 LoRA loads here** - the ones trained against a pruned checkpoint natively, the ones trained
135
+ against the released one by projection.
136
+
137
+ ### Pruned-trained LoRAs, natively
138
+
139
+ ai-toolkit trains against a pruned checkpoint by default, so its H3 LoRAs carry `adaln_proj.linear.lora_A` of shape
140
+ `[rank, 8]`. That is a size mismatch against the released `Linear(2688, ...)` and an exact fit here.
141
 
142
  This works because the coordinates are **not ours**. A rank-8 basis is unique only up to sign and rotation, so a
143
  pruned-trained LoRA is portable only between checkpoints that share the coordinate convention. Rather than derive a
 
158
  accurate, and would still turn every existing LoRA into noise. Both files load with all 50 AdaLN projections wrapped
159
  by PEFT at scale exactly 1.0, 362 modules in total.
160
 
161
+ ### Released-trained LoRAs, by projection
162
+
163
+ LoRAs trained against the **released** checkpoint's 2688-wide AdaLN projections - the 4-step turbo LoRA and its
164
+ conversions - are mapped onto these coordinates when they load. Nothing about them is approximated beyond what the
165
+ pruning already approximates: the identity that folded the weights folds the adapter.
166
+
167
+ A LoRA on the released projection contributes `lora_B @ (lora_A @ x)`, and `x = mean + c @ basis`, so
168
+
169
+ ```
170
+ lora_B @ (lora_A @ x) = lora_B @ ((lora_A @ basis.T) @ c) + lora_B @ (lora_A @ mean)
171
+ ```
172
+
173
+ `lora_A @ basis.T` is an `[rank, 8]` `lora_A` over the pruned coordinates, at the same rank; `lora_B` is unchanged;
174
+ and what is left over is a **constant**, which no `Linear(8 -> ...)` can produce. That constant is carried as a
175
+ float32 offset on the modulation, alongside the fold's own `folded_bias` and for the same reason - it is ~98% of
176
+ what these adapters do to the AdaLN path, so rounding it into bfloat16 would spend a full rounding step of the
177
+ modulation on almost the whole update. The factors are computed in float64.
178
+
179
+ `adaln_basis` and `adaln_mean` ship as `adaln_affine.safetensors` (97 KB) beside each partition's weights. They are
180
+ the map the projections were folded with, read back, not re-derived - for the same reason the table is copied and
181
+ not recomputed.
182
+
183
+ **Why not PEFT's `lora_bias`.** That is the obvious place to put a constant, and it is the wrong one here.
184
+ `lora_bias` is a `LoraConfig` flag, not a per-module one, so switching it on for the 51 AdaLN modules also switches
185
+ it on for the other 312: it warns on every bias-free target, creates zero biases on the attention and feed-forward
186
+ modules that then read as missing keys, and makes `fuse_lora` raise `Impossible to merge LoRA with lora_bias=True
187
+ because the base layer has no bias` on all 51 - `adaln_proj.linear` is deliberately bias-free, which is what makes
188
+ PEFT wrap it exactly as it wraps the released projection. It would also put the constant in bfloat16, on the wrong
189
+ side of the argument this repo already made for `folded_bias`. The float32 buffer beside `folded_bias` avoids all
190
+ four, and PEFT still owns the state that matters: the offsets are read through `active_adapters`, `scaling` and
191
+ `disable_adapters`, so they follow `set_adapters`, `disable_lora` and `delete_adapters` on their own.
192
+
193
+ Measured per AdaLN module over 200 off-grid timesteps, projected against full-space, both in float64:
194
+
195
+ | | worst of 51 | mean of 51 |
196
+ |---|---|---|
197
+ | rank-8 subspace residual on the timestep curve | 1.47e-5 | - |
198
+ | [`larryvrh/MiniMax-H3-Turbo-Lora`](https://huggingface.co/larryvrh/MiniMax-H3-Turbo-Lora) v4 step 600 EMA | **2.05e-5** | 1.26e-5 |
199
+ | [`InstantX/MiniMax-H3-Turbo-Lora-Diffusers`](https://huggingface.co/InstantX/MiniMax-H3-Turbo-Lora-Diffusers) | **1.53e-5** | 0.88e-5 |
200
+
201
+ which is the subspace residual and nothing else. Both files attach to **363 modules** - 312 attention/feed-forward
202
+ and token-refiner ones untouched, 51 AdaLN ones projected - all at scale exactly 1.0. Loaded on top of the released
203
+ weights and on top of these, the LoRA'd AdaLN function was compared against an exact float64 evaluation at three
204
+ probe modules: the pruned side is the closer of the two at all three, the same way the fold itself is.
205
+
206
+ End to end, 960x544, 124 frames, 7 steps, seed 42, `larryvrh` v4, against the identical request on the released
207
+ weights: **cosine 0.9958** with the LoRA on and **0.9992** with it off, and the on/off difference - the turbo effect
208
+ itself - is 0.9609 here against 0.9641 there. Frame for frame the two runs are the same video.
209
+
210
+ How much of that is the AdaLN half? On these particular files, not much - their AdaLN update is about 2e-4 of the
211
+ modulation, and simply *discarding* those 51 modules lands at 0.9960 against the released run where projecting
212
+ lands at 0.9958, which is the same number twice. What is not the same number is projecting them and dropping the
213
+ constant term: **0.9947**. The half-applied version is the only one of the three that is measurably wrong, which
214
+ is both the argument for where the constant is kept and the reason to do this exactly rather than approximately -
215
+ an adapter that leans harder on AdaLN than a turbo LoRA does has no other way to arrive intact.
216
+
217
+ A file whose AdaLN modules are not uniformly 8-wide or uniformly 2688-wide raises rather than loading some of them:
218
+ an adapter that applies to 50 of its 51 AdaLN projections is not the adapter anyone trained.
219
+
220
+ One caveat: the projection happens at load, and the constant terms are not PEFT parameters, so
221
+ `save_lora_weights` on a projected adapter writes the projected factors *without* them. Distribute the original
222
+ file, not a re-save of it.
223
+
224
+ Everything outside `adaln_proj` and `norm_out` - attention, feed-forward, the token refiner - is untouched by the
225
+ pruning and was always loading either way.
226
 
227
  LoRA loading for MiniMax-H3 landed in diffusers via
228
  [huggingface/diffusers#14408](https://github.com/huggingface/diffusers/pull/14408). This repo declares the stock
 
352
  `MiniMaxH3ConvRotLinear` is a plain `nn.Linear` subclass, so PEFT wraps it as a `base_layer` and a LoRA's own
353
  branch reads the **unrotated** input in bfloat16. A LoRA trained against the ordinary checkpoint is therefore
354
  correct on top of a rotated, quantized base - and `adaln_proj`, which is where pruned-trained LoRAs put most of
355
+ their weight and where a released-trained one is projected, is never rotated or quantized at all.
356
+
357
+ The order below is unchanged by the AdaLN projection: the projection happens at load, before anything is rotated,
358
+ and neither `enable_convrot` nor `quantize_` touches the modules it wrote to. Verified on `larryvrh` v4 - the
359
+ AdaLN update at three probe modules is bit-identical before and after quantizing, with all 363 modules and all 51
360
+ constant terms still attached.
361
 
362
  The order matters, and there is one thing not to do:
363
 
transformer/adaln_affine.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:34f285e7aeae741666868bf5912506ab4793098357a01a9d8358f6ce7704d532
3
+ size 96960
transformer/diffusion_pytorch_model.safetensors.index.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "metadata": {
3
- "total_size": 40235366432
4
  },
5
  "weight_map": {
6
  "audio_proj_in.bias": "diffusion_pytorch_model-00001-of-00014.safetensors",
@@ -637,6 +637,8 @@
637
  "transformer_blocks.9.ff.net.0.proj.weight": "diffusion_pytorch_model-00003-of-00014.safetensors",
638
  "transformer_blocks.9.ff.net.2.weight": "diffusion_pytorch_model-00003-of-00014.safetensors",
639
  "transformer_blocks.9.norm1.weight": "diffusion_pytorch_model-00003-of-00014.safetensors",
640
- "transformer_blocks.9.norm2.weight": "diffusion_pytorch_model-00003-of-00014.safetensors"
 
 
641
  }
642
- }
 
1
  {
2
  "metadata": {
3
+ "total_size": 40235463200
4
  },
5
  "weight_map": {
6
  "audio_proj_in.bias": "diffusion_pytorch_model-00001-of-00014.safetensors",
 
637
  "transformer_blocks.9.ff.net.0.proj.weight": "diffusion_pytorch_model-00003-of-00014.safetensors",
638
  "transformer_blocks.9.ff.net.2.weight": "diffusion_pytorch_model-00003-of-00014.safetensors",
639
  "transformer_blocks.9.norm1.weight": "diffusion_pytorch_model-00003-of-00014.safetensors",
640
+ "transformer_blocks.9.norm2.weight": "diffusion_pytorch_model-00003-of-00014.safetensors",
641
+ "adaln_basis": "adaln_affine.safetensors",
642
+ "adaln_mean": "adaln_affine.safetensors"
643
  }
644
+ }
transformer/modeling_minimax_h3_pruned.py CHANGED
@@ -17,12 +17,15 @@ Everything outside the timestep path is inherited from `MiniMaxH3Transformer3DMo
17
  token refiner, the output heads and `forward` itself are the released implementation, unmodified. Only what feeds the
18
  AdaLN projections changes.
19
 
20
- The one other thing this file adds is `enable_convrot` / `quantize_8bit`: an opt-in Hadamard conditioning of the
21
  attention and feed-forward linears that makes 8-bit *compute* (int8 or fp8 dynamic activations, via torchao) land
22
- within a rounding step of bfloat16. It is inert unless called, so the plain pruned path is byte for byte what it was.
 
 
23
  """
24
 
25
  import math
 
26
 
27
  import torch
28
  import torch.nn as nn
@@ -36,10 +39,17 @@ from diffusers.models.transformers.transformer_minimax_h3 import (
36
  MiniMaxH3Transformer3DModel,
37
  MiniMaxH3TransformerBlock,
38
  )
 
39
 
40
 
 
 
41
  _HADAMARD_CACHE: dict = {}
42
 
 
 
 
 
43
  # The linears ConvRot conditions: the block stack's attention and feed-forward projections, and nothing else.
44
  # This is the set ComfyUI's `*_int8_convrot` checkpoints quantize (they carry one fused `qkv_proj`; the three
45
  # split projections here share an input, so rotating each is the same transform). The AdaLN path, the patch
@@ -129,7 +139,58 @@ class MiniMaxH3PrunedTimeProj(nn.Module):
129
  return timestep
130
 
131
 
132
- class MiniMaxH3PrunedAdaLayerNormModulation(nn.Module):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  r"""`MiniMaxH3AdaLayerNormModulation` over the pruned timestep coordinates.
134
 
135
  Two differences from the released module. It applies no `silu` - the table already holds the coordinates of the
@@ -143,33 +204,31 @@ class MiniMaxH3PrunedAdaLayerNormModulation(nn.Module):
143
  """
144
 
145
  def __init__(self, adaln_rank: int, hidden_size: int) -> None:
146
- super().__init__()
147
- self.hidden_size = hidden_size
148
  out_features = 6 * hidden_size * MINIMAX_H3_MODALITY_NUM
 
 
149
  self.linear = nn.Linear(adaln_rank, out_features, bias=False)
150
- self.register_buffer("folded_bias", torch.zeros(out_features), persistent=True)
151
 
152
  def forward(self, temb: torch.Tensor) -> tuple[torch.Tensor, ...]:
153
  dtype = get_parameter_dtype(self.linear)
154
  temb = self.linear(temb.to(dtype))
155
- temb = (temb.float() + self.folded_bias).to(dtype)
156
  temb = temb.view(-1, 6 * self.hidden_size)
157
  return temb.chunk(6, dim=-1)
158
 
159
 
160
- class MiniMaxH3PrunedAdaLayerNormOut(nn.Module):
161
  r"""`MiniMaxH3AdaLayerNormOut` over the pruned timestep coordinates; see the modulation module above."""
162
 
163
  def __init__(self, hidden_size: int, adaln_rank: int, eps: float) -> None:
164
- super().__init__()
165
  self.norm = nn.RMSNorm(hidden_size, eps=eps)
166
  self.linear = nn.Linear(adaln_rank, 2 * hidden_size, bias=False)
167
- self.register_buffer("folded_bias", torch.zeros(2 * hidden_size), persistent=True)
168
 
169
  def forward(self, hidden_states: torch.Tensor, temb: torch.Tensor, timestep_indices: torch.Tensor) -> torch.Tensor:
170
  dtype = get_parameter_dtype(self.linear)
171
  temb = self.linear(temb.to(dtype))
172
- shift, scale = (temb.float() + self.folded_bias).to(dtype).chunk(2, dim=-1)
173
  hidden_states = self.norm(hidden_states)
174
  return hidden_states * (1.0 + scale.index_select(0, timestep_indices)) + shift.index_select(
175
  0, timestep_indices
@@ -188,7 +247,8 @@ class MiniMaxH3PrunedTransformer3DModel(MiniMaxH3Transformer3DModel):
188
  Only what builds the timestep path differs from [`MiniMaxH3Transformer3DModel`]: `time_proj` becomes an identity,
189
  `time_embedder` becomes [`MiniMaxH3PrunedTimeEmbedder`], and the AdaLN projections take `adaln_rank` inputs.
190
  `forward` is inherited unchanged. The module names are the released ones, so LoRAs trained against a pruned
191
- checkpoint - what the common trainers use by default - load natively.
 
192
 
193
  Args:
194
  adaln_rank (`int`, defaults to `8`):
@@ -217,6 +277,8 @@ class MiniMaxH3PrunedTransformer3DModel(MiniMaxH3Transformer3DModel):
217
  "audio_proj_out",
218
  "rope",
219
  "folded_bias",
 
 
220
  ]
221
 
222
  @register_to_config
@@ -259,6 +321,13 @@ class MiniMaxH3PrunedTransformer3DModel(MiniMaxH3Transformer3DModel):
259
  self.time_proj = MiniMaxH3PrunedTimeProj()
260
  self.time_embedder = MiniMaxH3PrunedTimeEmbedder(table_size=time_table_size, adaln_rank=adaln_rank)
261
 
 
 
 
 
 
 
 
262
  # 3. Rotary embedding over the packed (t, h, w) grid
263
  self.rope = MiniMaxH3RotaryPosEmbed(rope_freq_dim=rope_freq_dim, rope_theta=rope_theta)
264
 
@@ -303,6 +372,140 @@ class MiniMaxH3PrunedTransformer3DModel(MiniMaxH3Transformer3DModel):
303
 
304
  self.gradient_checkpointing = False
305
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  # -- 8-bit compute -----------------------------------------------------------------------------------------
307
  #
308
  # `enable_convrot` and `quantize_8bit` are opt-in and change nothing until called.
 
17
  token refiner, the output heads and `forward` itself are the released implementation, unmodified. Only what feeds the
18
  AdaLN projections changes.
19
 
20
+ Two other things this file adds. `enable_convrot` / `quantize_8bit`: an opt-in Hadamard conditioning of the
21
  attention and feed-forward linears that makes 8-bit *compute* (int8 or fp8 dynamic activations, via torchao) land
22
+ within a rounding step of bfloat16. And `load_lora_adapter`, overridden to project a LoRA trained against the
23
+ *released* 2688-wide AdaLN projections onto these 8-wide ones. Both are inert unless used, so the plain pruned path
24
+ is byte for byte what it was.
25
  """
26
 
27
  import math
28
+ import re
29
 
30
  import torch
31
  import torch.nn as nn
 
39
  MiniMaxH3Transformer3DModel,
40
  MiniMaxH3TransformerBlock,
41
  )
42
+ from diffusers.utils import logging
43
 
44
 
45
+ logger = logging.get_logger(__name__)
46
+
47
  _HADAMARD_CACHE: dict = {}
48
 
49
+ # The 51 AdaLN projections, as a LoRA state dict names them, with or without a `transformer.` / `transformer_ref.`
50
+ # component prefix. Group 1 is the module path the model itself knows the projection by.
51
+ ADALN_LORA_A_KEY = re.compile(r"(?:^|\.)((?:transformer_blocks\.\d+\.adaln_proj|norm_out)\.linear)\.lora_A\.weight$")
52
+
53
  # The linears ConvRot conditions: the block stack's attention and feed-forward projections, and nothing else.
54
  # This is the set ComfyUI's `*_int8_convrot` checkpoints quantize (they carry one fused `qkv_proj`; the three
55
  # split projections here share an input, so rotating each is the same transform). The AdaLN path, the patch
 
139
  return timestep
140
 
141
 
142
+ class MiniMaxH3PrunedAdaLN(nn.Module):
143
+ r"""Shared by both pruned AdaLN modules: the folded float32 bias, plus the LoRA offsets that ride alongside it.
144
+
145
+ A LoRA trained against the released 2688-wide projection contributes `lora_B @ (lora_A @ x)` to the modulation.
146
+ With `x = mean + c @ basis`, that splits into a coordinate term the projected factors reproduce and a *constant*
147
+ term, `lora_B @ (lora_A @ mean)`, which no `Linear(8 -> out)` can express. That constant is held here, as a
148
+ per-adapter float32 buffer added to `folded_bias` - the same place, and the same precision, the fold's own
149
+ constant term lives in. It is deliberately not the projection's `bias`: rounding it into bfloat16 would spend a
150
+ full rounding step of the modulation on a term that is most of what the adapter does to the AdaLN path.
151
+ """
152
+
153
+ def __init__(self, out_features: int) -> None:
154
+ super().__init__()
155
+ self.register_buffer("folded_bias", torch.zeros(out_features), persistent=True)
156
+ # `{adapter name: buffer attribute}`. Plain state, not a submodule: the buffers themselves are what move
157
+ # with the module, and being non-persistent they stay out of the checkpoint, as an adapter should.
158
+ self._lora_adaln_offsets: dict[str, str] = {}
159
+
160
+ def register_lora_adaln_offset(self, adapter_name: str, offset: torch.Tensor) -> None:
161
+ r"""Attach one adapter's constant term, on the device and in the precision `folded_bias` is kept in."""
162
+ attribute = self._lora_adaln_offsets.get(adapter_name)
163
+ if attribute is None:
164
+ attribute = f"lora_adaln_offset_{len(self._lora_adaln_offsets)}"
165
+ self._lora_adaln_offsets[adapter_name] = attribute
166
+ value = offset.to(device=self.folded_bias.device, dtype=torch.float32)
167
+ self.register_buffer(attribute, value, persistent=False)
168
+
169
+ def lora_adaln_bias(self) -> torch.Tensor:
170
+ r"""`folded_bias` plus every active adapter's constant term at its current scaling.
171
+
172
+ PEFT owns everything this reads - `active_adapters`, `scaling`, `disable_adapters` - so the offsets follow
173
+ `set_adapters`, `disable_lora` and `delete_adapters` with no bookkeeping of their own. They apply whether or
174
+ not an adapter is merged: `fuse_lora` folds `lora_B @ lora_A` into the projection's weight, and there is
175
+ nowhere in a bias-free `Linear` for this term to be folded to.
176
+ """
177
+ bias = self.folded_bias
178
+ offsets = self._lora_adaln_offsets
179
+ if not offsets:
180
+ return bias
181
+ layer = self.linear
182
+ scaling = getattr(layer, "scaling", None)
183
+ if not isinstance(scaling, dict) or getattr(layer, "disable_adapters", False):
184
+ return bias
185
+ for adapter_name in getattr(layer, "active_adapters", ()):
186
+ attribute = offsets.get(adapter_name)
187
+ if attribute is None or adapter_name not in scaling:
188
+ continue
189
+ bias = bias + getattr(self, attribute) * float(scaling[adapter_name])
190
+ return bias
191
+
192
+
193
+ class MiniMaxH3PrunedAdaLayerNormModulation(MiniMaxH3PrunedAdaLN):
194
  r"""`MiniMaxH3AdaLayerNormModulation` over the pruned timestep coordinates.
195
 
196
  Two differences from the released module. It applies no `silu` - the table already holds the coordinates of the
 
204
  """
205
 
206
  def __init__(self, adaln_rank: int, hidden_size: int) -> None:
 
 
207
  out_features = 6 * hidden_size * MINIMAX_H3_MODALITY_NUM
208
+ super().__init__(out_features)
209
+ self.hidden_size = hidden_size
210
  self.linear = nn.Linear(adaln_rank, out_features, bias=False)
 
211
 
212
  def forward(self, temb: torch.Tensor) -> tuple[torch.Tensor, ...]:
213
  dtype = get_parameter_dtype(self.linear)
214
  temb = self.linear(temb.to(dtype))
215
+ temb = (temb.float() + self.lora_adaln_bias()).to(dtype)
216
  temb = temb.view(-1, 6 * self.hidden_size)
217
  return temb.chunk(6, dim=-1)
218
 
219
 
220
+ class MiniMaxH3PrunedAdaLayerNormOut(MiniMaxH3PrunedAdaLN):
221
  r"""`MiniMaxH3AdaLayerNormOut` over the pruned timestep coordinates; see the modulation module above."""
222
 
223
  def __init__(self, hidden_size: int, adaln_rank: int, eps: float) -> None:
224
+ super().__init__(2 * hidden_size)
225
  self.norm = nn.RMSNorm(hidden_size, eps=eps)
226
  self.linear = nn.Linear(adaln_rank, 2 * hidden_size, bias=False)
 
227
 
228
  def forward(self, hidden_states: torch.Tensor, temb: torch.Tensor, timestep_indices: torch.Tensor) -> torch.Tensor:
229
  dtype = get_parameter_dtype(self.linear)
230
  temb = self.linear(temb.to(dtype))
231
+ shift, scale = (temb.float() + self.lora_adaln_bias()).to(dtype).chunk(2, dim=-1)
232
  hidden_states = self.norm(hidden_states)
233
  return hidden_states * (1.0 + scale.index_select(0, timestep_indices)) + shift.index_select(
234
  0, timestep_indices
 
247
  Only what builds the timestep path differs from [`MiniMaxH3Transformer3DModel`]: `time_proj` becomes an identity,
248
  `time_embedder` becomes [`MiniMaxH3PrunedTimeEmbedder`], and the AdaLN projections take `adaln_rank` inputs.
249
  `forward` is inherited unchanged. The module names are the released ones, so LoRAs trained against a pruned
250
+ checkpoint - what the common trainers use by default - load natively, and `load_lora_adapter` projects LoRAs
251
+ trained against the released checkpoint's `time_embed_dim`-wide projections onto the same coordinates.
252
 
253
  Args:
254
  adaln_rank (`int`, defaults to `8`):
 
277
  "audio_proj_out",
278
  "rope",
279
  "folded_bias",
280
+ "adaln_basis",
281
+ "adaln_mean",
282
  ]
283
 
284
  @register_to_config
 
321
  self.time_proj = MiniMaxH3PrunedTimeProj()
322
  self.time_embedder = MiniMaxH3PrunedTimeEmbedder(table_size=time_table_size, adaln_rank=adaln_rank)
323
 
324
+ # 2b. The affine map the fold was performed with: `silu(time_embedder(t)) ~= adaln_mean + c(t) @ adaln_basis`.
325
+ # Nothing in `forward` reads these - the folded projections already carry them. They are stored so that a
326
+ # LoRA trained on the released 2688-wide projections can be mapped onto these coordinates at load time;
327
+ # see `load_lora_adapter`. 97 KB per partition.
328
+ self.register_buffer("adaln_basis", torch.zeros(adaln_rank, time_embed_dim), persistent=True)
329
+ self.register_buffer("adaln_mean", torch.zeros(time_embed_dim), persistent=True)
330
+
331
  # 3. Rotary embedding over the packed (t, h, w) grid
332
  self.rope = MiniMaxH3RotaryPosEmbed(rope_freq_dim=rope_freq_dim, rope_theta=rope_theta)
333
 
 
372
 
373
  self.gradient_checkpointing = False
374
 
375
+ # -- LoRA ---------------------------------------------------------------------------------------------------
376
+
377
+ def project_adaln_lora(self, state_dict: dict, prefix: str | None = None) -> tuple[dict, dict]:
378
+ r"""Map a LoRA's AdaLN factors from the released timestep embedding onto the pruned coordinates.
379
+
380
+ The released projection reads `x = silu(time_embedder(t))`, so a LoRA on it contributes
381
+
382
+ lora_B @ (lora_A @ x) = lora_B @ (lora_A @ (mean + basis.T @ c))
383
+ = (lora_B @ (lora_A @ basis.T)) @ c + lora_B @ (lora_A @ mean)
384
+
385
+ which is a rank-preserving `[rank, adaln_rank]` `lora_A` over the pruned coordinates plus a constant output
386
+ offset. Both are computed in float64 from the file's own factors; the only error is the rank-8 subspace's
387
+ own residual on the timestep curve, 1.5e-5 relative, which is ~250x below one bfloat16 step of the weights
388
+ being adapted.
389
+
390
+ Returns `(state_dict, {module path: offset})`, the state dict unchanged and the offsets empty when the AdaLN
391
+ factors are already `adaln_rank`-wide (a LoRA trained against a pruned checkpoint - most of them).
392
+
393
+ Every AdaLN module in a file has to be one or the other. A file that mixes widths is not something this can
394
+ half-apply, so it raises.
395
+
396
+ `prefix` scopes this to one component's keys, the same way `load_lora_adapter` scopes the load - a file that
397
+ names both partitions holds two different adapters under one roof, and only one of them is going into this
398
+ module.
399
+ """
400
+ adaln_rank = self.config.adaln_rank
401
+ time_embed_dim = self.config.time_embed_dim
402
+
403
+ modules = {}
404
+ for key in state_dict:
405
+ if prefix is not None and not key.startswith(f"{prefix}."):
406
+ continue
407
+ match = ADALN_LORA_A_KEY.search(key)
408
+ if match is not None:
409
+ modules[key] = match.group(1)
410
+ if not modules:
411
+ return state_dict, {}
412
+
413
+ widths = sorted({int(state_dict[key].shape[1]) for key in modules})
414
+ if widths == [adaln_rank]:
415
+ return state_dict, {}
416
+ if widths != [time_embed_dim]:
417
+ odd = sorted(
418
+ {modules[key] for key in modules if int(state_dict[key].shape[1]) != max(widths)},
419
+ key=lambda name: (name != "norm_out.linear", name),
420
+ )
421
+ raise ValueError(
422
+ f"This LoRA's {len(modules)} AdaLN projections read inputs of width {widths}. On this checkpoint they "
423
+ f"have to be uniformly {adaln_rank} wide (trained against a pruned checkpoint, loaded as they are) or "
424
+ f"uniformly {time_embed_dim} wide (trained against the released checkpoint, projected onto the pruned "
425
+ "coordinates at load). An adapter cannot be applied to some of its AdaLN modules and not others, so "
426
+ f"nothing was loaded. The minority width is on: {odd[:4]}{' ...' if len(odd) > 4 else ''}."
427
+ )
428
+
429
+ basis = self.adaln_basis
430
+ mean = self.adaln_mean
431
+ if basis.abs().sum() == 0:
432
+ raise ValueError(
433
+ "Projecting a released-checkpoint LoRA needs `adaln_basis` and `adaln_mean`, the affine map this "
434
+ "checkpoint's AdaLN projections were folded with, and this model was loaded without them. Re-download "
435
+ "the repository: they ship as `adaln_affine.safetensors` next to the weights."
436
+ )
437
+ basis = basis.double()
438
+ mean = mean.double()
439
+
440
+ projected = dict(state_dict)
441
+ offsets = {}
442
+ for key, module in modules.items():
443
+ lora_b_key = key[: -len("lora_A.weight")] + "lora_B.weight"
444
+ if lora_b_key not in state_dict:
445
+ raise ValueError(
446
+ f"{key} has no matching {lora_b_key}. The constant term of the projection is "
447
+ "`lora_B @ (lora_A @ mean)`, so both factors have to be present; nothing was loaded."
448
+ )
449
+ lora_a = state_dict[key].to(device=basis.device, dtype=torch.float64)
450
+ lora_b = state_dict[lora_b_key].to(device=basis.device, dtype=torch.float64)
451
+ # float32 rather than the file's bfloat16: the projection is exact arithmetic on the file's factors and
452
+ # there is no reason to round it twice. PEFT casts to the adapter's dtype when it loads them.
453
+ projected[key] = (lora_a @ basis.T).to(torch.float32).cpu().contiguous()
454
+ offsets[module] = (lora_b @ (lora_a @ mean)).to(torch.float32).cpu().contiguous()
455
+ return projected, offsets
456
+
457
+ def load_lora_adapter(self, pretrained_model_name_or_path_or_dict, prefix="transformer", hotswap=False, **kwargs):
458
+ r"""`PeftAdapterMixin.load_lora_adapter`, with the AdaLN projection of [`project_adaln_lora`] in front of it.
459
+
460
+ LoRAs trained against a pruned checkpoint pass through untouched. LoRAs trained against the released
461
+ checkpoint's 2688-wide AdaLN projections - the official turbo LoRA and its conversions - are mapped onto the
462
+ pruned coordinates here, which is the only thing that ever stopped them loading. Everything outside the AdaLN
463
+ path is identical between the two checkpoints and is neither inspected nor changed.
464
+ """
465
+ state_dict = pretrained_model_name_or_path_or_dict
466
+ if not isinstance(state_dict, dict):
467
+ from diffusers.loaders.lora_base import _fetch_state_dict
468
+
469
+ state_dict, _ = _fetch_state_dict(
470
+ pretrained_model_name_or_path_or_dict=state_dict,
471
+ weight_name=kwargs.get("weight_name"),
472
+ use_safetensors=kwargs.get("use_safetensors", True),
473
+ local_files_only=kwargs.get("local_files_only"),
474
+ cache_dir=kwargs.get("cache_dir"),
475
+ force_download=kwargs.get("force_download", False),
476
+ proxies=kwargs.get("proxies"),
477
+ token=kwargs.get("token"),
478
+ revision=kwargs.get("revision"),
479
+ subfolder=kwargs.get("subfolder"),
480
+ user_agent={"file_type": "attn_procs_weights", "framework": "pytorch"},
481
+ allow_pickle=False,
482
+ metadata=kwargs.get("metadata"),
483
+ )
484
+
485
+ state_dict, offsets = self.project_adaln_lora(state_dict, prefix=prefix)
486
+ if offsets:
487
+ logger.info(
488
+ f"Projecting {len(offsets)} AdaLN LoRA modules from the released {self.config.time_embed_dim}-wide "
489
+ f"timestep embedding onto this checkpoint's {self.config.adaln_rank} pruned coordinates; each one's "
490
+ "constant term is carried as a float32 offset on the modulation."
491
+ )
492
+
493
+ before = set(getattr(self, "peft_config", None) or ())
494
+ super().load_lora_adapter(state_dict, prefix=prefix, hotswap=hotswap, **kwargs)
495
+ if not offsets:
496
+ return
497
+
498
+ added = set(getattr(self, "peft_config", None) or ()) - before
499
+ adapter_name = added.pop() if len(added) == 1 else kwargs.get("adapter_name")
500
+ if adapter_name is None:
501
+ raise RuntimeError(
502
+ "The AdaLN projection could not tell which adapter was just loaded, so its constant terms were not "
503
+ f"attached and the adapter is incomplete. Adapters before: {sorted(before)}, after: "
504
+ f"{sorted(getattr(self, 'peft_config', None) or ())}. Pass `adapter_name` explicitly."
505
+ )
506
+ for module, offset in offsets.items():
507
+ self.get_submodule(module.rsplit(".", 1)[0]).register_lora_adaln_offset(adapter_name, offset)
508
+
509
  # -- 8-bit compute -----------------------------------------------------------------------------------------
510
  #
511
  # `enable_convrot` and `quantize_8bit` are opt-in and change nothing until called.
transformer_ref/adaln_affine.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:98fbeba868fd1d08b8232dce62abb3f5427dd49fb08462869633a3d53533b50f
3
+ size 96960
transformer_ref/diffusion_pytorch_model.safetensors.index.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "metadata": {
3
- "total_size": 40235366432
4
  },
5
  "weight_map": {
6
  "audio_proj_in.bias": "diffusion_pytorch_model-00001-of-00014.safetensors",
@@ -637,6 +637,8 @@
637
  "transformer_blocks.9.ff.net.0.proj.weight": "diffusion_pytorch_model-00003-of-00014.safetensors",
638
  "transformer_blocks.9.ff.net.2.weight": "diffusion_pytorch_model-00003-of-00014.safetensors",
639
  "transformer_blocks.9.norm1.weight": "diffusion_pytorch_model-00003-of-00014.safetensors",
640
- "transformer_blocks.9.norm2.weight": "diffusion_pytorch_model-00003-of-00014.safetensors"
 
 
641
  }
642
- }
 
1
  {
2
  "metadata": {
3
+ "total_size": 40235463200
4
  },
5
  "weight_map": {
6
  "audio_proj_in.bias": "diffusion_pytorch_model-00001-of-00014.safetensors",
 
637
  "transformer_blocks.9.ff.net.0.proj.weight": "diffusion_pytorch_model-00003-of-00014.safetensors",
638
  "transformer_blocks.9.ff.net.2.weight": "diffusion_pytorch_model-00003-of-00014.safetensors",
639
  "transformer_blocks.9.norm1.weight": "diffusion_pytorch_model-00003-of-00014.safetensors",
640
+ "transformer_blocks.9.norm2.weight": "diffusion_pytorch_model-00003-of-00014.safetensors",
641
+ "adaln_basis": "adaln_affine.safetensors",
642
+ "adaln_mean": "adaln_affine.safetensors"
643
  }
644
+ }
transformer_ref/modeling_minimax_h3_pruned.py CHANGED
@@ -17,12 +17,15 @@ Everything outside the timestep path is inherited from `MiniMaxH3Transformer3DMo
17
  token refiner, the output heads and `forward` itself are the released implementation, unmodified. Only what feeds the
18
  AdaLN projections changes.
19
 
20
- The one other thing this file adds is `enable_convrot` / `quantize_8bit`: an opt-in Hadamard conditioning of the
21
  attention and feed-forward linears that makes 8-bit *compute* (int8 or fp8 dynamic activations, via torchao) land
22
- within a rounding step of bfloat16. It is inert unless called, so the plain pruned path is byte for byte what it was.
 
 
23
  """
24
 
25
  import math
 
26
 
27
  import torch
28
  import torch.nn as nn
@@ -36,10 +39,17 @@ from diffusers.models.transformers.transformer_minimax_h3 import (
36
  MiniMaxH3Transformer3DModel,
37
  MiniMaxH3TransformerBlock,
38
  )
 
39
 
40
 
 
 
41
  _HADAMARD_CACHE: dict = {}
42
 
 
 
 
 
43
  # The linears ConvRot conditions: the block stack's attention and feed-forward projections, and nothing else.
44
  # This is the set ComfyUI's `*_int8_convrot` checkpoints quantize (they carry one fused `qkv_proj`; the three
45
  # split projections here share an input, so rotating each is the same transform). The AdaLN path, the patch
@@ -129,7 +139,58 @@ class MiniMaxH3PrunedTimeProj(nn.Module):
129
  return timestep
130
 
131
 
132
- class MiniMaxH3PrunedAdaLayerNormModulation(nn.Module):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  r"""`MiniMaxH3AdaLayerNormModulation` over the pruned timestep coordinates.
134
 
135
  Two differences from the released module. It applies no `silu` - the table already holds the coordinates of the
@@ -143,33 +204,31 @@ class MiniMaxH3PrunedAdaLayerNormModulation(nn.Module):
143
  """
144
 
145
  def __init__(self, adaln_rank: int, hidden_size: int) -> None:
146
- super().__init__()
147
- self.hidden_size = hidden_size
148
  out_features = 6 * hidden_size * MINIMAX_H3_MODALITY_NUM
 
 
149
  self.linear = nn.Linear(adaln_rank, out_features, bias=False)
150
- self.register_buffer("folded_bias", torch.zeros(out_features), persistent=True)
151
 
152
  def forward(self, temb: torch.Tensor) -> tuple[torch.Tensor, ...]:
153
  dtype = get_parameter_dtype(self.linear)
154
  temb = self.linear(temb.to(dtype))
155
- temb = (temb.float() + self.folded_bias).to(dtype)
156
  temb = temb.view(-1, 6 * self.hidden_size)
157
  return temb.chunk(6, dim=-1)
158
 
159
 
160
- class MiniMaxH3PrunedAdaLayerNormOut(nn.Module):
161
  r"""`MiniMaxH3AdaLayerNormOut` over the pruned timestep coordinates; see the modulation module above."""
162
 
163
  def __init__(self, hidden_size: int, adaln_rank: int, eps: float) -> None:
164
- super().__init__()
165
  self.norm = nn.RMSNorm(hidden_size, eps=eps)
166
  self.linear = nn.Linear(adaln_rank, 2 * hidden_size, bias=False)
167
- self.register_buffer("folded_bias", torch.zeros(2 * hidden_size), persistent=True)
168
 
169
  def forward(self, hidden_states: torch.Tensor, temb: torch.Tensor, timestep_indices: torch.Tensor) -> torch.Tensor:
170
  dtype = get_parameter_dtype(self.linear)
171
  temb = self.linear(temb.to(dtype))
172
- shift, scale = (temb.float() + self.folded_bias).to(dtype).chunk(2, dim=-1)
173
  hidden_states = self.norm(hidden_states)
174
  return hidden_states * (1.0 + scale.index_select(0, timestep_indices)) + shift.index_select(
175
  0, timestep_indices
@@ -188,7 +247,8 @@ class MiniMaxH3PrunedTransformer3DModel(MiniMaxH3Transformer3DModel):
188
  Only what builds the timestep path differs from [`MiniMaxH3Transformer3DModel`]: `time_proj` becomes an identity,
189
  `time_embedder` becomes [`MiniMaxH3PrunedTimeEmbedder`], and the AdaLN projections take `adaln_rank` inputs.
190
  `forward` is inherited unchanged. The module names are the released ones, so LoRAs trained against a pruned
191
- checkpoint - what the common trainers use by default - load natively.
 
192
 
193
  Args:
194
  adaln_rank (`int`, defaults to `8`):
@@ -217,6 +277,8 @@ class MiniMaxH3PrunedTransformer3DModel(MiniMaxH3Transformer3DModel):
217
  "audio_proj_out",
218
  "rope",
219
  "folded_bias",
 
 
220
  ]
221
 
222
  @register_to_config
@@ -259,6 +321,13 @@ class MiniMaxH3PrunedTransformer3DModel(MiniMaxH3Transformer3DModel):
259
  self.time_proj = MiniMaxH3PrunedTimeProj()
260
  self.time_embedder = MiniMaxH3PrunedTimeEmbedder(table_size=time_table_size, adaln_rank=adaln_rank)
261
 
 
 
 
 
 
 
 
262
  # 3. Rotary embedding over the packed (t, h, w) grid
263
  self.rope = MiniMaxH3RotaryPosEmbed(rope_freq_dim=rope_freq_dim, rope_theta=rope_theta)
264
 
@@ -303,6 +372,140 @@ class MiniMaxH3PrunedTransformer3DModel(MiniMaxH3Transformer3DModel):
303
 
304
  self.gradient_checkpointing = False
305
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  # -- 8-bit compute -----------------------------------------------------------------------------------------
307
  #
308
  # `enable_convrot` and `quantize_8bit` are opt-in and change nothing until called.
 
17
  token refiner, the output heads and `forward` itself are the released implementation, unmodified. Only what feeds the
18
  AdaLN projections changes.
19
 
20
+ Two other things this file adds. `enable_convrot` / `quantize_8bit`: an opt-in Hadamard conditioning of the
21
  attention and feed-forward linears that makes 8-bit *compute* (int8 or fp8 dynamic activations, via torchao) land
22
+ within a rounding step of bfloat16. And `load_lora_adapter`, overridden to project a LoRA trained against the
23
+ *released* 2688-wide AdaLN projections onto these 8-wide ones. Both are inert unless used, so the plain pruned path
24
+ is byte for byte what it was.
25
  """
26
 
27
  import math
28
+ import re
29
 
30
  import torch
31
  import torch.nn as nn
 
39
  MiniMaxH3Transformer3DModel,
40
  MiniMaxH3TransformerBlock,
41
  )
42
+ from diffusers.utils import logging
43
 
44
 
45
+ logger = logging.get_logger(__name__)
46
+
47
  _HADAMARD_CACHE: dict = {}
48
 
49
+ # The 51 AdaLN projections, as a LoRA state dict names them, with or without a `transformer.` / `transformer_ref.`
50
+ # component prefix. Group 1 is the module path the model itself knows the projection by.
51
+ ADALN_LORA_A_KEY = re.compile(r"(?:^|\.)((?:transformer_blocks\.\d+\.adaln_proj|norm_out)\.linear)\.lora_A\.weight$")
52
+
53
  # The linears ConvRot conditions: the block stack's attention and feed-forward projections, and nothing else.
54
  # This is the set ComfyUI's `*_int8_convrot` checkpoints quantize (they carry one fused `qkv_proj`; the three
55
  # split projections here share an input, so rotating each is the same transform). The AdaLN path, the patch
 
139
  return timestep
140
 
141
 
142
+ class MiniMaxH3PrunedAdaLN(nn.Module):
143
+ r"""Shared by both pruned AdaLN modules: the folded float32 bias, plus the LoRA offsets that ride alongside it.
144
+
145
+ A LoRA trained against the released 2688-wide projection contributes `lora_B @ (lora_A @ x)` to the modulation.
146
+ With `x = mean + c @ basis`, that splits into a coordinate term the projected factors reproduce and a *constant*
147
+ term, `lora_B @ (lora_A @ mean)`, which no `Linear(8 -> out)` can express. That constant is held here, as a
148
+ per-adapter float32 buffer added to `folded_bias` - the same place, and the same precision, the fold's own
149
+ constant term lives in. It is deliberately not the projection's `bias`: rounding it into bfloat16 would spend a
150
+ full rounding step of the modulation on a term that is most of what the adapter does to the AdaLN path.
151
+ """
152
+
153
+ def __init__(self, out_features: int) -> None:
154
+ super().__init__()
155
+ self.register_buffer("folded_bias", torch.zeros(out_features), persistent=True)
156
+ # `{adapter name: buffer attribute}`. Plain state, not a submodule: the buffers themselves are what move
157
+ # with the module, and being non-persistent they stay out of the checkpoint, as an adapter should.
158
+ self._lora_adaln_offsets: dict[str, str] = {}
159
+
160
+ def register_lora_adaln_offset(self, adapter_name: str, offset: torch.Tensor) -> None:
161
+ r"""Attach one adapter's constant term, on the device and in the precision `folded_bias` is kept in."""
162
+ attribute = self._lora_adaln_offsets.get(adapter_name)
163
+ if attribute is None:
164
+ attribute = f"lora_adaln_offset_{len(self._lora_adaln_offsets)}"
165
+ self._lora_adaln_offsets[adapter_name] = attribute
166
+ value = offset.to(device=self.folded_bias.device, dtype=torch.float32)
167
+ self.register_buffer(attribute, value, persistent=False)
168
+
169
+ def lora_adaln_bias(self) -> torch.Tensor:
170
+ r"""`folded_bias` plus every active adapter's constant term at its current scaling.
171
+
172
+ PEFT owns everything this reads - `active_adapters`, `scaling`, `disable_adapters` - so the offsets follow
173
+ `set_adapters`, `disable_lora` and `delete_adapters` with no bookkeeping of their own. They apply whether or
174
+ not an adapter is merged: `fuse_lora` folds `lora_B @ lora_A` into the projection's weight, and there is
175
+ nowhere in a bias-free `Linear` for this term to be folded to.
176
+ """
177
+ bias = self.folded_bias
178
+ offsets = self._lora_adaln_offsets
179
+ if not offsets:
180
+ return bias
181
+ layer = self.linear
182
+ scaling = getattr(layer, "scaling", None)
183
+ if not isinstance(scaling, dict) or getattr(layer, "disable_adapters", False):
184
+ return bias
185
+ for adapter_name in getattr(layer, "active_adapters", ()):
186
+ attribute = offsets.get(adapter_name)
187
+ if attribute is None or adapter_name not in scaling:
188
+ continue
189
+ bias = bias + getattr(self, attribute) * float(scaling[adapter_name])
190
+ return bias
191
+
192
+
193
+ class MiniMaxH3PrunedAdaLayerNormModulation(MiniMaxH3PrunedAdaLN):
194
  r"""`MiniMaxH3AdaLayerNormModulation` over the pruned timestep coordinates.
195
 
196
  Two differences from the released module. It applies no `silu` - the table already holds the coordinates of the
 
204
  """
205
 
206
  def __init__(self, adaln_rank: int, hidden_size: int) -> None:
 
 
207
  out_features = 6 * hidden_size * MINIMAX_H3_MODALITY_NUM
208
+ super().__init__(out_features)
209
+ self.hidden_size = hidden_size
210
  self.linear = nn.Linear(adaln_rank, out_features, bias=False)
 
211
 
212
  def forward(self, temb: torch.Tensor) -> tuple[torch.Tensor, ...]:
213
  dtype = get_parameter_dtype(self.linear)
214
  temb = self.linear(temb.to(dtype))
215
+ temb = (temb.float() + self.lora_adaln_bias()).to(dtype)
216
  temb = temb.view(-1, 6 * self.hidden_size)
217
  return temb.chunk(6, dim=-1)
218
 
219
 
220
+ class MiniMaxH3PrunedAdaLayerNormOut(MiniMaxH3PrunedAdaLN):
221
  r"""`MiniMaxH3AdaLayerNormOut` over the pruned timestep coordinates; see the modulation module above."""
222
 
223
  def __init__(self, hidden_size: int, adaln_rank: int, eps: float) -> None:
224
+ super().__init__(2 * hidden_size)
225
  self.norm = nn.RMSNorm(hidden_size, eps=eps)
226
  self.linear = nn.Linear(adaln_rank, 2 * hidden_size, bias=False)
 
227
 
228
  def forward(self, hidden_states: torch.Tensor, temb: torch.Tensor, timestep_indices: torch.Tensor) -> torch.Tensor:
229
  dtype = get_parameter_dtype(self.linear)
230
  temb = self.linear(temb.to(dtype))
231
+ shift, scale = (temb.float() + self.lora_adaln_bias()).to(dtype).chunk(2, dim=-1)
232
  hidden_states = self.norm(hidden_states)
233
  return hidden_states * (1.0 + scale.index_select(0, timestep_indices)) + shift.index_select(
234
  0, timestep_indices
 
247
  Only what builds the timestep path differs from [`MiniMaxH3Transformer3DModel`]: `time_proj` becomes an identity,
248
  `time_embedder` becomes [`MiniMaxH3PrunedTimeEmbedder`], and the AdaLN projections take `adaln_rank` inputs.
249
  `forward` is inherited unchanged. The module names are the released ones, so LoRAs trained against a pruned
250
+ checkpoint - what the common trainers use by default - load natively, and `load_lora_adapter` projects LoRAs
251
+ trained against the released checkpoint's `time_embed_dim`-wide projections onto the same coordinates.
252
 
253
  Args:
254
  adaln_rank (`int`, defaults to `8`):
 
277
  "audio_proj_out",
278
  "rope",
279
  "folded_bias",
280
+ "adaln_basis",
281
+ "adaln_mean",
282
  ]
283
 
284
  @register_to_config
 
321
  self.time_proj = MiniMaxH3PrunedTimeProj()
322
  self.time_embedder = MiniMaxH3PrunedTimeEmbedder(table_size=time_table_size, adaln_rank=adaln_rank)
323
 
324
+ # 2b. The affine map the fold was performed with: `silu(time_embedder(t)) ~= adaln_mean + c(t) @ adaln_basis`.
325
+ # Nothing in `forward` reads these - the folded projections already carry them. They are stored so that a
326
+ # LoRA trained on the released 2688-wide projections can be mapped onto these coordinates at load time;
327
+ # see `load_lora_adapter`. 97 KB per partition.
328
+ self.register_buffer("adaln_basis", torch.zeros(adaln_rank, time_embed_dim), persistent=True)
329
+ self.register_buffer("adaln_mean", torch.zeros(time_embed_dim), persistent=True)
330
+
331
  # 3. Rotary embedding over the packed (t, h, w) grid
332
  self.rope = MiniMaxH3RotaryPosEmbed(rope_freq_dim=rope_freq_dim, rope_theta=rope_theta)
333
 
 
372
 
373
  self.gradient_checkpointing = False
374
 
375
+ # -- LoRA ---------------------------------------------------------------------------------------------------
376
+
377
+ def project_adaln_lora(self, state_dict: dict, prefix: str | None = None) -> tuple[dict, dict]:
378
+ r"""Map a LoRA's AdaLN factors from the released timestep embedding onto the pruned coordinates.
379
+
380
+ The released projection reads `x = silu(time_embedder(t))`, so a LoRA on it contributes
381
+
382
+ lora_B @ (lora_A @ x) = lora_B @ (lora_A @ (mean + basis.T @ c))
383
+ = (lora_B @ (lora_A @ basis.T)) @ c + lora_B @ (lora_A @ mean)
384
+
385
+ which is a rank-preserving `[rank, adaln_rank]` `lora_A` over the pruned coordinates plus a constant output
386
+ offset. Both are computed in float64 from the file's own factors; the only error is the rank-8 subspace's
387
+ own residual on the timestep curve, 1.5e-5 relative, which is ~250x below one bfloat16 step of the weights
388
+ being adapted.
389
+
390
+ Returns `(state_dict, {module path: offset})`, the state dict unchanged and the offsets empty when the AdaLN
391
+ factors are already `adaln_rank`-wide (a LoRA trained against a pruned checkpoint - most of them).
392
+
393
+ Every AdaLN module in a file has to be one or the other. A file that mixes widths is not something this can
394
+ half-apply, so it raises.
395
+
396
+ `prefix` scopes this to one component's keys, the same way `load_lora_adapter` scopes the load - a file that
397
+ names both partitions holds two different adapters under one roof, and only one of them is going into this
398
+ module.
399
+ """
400
+ adaln_rank = self.config.adaln_rank
401
+ time_embed_dim = self.config.time_embed_dim
402
+
403
+ modules = {}
404
+ for key in state_dict:
405
+ if prefix is not None and not key.startswith(f"{prefix}."):
406
+ continue
407
+ match = ADALN_LORA_A_KEY.search(key)
408
+ if match is not None:
409
+ modules[key] = match.group(1)
410
+ if not modules:
411
+ return state_dict, {}
412
+
413
+ widths = sorted({int(state_dict[key].shape[1]) for key in modules})
414
+ if widths == [adaln_rank]:
415
+ return state_dict, {}
416
+ if widths != [time_embed_dim]:
417
+ odd = sorted(
418
+ {modules[key] for key in modules if int(state_dict[key].shape[1]) != max(widths)},
419
+ key=lambda name: (name != "norm_out.linear", name),
420
+ )
421
+ raise ValueError(
422
+ f"This LoRA's {len(modules)} AdaLN projections read inputs of width {widths}. On this checkpoint they "
423
+ f"have to be uniformly {adaln_rank} wide (trained against a pruned checkpoint, loaded as they are) or "
424
+ f"uniformly {time_embed_dim} wide (trained against the released checkpoint, projected onto the pruned "
425
+ "coordinates at load). An adapter cannot be applied to some of its AdaLN modules and not others, so "
426
+ f"nothing was loaded. The minority width is on: {odd[:4]}{' ...' if len(odd) > 4 else ''}."
427
+ )
428
+
429
+ basis = self.adaln_basis
430
+ mean = self.adaln_mean
431
+ if basis.abs().sum() == 0:
432
+ raise ValueError(
433
+ "Projecting a released-checkpoint LoRA needs `adaln_basis` and `adaln_mean`, the affine map this "
434
+ "checkpoint's AdaLN projections were folded with, and this model was loaded without them. Re-download "
435
+ "the repository: they ship as `adaln_affine.safetensors` next to the weights."
436
+ )
437
+ basis = basis.double()
438
+ mean = mean.double()
439
+
440
+ projected = dict(state_dict)
441
+ offsets = {}
442
+ for key, module in modules.items():
443
+ lora_b_key = key[: -len("lora_A.weight")] + "lora_B.weight"
444
+ if lora_b_key not in state_dict:
445
+ raise ValueError(
446
+ f"{key} has no matching {lora_b_key}. The constant term of the projection is "
447
+ "`lora_B @ (lora_A @ mean)`, so both factors have to be present; nothing was loaded."
448
+ )
449
+ lora_a = state_dict[key].to(device=basis.device, dtype=torch.float64)
450
+ lora_b = state_dict[lora_b_key].to(device=basis.device, dtype=torch.float64)
451
+ # float32 rather than the file's bfloat16: the projection is exact arithmetic on the file's factors and
452
+ # there is no reason to round it twice. PEFT casts to the adapter's dtype when it loads them.
453
+ projected[key] = (lora_a @ basis.T).to(torch.float32).cpu().contiguous()
454
+ offsets[module] = (lora_b @ (lora_a @ mean)).to(torch.float32).cpu().contiguous()
455
+ return projected, offsets
456
+
457
+ def load_lora_adapter(self, pretrained_model_name_or_path_or_dict, prefix="transformer", hotswap=False, **kwargs):
458
+ r"""`PeftAdapterMixin.load_lora_adapter`, with the AdaLN projection of [`project_adaln_lora`] in front of it.
459
+
460
+ LoRAs trained against a pruned checkpoint pass through untouched. LoRAs trained against the released
461
+ checkpoint's 2688-wide AdaLN projections - the official turbo LoRA and its conversions - are mapped onto the
462
+ pruned coordinates here, which is the only thing that ever stopped them loading. Everything outside the AdaLN
463
+ path is identical between the two checkpoints and is neither inspected nor changed.
464
+ """
465
+ state_dict = pretrained_model_name_or_path_or_dict
466
+ if not isinstance(state_dict, dict):
467
+ from diffusers.loaders.lora_base import _fetch_state_dict
468
+
469
+ state_dict, _ = _fetch_state_dict(
470
+ pretrained_model_name_or_path_or_dict=state_dict,
471
+ weight_name=kwargs.get("weight_name"),
472
+ use_safetensors=kwargs.get("use_safetensors", True),
473
+ local_files_only=kwargs.get("local_files_only"),
474
+ cache_dir=kwargs.get("cache_dir"),
475
+ force_download=kwargs.get("force_download", False),
476
+ proxies=kwargs.get("proxies"),
477
+ token=kwargs.get("token"),
478
+ revision=kwargs.get("revision"),
479
+ subfolder=kwargs.get("subfolder"),
480
+ user_agent={"file_type": "attn_procs_weights", "framework": "pytorch"},
481
+ allow_pickle=False,
482
+ metadata=kwargs.get("metadata"),
483
+ )
484
+
485
+ state_dict, offsets = self.project_adaln_lora(state_dict, prefix=prefix)
486
+ if offsets:
487
+ logger.info(
488
+ f"Projecting {len(offsets)} AdaLN LoRA modules from the released {self.config.time_embed_dim}-wide "
489
+ f"timestep embedding onto this checkpoint's {self.config.adaln_rank} pruned coordinates; each one's "
490
+ "constant term is carried as a float32 offset on the modulation."
491
+ )
492
+
493
+ before = set(getattr(self, "peft_config", None) or ())
494
+ super().load_lora_adapter(state_dict, prefix=prefix, hotswap=hotswap, **kwargs)
495
+ if not offsets:
496
+ return
497
+
498
+ added = set(getattr(self, "peft_config", None) or ()) - before
499
+ adapter_name = added.pop() if len(added) == 1 else kwargs.get("adapter_name")
500
+ if adapter_name is None:
501
+ raise RuntimeError(
502
+ "The AdaLN projection could not tell which adapter was just loaded, so its constant terms were not "
503
+ f"attached and the adapter is incomplete. Adapters before: {sorted(before)}, after: "
504
+ f"{sorted(getattr(self, 'peft_config', None) or ())}. Pass `adapter_name` explicitly."
505
+ )
506
+ for module, offset in offsets.items():
507
+ self.get_submodule(module.rsplit(".", 1)[0]).register_lora_adaln_offset(adapter_name, offset)
508
+
509
  # -- 8-bit compute -----------------------------------------------------------------------------------------
510
  #
511
  # `enable_convrot` and `quantize_8bit` are opt-in and change nothing until called.