akhaliq HF Staff commited on
Commit
c38b474
·
1 Parent(s): 65a89d4

Add lightx2v 8-step v1.0 LoRA as lightx8; fix step-count off-by-one

Browse files

lightx8 loads minimax_h3_fl2v_turbo_8step_v1.0_bf16.safetensors through
the same diffusers-native PEFT mapping (rank 128, alpha 8, scale 0.0625),
suggested 8 steps, H3_LIGHTX8=off skips it.

MiniMaxH3Scheduler reads num_inference_steps as sigma grid points
including the terminal zero, so N evaluations need N + 1 points (as the
ModelTC inference script does). Pass steps + 1 so the UI's step count
matches the actual NFE count.

Files changed (3) hide show
  1. README.md +6 -1
  2. app.py +7 -4
  3. h3_lora.py +12 -7
README.md CHANGED
@@ -55,6 +55,10 @@ and the low-rank factors stay resident so switching is an in-place unfold/fold t
55
  `H3_LORA` selects the larry file (`off` skips it), `H3_LIGHTX=off` skips lightx, `H3_LORA_DEFAULT` picks which set
56
  starts folded, and `H3_LORA_STRENGTH` scales the larry update (the card's sharpness/artifact dial).
57
 
 
 
 
 
58
  A third, non-turbo set is [`fal/MiniMax-H3-Realism-People-LoRA`](https://huggingface.co/fal/MiniMax-H3-Realism-People-LoRA)
59
  (`realism` in the dropdown, trigger word `r34l1sm`): realistic people at the full 28 steps. It ships in the same
60
  reference-tree layout as larry under a `diffusion_model.` prefix, so it folds through the same key mapping.
@@ -191,7 +195,8 @@ one-time `PIPE.to("cuda")` is inside the first row's 339 s and does not reappear
191
  | `H3_LORA` | `minimax_h3_turbo_v4_step600_ema.safetensors` | Turbo LoRA file folded into the transformer at startup. `off` disables. |
192
  | `H3_LORA_REPO` | `larryvrh/MiniMax-H3-Turbo-Lora` | Hub repo the LoRA is fetched from. |
193
  | `H3_LORA_STRENGTH` | `1.0` | Scales the larry LoRA delta (sharpness/artifact trade-off). |
194
- | `H3_LIGHTX` | `on` | Set to `off` to skip loading the lightx2v LoRA set. |
 
195
  | `H3_REALISM` | `on` | Set to `off` to skip loading the fal realism-people LoRA set. |
196
  | `H3_JOYFOX` | `on` | Set to `off` to skip loading the joyfox 4-step turbo LoRA set. |
197
  | `H3_LORA_DEFAULT` | `larry` | Which loaded LoRA set starts folded (`larry` / `lightx` / `realism` / `joyfox`). |
 
55
  `H3_LORA` selects the larry file (`off` skips it), `H3_LIGHTX=off` skips lightx, `H3_LORA_DEFAULT` picks which set
56
  starts folded, and `H3_LORA_STRENGTH` scales the larry update (the card's sharpness/artifact dial).
57
 
58
+ The lightx set also comes as `lightx8` — the 8-step v1.0 diffusers checkpoint
59
+ (`minimax_h3_fl2v_turbo_8step_v1.0_bf16.safetensors`), same PEFT layout and scale, suggested 8 steps.
60
+ `H3_LIGHTX8=off` skips it.
61
+
62
  A third, non-turbo set is [`fal/MiniMax-H3-Realism-People-LoRA`](https://huggingface.co/fal/MiniMax-H3-Realism-People-LoRA)
63
  (`realism` in the dropdown, trigger word `r34l1sm`): realistic people at the full 28 steps. It ships in the same
64
  reference-tree layout as larry under a `diffusion_model.` prefix, so it folds through the same key mapping.
 
195
  | `H3_LORA` | `minimax_h3_turbo_v4_step600_ema.safetensors` | Turbo LoRA file folded into the transformer at startup. `off` disables. |
196
  | `H3_LORA_REPO` | `larryvrh/MiniMax-H3-Turbo-Lora` | Hub repo the LoRA is fetched from. |
197
  | `H3_LORA_STRENGTH` | `1.0` | Scales the larry LoRA delta (sharpness/artifact trade-off). |
198
+ | `H3_LIGHTX` | `on` | Set to `off` to skip loading the lightx2v 4-step LoRA set. |
199
+ | `H3_LIGHTX8` | `on` | Set to `off` to skip loading the lightx2v 8-step v1.0 LoRA set. |
200
  | `H3_REALISM` | `on` | Set to `off` to skip loading the fal realism-people LoRA set. |
201
  | `H3_JOYFOX` | `on` | Set to `off` to skip loading the joyfox 4-step turbo LoRA set. |
202
  | `H3_LORA_DEFAULT` | `larry` | Which loaded LoRA set starts folded (`larry` / `lightx` / `realism` / `joyfox`). |
app.py CHANGED
@@ -254,6 +254,8 @@ def _generate(prompt_embeds, text_token_tags, image, last_image, height, width,
254
 
255
  # The modular denoise loop has no callback hook, so count video scheduler steps through a wrapper: one call per
256
  # denoise step. When the count reaches the total, what remains inside `PIPE(...)` is the two decoders.
 
 
257
  steps = int(steps)
258
  PROGRESS.update(phase="denoise", step=0, steps=steps)
259
  original_step = PIPE.scheduler.step
@@ -282,7 +284,7 @@ def _generate(prompt_embeds, text_token_tags, image, last_image, height, width,
282
  height=height,
283
  width=width,
284
  num_frames=num_frames,
285
- num_inference_steps=steps,
286
  generator=torch.Generator("cpu").manual_seed(int(seed)),
287
  )
288
  finally:
@@ -326,8 +328,8 @@ def _fit_keyframe(image_path, current_canvas):
326
 
327
 
328
  def _resolve_lora(lora, use_lora) -> str:
329
- """`lora` (`larry` / `lightx` / `realism` / `joyfox` / `off`) wins; the legacy bool maps onto `larry` / `off`."""
330
- if isinstance(lora, str) and lora in ("larry", "lightx", "realism", "joyfox", "off"):
331
  return lora
332
  return "larry" if use_lora else "off"
333
 
@@ -452,7 +454,8 @@ def studio_config():
452
  # The LoRA dropdown: value -> {label, suggested steps}.
453
  "loras": {
454
  **{
455
- name: {"label": spec["label"], "steps": {"larry": 6, "lightx": 4, "realism": 28, "joyfox": 4}.get(name, 6)}
 
456
  for name, spec in sets.items()
457
  },
458
  "off": {"label": "off (base model)", "steps": 28},
 
254
 
255
  # The modular denoise loop has no callback hook, so count video scheduler steps through a wrapper: one call per
256
  # denoise step. When the count reaches the total, what remains inside `PIPE(...)` is the two decoders.
257
+ # `MiniMaxH3Scheduler` reads `num_inference_steps` as sigma grid points, terminal zero included, so N model
258
+ # evaluations need N + 1 points (the lightx/ModelTC inference script does the same).
259
  steps = int(steps)
260
  PROGRESS.update(phase="denoise", step=0, steps=steps)
261
  original_step = PIPE.scheduler.step
 
284
  height=height,
285
  width=width,
286
  num_frames=num_frames,
287
+ num_inference_steps=steps + 1,
288
  generator=torch.Generator("cpu").manual_seed(int(seed)),
289
  )
290
  finally:
 
328
 
329
 
330
  def _resolve_lora(lora, use_lora) -> str:
331
+ """`lora` (`larry` / `lightx` / `lightx8` / `realism` / `joyfox` / `off`) wins; the legacy bool maps onto `larry` / `off`."""
332
+ if isinstance(lora, str) and lora in ("larry", "lightx", "lightx8", "realism", "joyfox", "off"):
333
  return lora
334
  return "larry" if use_lora else "off"
335
 
 
454
  # The LoRA dropdown: value -> {label, suggested steps}.
455
  "loras": {
456
  **{
457
+ name: {"label": spec["label"],
458
+ "steps": {"larry": 6, "lightx": 4, "lightx8": 8, "realism": 28, "joyfox": 4}.get(name, 6)}
459
  for name, spec in sets.items()
460
  },
461
  "off": {"label": "off (base model)", "steps": 28},
h3_lora.py CHANGED
@@ -17,9 +17,10 @@ The two supported LoRAs ship in different layouts:
17
  `final_layer.adaln_proj.linear` -> `norm_out.linear`. The row transforms are applied to `lora_B` directly
18
  (rows of `B @ A` are rows of `B`), so no full delta is ever materialized at load.
19
 
20
- * `lightx` (`lightx2v/Minimax-h3-Turbo`) is a PEFT checkpoint against the diffusers tree itself —
21
  `transformer_blocks.N.attn.to_q.lora_A.default.weight` and friends — rank 128, `alpha == 8`, so the fold scale is
22
  `8 / 128 = 0.0625` (matching `set_adapters(weights=1.0)` in their inference script). Keys map name-for-name.
 
23
 
24
  * `realism` (`fal/MiniMax-H3-Realism-People-LoRA`) is a *style* LoRA, not a turbo one — realistic people, trigger
25
  word `r34l1sm`. Same reference tree as larry under a `diffusion_model.` prefix, attention only (`qkv_proj` /
@@ -35,8 +36,8 @@ The two supported LoRAs ship in different layouts:
35
  (`[96768, 2688]`), so those deltas have no counterpart to fold into.
36
 
37
  `H3_LORA` selects the larry file (`off` skips loading it), `H3_LIGHTX=off` skips lightx, `H3_REALISM=off` skips
38
- realism, `H3_JOYFOX=off` skips joyfox, `H3_LORA_DEFAULT` picks which set starts folded, and `H3_LORA_STRENGTH` is
39
- the larry card's sharpness/artifact dial.
40
  """
41
 
42
  from __future__ import annotations
@@ -51,6 +52,7 @@ LARRY_REPO = os.environ.get("H3_LORA_REPO", "larryvrh/MiniMax-H3-Turbo-Lora")
51
  LARRY_FILE = os.environ.get("H3_LORA", "minimax_h3_turbo_v4_step600_ema.safetensors")
52
  LIGHTX_REPO = os.environ.get("H3_LIGHTX_REPO", "lightx2v/Minimax-h3-Turbo")
53
  LIGHTX_FILE = os.environ.get("H3_LIGHTX_FILE", "minimax_h3_fl2v_turbo_4step_v0.1.safetensors")
 
54
  LIGHTX_ALPHA = 8
55
  REALISM_REPO = os.environ.get("H3_REALISM_REPO", "fal/MiniMax-H3-Realism-People-LoRA")
56
  REALISM_FILE = os.environ.get("H3_REALISM_FILE", "h3-realism-people-t2v-i2v-r2v.safetensors")
@@ -107,19 +109,20 @@ def _load_larry(inner_dim: int) -> dict:
107
  }
108
 
109
 
110
- def _load_lightx() -> dict:
 
111
  from huggingface_hub import hf_hub_download
112
  from safetensors.torch import load_file
113
 
114
- lora = load_file(hf_hub_download(LIGHTX_REPO, LIGHTX_FILE))
115
  suffix_a, suffix_b = ".lora_A.default.weight", ".lora_B.default.weight"
116
  bases = sorted({key[: -len(suffix_a)] for key in lora if key.endswith(suffix_a)})
117
  ranks = {lora[f"{name}{suffix_a}"].shape[0] for name in bases}
118
  if len(ranks) != 1:
119
- raise ValueError(f"Mixed LoRA ranks in {LIGHTX_FILE}: {sorted(ranks)}")
120
  entries = [(f"{name}.weight", lora[f"{name}{suffix_a}"], lora[f"{name}{suffix_b}"]) for name in bases]
121
  return {
122
- "label": f"{LIGHTX_REPO}/{LIGHTX_FILE}",
123
  "scale": LIGHTX_ALPHA / ranks.pop(),
124
  "entries": entries,
125
  }
@@ -195,6 +198,8 @@ def apply_lora(transformer) -> str | None:
195
  sets["larry"] = _load_larry(inner_dim)
196
  if os.environ.get("H3_LIGHTX", "on").lower() not in ("", "off", "none"):
197
  sets["lightx"] = _load_lightx()
 
 
198
  if os.environ.get("H3_REALISM", "on").lower() not in ("", "off", "none"):
199
  sets["realism"] = _load_realism(inner_dim)
200
  if os.environ.get("H3_JOYFOX", "on").lower() not in ("", "off", "none"):
 
17
  `final_layer.adaln_proj.linear` -> `norm_out.linear`. The row transforms are applied to `lora_B` directly
18
  (rows of `B @ A` are rows of `B`), so no full delta is ever materialized at load.
19
 
20
+ * `lightx` / `lightx8` (`lightx2v/Minimax-h3-Turbo`) are PEFT checkpoints against the diffusers tree itself —
21
  `transformer_blocks.N.attn.to_q.lora_A.default.weight` and friends — rank 128, `alpha == 8`, so the fold scale is
22
  `8 / 128 = 0.0625` (matching `set_adapters(weights=1.0)` in their inference script). Keys map name-for-name.
23
+ `lightx` is the 4-step file, `lightx8` the 8-step v1.0 file.
24
 
25
  * `realism` (`fal/MiniMax-H3-Realism-People-LoRA`) is a *style* LoRA, not a turbo one — realistic people, trigger
26
  word `r34l1sm`. Same reference tree as larry under a `diffusion_model.` prefix, attention only (`qkv_proj` /
 
36
  (`[96768, 2688]`), so those deltas have no counterpart to fold into.
37
 
38
  `H3_LORA` selects the larry file (`off` skips loading it), `H3_LIGHTX=off` skips lightx, `H3_REALISM=off` skips
39
+ realism, `H3_JOYFOX=off` skips joyfox, `H3_LIGHTX8=off` skips the lightx 8-step file, `H3_LORA_DEFAULT` picks which
40
+ set starts folded, and `H3_LORA_STRENGTH` is the larry card's sharpness/artifact dial.
41
  """
42
 
43
  from __future__ import annotations
 
52
  LARRY_FILE = os.environ.get("H3_LORA", "minimax_h3_turbo_v4_step600_ema.safetensors")
53
  LIGHTX_REPO = os.environ.get("H3_LIGHTX_REPO", "lightx2v/Minimax-h3-Turbo")
54
  LIGHTX_FILE = os.environ.get("H3_LIGHTX_FILE", "minimax_h3_fl2v_turbo_4step_v0.1.safetensors")
55
+ LIGHTX8_FILE = os.environ.get("H3_LIGHTX8_FILE", "minimax_h3_fl2v_turbo_8step_v1.0_bf16.safetensors")
56
  LIGHTX_ALPHA = 8
57
  REALISM_REPO = os.environ.get("H3_REALISM_REPO", "fal/MiniMax-H3-Realism-People-LoRA")
58
  REALISM_FILE = os.environ.get("H3_REALISM_FILE", "h3-realism-people-t2v-i2v-r2v.safetensors")
 
109
  }
110
 
111
 
112
+ def _load_lightx(file: str = LIGHTX_FILE) -> dict:
113
+ """A diffusers-native PEFT checkpoint from `lightx2v/Minimax-h3-Turbo` — keys map name-for-name."""
114
  from huggingface_hub import hf_hub_download
115
  from safetensors.torch import load_file
116
 
117
+ lora = load_file(hf_hub_download(LIGHTX_REPO, file))
118
  suffix_a, suffix_b = ".lora_A.default.weight", ".lora_B.default.weight"
119
  bases = sorted({key[: -len(suffix_a)] for key in lora if key.endswith(suffix_a)})
120
  ranks = {lora[f"{name}{suffix_a}"].shape[0] for name in bases}
121
  if len(ranks) != 1:
122
+ raise ValueError(f"Mixed LoRA ranks in {file}: {sorted(ranks)}")
123
  entries = [(f"{name}.weight", lora[f"{name}{suffix_a}"], lora[f"{name}{suffix_b}"]) for name in bases]
124
  return {
125
+ "label": f"{LIGHTX_REPO}/{file}",
126
  "scale": LIGHTX_ALPHA / ranks.pop(),
127
  "entries": entries,
128
  }
 
198
  sets["larry"] = _load_larry(inner_dim)
199
  if os.environ.get("H3_LIGHTX", "on").lower() not in ("", "off", "none"):
200
  sets["lightx"] = _load_lightx()
201
+ if os.environ.get("H3_LIGHTX8", "on").lower() not in ("", "off", "none"):
202
+ sets["lightx8"] = _load_lightx(LIGHTX8_FILE)
203
  if os.environ.get("H3_REALISM", "on").lower() not in ("", "off", "none"):
204
  sets["realism"] = _load_realism(inner_dim)
205
  if os.environ.get("H3_JOYFOX", "on").lower() not in ("", "off", "none"):