akhaliq HF Staff commited on
Commit
868ff3a
·
1 Parent(s): d82c64a

Default to the ckpt500 turbo weights, add a per-request LoRA toggle

Browse files

- H3_LORA default is now minimax_h3_turbo_4step_ckpt500.safetensors, the
model card's recommended default (newest, sharpest, non-EMA)
- the low-rank factors stay resident after the startup fold, so
h3_lora.set_enabled() folds/unfolds B@A in place per request; the AoTI
blocks read the same live storage. UI gets a 'Turbo LoRA' checkbox
(default on), the API a use_lora parameter (default true)
- default steps 4 -> 6 per the card's updated guidance (comfort zone is
6-8 at this checkpoint; 4 is softer)

README.md CHANGED
@@ -47,11 +47,12 @@ callers get the same treatment the old upload event gave.
47
  ## 4-step Turbo LoRA
48
 
49
  The transformer runs with [`larryvrh/MiniMax-H3-Turbo-Lora`](https://huggingface.co/larryvrh/MiniMax-H3-Turbo-Lora)
50
- folded into its bf16 weights at startup (`h3_lora.py`), so the default is **4 sampling steps** instead of 28 the
51
- LoRA's design point. The checkpoint is an early preview: sharper than the base model at 4 steps, but not a finished
52
- run. The fold mirrors the diffusers key conversion exactly (fused-QKV thirds, the `SwiGLU` gate/value swap, the
53
- shared AdaLN row layout) and happens before the AoTI package is patched in, so compiled blocks carry the update too.
54
- `H3_LORA=minimax_h3_turbo_4step_ema.safetensors` picks the smoother time-averaged variant, `H3_LORA=off` disables.
 
55
 
56
  ## AoTI-compiled blocks
57
 
@@ -175,7 +176,7 @@ one-time `PIPE.to("cuda")` is inside the first row's 339 s and does not reappear
175
  | `H3_ATTENTION` | `_native_cudnn` | cuDNN's fused kernel, 10–20% faster than the SDPA default and needs nothing installed. flash-attention 3 is sm90-only and this pool is sm120. |
176
  | `H3_GPU_DURATION` | `900` | Seconds per request; the pool applies a 1.5 duration factor. |
177
  | `H3_GPU_SIZE` | `xlarge` | ZeroGPU allocation size. `large` does not fit. |
178
- | `H3_LORA` | `minimax_h3_turbo_4step.safetensors` | Turbo LoRA file folded into the transformer at startup. `off` disables. |
179
  | `H3_LORA_REPO` | `larryvrh/MiniMax-H3-Turbo-Lora` | Hub repo the LoRA is fetched from. |
180
 
181
  ## Whose GPU quota pays
 
47
  ## 4-step Turbo LoRA
48
 
49
  The transformer runs with [`larryvrh/MiniMax-H3-Turbo-Lora`](https://huggingface.co/larryvrh/MiniMax-H3-Turbo-Lora)
50
+ folded into its bf16 weights at startup (`h3_lora.py`), so the default is **6 sampling steps** instead of 28 (the
51
+ card's comfort zone at the current checkpoint; 4 is the design point but softer). The fold mirrors the diffusers key
52
+ conversion exactly (fused-QKV thirds, the `SwiGLU` gate/value swap, the shared AdaLN row layout) and happens before
53
+ the AoTI package is patched in, so compiled blocks carry the update too. The low-rank factors stay resident, so the
54
+ **Turbo LoRA checkbox** (or the `use_lora` API parameter) folds/unfolds `lora_B @ lora_A` in place per request —
55
+ off gives the base model at whatever step count you pick. `H3_LORA` selects the file (`off` disables entirely).
56
 
57
  ## AoTI-compiled blocks
58
 
 
176
  | `H3_ATTENTION` | `_native_cudnn` | cuDNN's fused kernel, 10–20% faster than the SDPA default and needs nothing installed. flash-attention 3 is sm90-only and this pool is sm120. |
177
  | `H3_GPU_DURATION` | `900` | Seconds per request; the pool applies a 1.5 duration factor. |
178
  | `H3_GPU_SIZE` | `xlarge` | ZeroGPU allocation size. `large` does not fit. |
179
+ | `H3_LORA` | `minimax_h3_turbo_4step_ckpt500.safetensors` | Turbo LoRA file folded into the transformer at startup. `off` disables. |
180
  | `H3_LORA_REPO` | `larryvrh/MiniMax-H3-Turbo-Lora` | Hub repo the LoRA is fetched from. |
181
 
182
  ## Whose GPU quota pays
__pycache__/app.cpython-314.pyc ADDED
Binary file (22.6 kB). View file
 
__pycache__/h3_lora.cpython-314.pyc ADDED
Binary file (8.2 kB). View file
 
app.py CHANGED
@@ -224,7 +224,7 @@ _DECODE_BASE, _DECODE_PER_DEFAULT_CANVAS, _DEFAULT_CANVAS_PIXELS = 15, 15, 960 *
224
  _PLACEMENT_ALLOWANCE, _PAD = 12, 10
225
 
226
 
227
- def get_duration(prompt_embeds, text_token_tags, image, last_image, height, width, num_frames, steps, seed, *a, **k):
228
  height, width, num_frames, steps = int(height), int(width), int(num_frames), int(steps)
229
  latent_frames = (num_frames - LATENTS_PER_CHUNK) // FRAMES_PER_CHUNK * LATENTS_PER_CHUNK + 2
230
  patches = (height // 32) * (width // 32)
@@ -235,7 +235,7 @@ def get_duration(prompt_embeds, text_token_tags, image, last_image, height, widt
235
 
236
 
237
  @spaces.GPU(duration=get_duration, size=GPU_SIZE)
238
- def _generate(prompt_embeds, text_token_tags, image, last_image, height, width, num_frames, steps, seed):
239
  """The only thing on GPU time: the packed-sequence denoise loop and the two decoders.
240
 
241
  Only the three generated outputs come back — a `@spaces.GPU` return crosses a process boundary by pickling, and
@@ -243,6 +243,11 @@ def _generate(prompt_embeds, text_token_tags, image, last_image, height, width,
243
  """
244
  import torch
245
 
 
 
 
 
 
246
  if PLACEMENT == "lazy":
247
  PIPE.to("cuda")
248
  elif PLACEMENT == "pack":
@@ -297,7 +302,7 @@ def _fit_keyframe(image_path, current_canvas):
297
  return image_path, label
298
 
299
 
300
- def generate(prompt, image_path=None, last_image_path=None, canvas=DEFAULT_CANVAS, duration=5, steps=4, seed=42, upsample=False, ip_token=None):
301
  """One request. `upsample` is last and defaults off, so a positional API client that predates it is unaffected."""
302
  if LOAD_ERROR:
303
  raise Exception(LOAD_ERROR)
@@ -345,6 +350,7 @@ def generate(prompt, image_path=None, last_image_path=None, canvas=DEFAULT_CANVA
345
  num_frames,
346
  steps,
347
  seed,
 
348
  )
349
  generate_seconds = time.time() - started
350
 
@@ -356,7 +362,8 @@ def generate(prompt, image_path=None, last_image_path=None, canvas=DEFAULT_CANVA
356
  f"{width}x{height} · {num_frames} frames ({num_frames / FPS:.3f} s) · {int(steps)} steps · "
357
  f"conditioner {condition_seconds:.0f}s ({plan['num_text_tokens']} tokens"
358
  f"{', upsampled' if refined else ''}) · "
359
- f"denoise + decode {generate_seconds:.0f}s ({generate_seconds / int(steps):.1f} s/step) · seed {int(seed)}"
 
360
  )
361
  print(f"[gen] {report}", flush=True)
362
  return FileData(path=path), report, refined
@@ -372,12 +379,12 @@ app = Server(title="MiniMax-H3 Studio")
372
 
373
  @app.api(name="generate")
374
  def _generate_api(prompt: str, image_path: FileData | None = None, last_image_path: FileData | None = None,
375
- canvas: str = DEFAULT_CANVAS, duration: float = 5, steps: int = 4, seed: float = 42,
376
- upsample: bool = False, request: Request = None) -> tuple[FileData, str, str]:
377
  """Generate a video with a synchronized soundtrack. Returns (video, report, refined prompt)."""
378
  # `request` is injected by the event system, not an API input; its x-ip-token bills the conditioner to the caller.
379
  ip_token = request.headers.get("x-ip-token") if request is not None else None
380
- return generate(prompt, image_path, last_image_path, canvas, duration, steps, seed, upsample, ip_token=ip_token)
381
 
382
 
383
  @app.get("/status")
 
224
  _PLACEMENT_ALLOWANCE, _PAD = 12, 10
225
 
226
 
227
+ def get_duration(prompt_embeds, text_token_tags, image, last_image, height, width, num_frames, steps, seed, use_lora=True, *a, **k):
228
  height, width, num_frames, steps = int(height), int(width), int(num_frames), int(steps)
229
  latent_frames = (num_frames - LATENTS_PER_CHUNK) // FRAMES_PER_CHUNK * LATENTS_PER_CHUNK + 2
230
  patches = (height // 32) * (width // 32)
 
235
 
236
 
237
  @spaces.GPU(duration=get_duration, size=GPU_SIZE)
238
+ def _generate(prompt_embeds, text_token_tags, image, last_image, height, width, num_frames, steps, seed, use_lora=True):
239
  """The only thing on GPU time: the packed-sequence denoise loop and the two decoders.
240
 
241
  Only the three generated outputs come back — a `@spaces.GPU` return crosses a process boundary by pickling, and
 
243
  """
244
  import torch
245
 
246
+ import h3_lora
247
+
248
+ # Fold/unfold the turbo LoRA in place to match the request (a no-op when the state already matches).
249
+ h3_lora.set_enabled(PIPE.transformer, bool(use_lora))
250
+
251
  if PLACEMENT == "lazy":
252
  PIPE.to("cuda")
253
  elif PLACEMENT == "pack":
 
302
  return image_path, label
303
 
304
 
305
+ def generate(prompt, image_path=None, last_image_path=None, canvas=DEFAULT_CANVAS, duration=5, steps=6, seed=42, upsample=False, use_lora=True, ip_token=None):
306
  """One request. `upsample` is last and defaults off, so a positional API client that predates it is unaffected."""
307
  if LOAD_ERROR:
308
  raise Exception(LOAD_ERROR)
 
350
  num_frames,
351
  steps,
352
  seed,
353
+ use_lora,
354
  )
355
  generate_seconds = time.time() - started
356
 
 
362
  f"{width}x{height} · {num_frames} frames ({num_frames / FPS:.3f} s) · {int(steps)} steps · "
363
  f"conditioner {condition_seconds:.0f}s ({plan['num_text_tokens']} tokens"
364
  f"{', upsampled' if refined else ''}) · "
365
+ f"denoise + decode {generate_seconds:.0f}s ({generate_seconds / int(steps):.1f} s/step) · "
366
+ f"turbo LoRA {'on' if use_lora else 'off'} · seed {int(seed)}"
367
  )
368
  print(f"[gen] {report}", flush=True)
369
  return FileData(path=path), report, refined
 
379
 
380
  @app.api(name="generate")
381
  def _generate_api(prompt: str, image_path: FileData | None = None, last_image_path: FileData | None = None,
382
+ canvas: str = DEFAULT_CANVAS, duration: float = 5, steps: int = 6, seed: float = 42,
383
+ upsample: bool = False, use_lora: bool = True, request: Request = None) -> tuple[FileData, str, str]:
384
  """Generate a video with a synchronized soundtrack. Returns (video, report, refined prompt)."""
385
  # `request` is injected by the event system, not an API input; its x-ip-token bills the conditioner to the caller.
386
  ip_token = request.headers.get("x-ip-token") if request is not None else None
387
+ return generate(prompt, image_path, last_image_path, canvas, duration, steps, seed, upsample, use_lora, ip_token=ip_token)
388
 
389
 
390
  @app.get("/status")
h3_lora.py CHANGED
@@ -27,8 +27,8 @@ import os
27
  import torch
28
 
29
  LORA_REPO = os.environ.get("H3_LORA_REPO", "larryvrh/MiniMax-H3-Turbo-Lora")
30
- # The trained weights; the `_ema` variant is an immature time-averaged snapshot at this checkpoint. `off` disables.
31
- LORA_FILE = os.environ.get("H3_LORA", "minimax_h3_turbo_4step.safetensors")
32
 
33
 
34
  def _delta_targets(name: str, delta: torch.Tensor, inner_dim: int) -> list[tuple[str, torch.Tensor]]:
@@ -75,14 +75,46 @@ def apply_lora(transformer) -> str | None:
75
  params = dict(transformer.named_parameters())
76
 
77
  folded = 0
 
78
  for name in bases:
79
  a = lora[f"{name}.lora_A.weight"].float()
80
  b = lora[f"{name}.lora_B.weight"].float()
81
  delta = b @ a # alpha == rank, so the scale is 1
82
- for key, converted in _delta_targets(name, delta, inner_dim):
 
83
  param = params.get(key)
84
  if param is None:
85
  raise KeyError(f"LoRA target `{key}` (from `{name}`) not found in the transformer")
86
  param.data = (param.data.float() + converted).to(param.dtype)
87
  folded += 1
 
 
 
 
 
 
 
 
 
 
 
 
88
  return f"LoRA `{LORA_REPO}/{LORA_FILE}` folded into {folded} weights ({len(bases)} modules)"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  import torch
28
 
29
  LORA_REPO = os.environ.get("H3_LORA_REPO", "larryvrh/MiniMax-H3-Turbo-Lora")
30
+ # The recommended default per the model card: ckpt500, non-EMA (newest, sharpest). `off` disables.
31
+ LORA_FILE = os.environ.get("H3_LORA", "minimax_h3_turbo_4step_ckpt500.safetensors")
32
 
33
 
34
  def _delta_targets(name: str, delta: torch.Tensor, inner_dim: int) -> list[tuple[str, torch.Tensor]]:
 
75
  params = dict(transformer.named_parameters())
76
 
77
  folded = 0
78
+ entries = []
79
  for name in bases:
80
  a = lora[f"{name}.lora_A.weight"].float()
81
  b = lora[f"{name}.lora_B.weight"].float()
82
  delta = b @ a # alpha == rank, so the scale is 1
83
+ targets = _delta_targets(name, delta, inner_dim)
84
+ for key, converted in targets:
85
  param = params.get(key)
86
  if param is None:
87
  raise KeyError(f"LoRA target `{key}` (from `{name}`) not found in the transformer")
88
  param.data = (param.data.float() + converted).to(param.dtype)
89
  folded += 1
90
+ entries.append((name, [key for key, _ in targets]))
91
+
92
+ # Keep the low-rank factors (~744 MB in bf16) so the fold can be toggled per request: adding/subtracting
93
+ # B @ A in place round-trips through one bf16 rounding, and the AoTI blocks read the same live storage.
94
+ transformer._lora_state = {
95
+ "enabled": True,
96
+ "inner_dim": inner_dim,
97
+ "entries": [
98
+ (name, lora[f"{name}.lora_A.weight"], lora[f"{name}.lora_B.weight"], keys)
99
+ for name, keys in entries
100
+ ],
101
+ }
102
  return f"LoRA `{LORA_REPO}/{LORA_FILE}` folded into {folded} weights ({len(bases)} modules)"
103
+
104
+
105
+ def set_enabled(transformer, enabled: bool) -> bool:
106
+ """Fold/unfold the LoRA in place. No-op when no LoRA was loaded or the state already matches."""
107
+ state = getattr(transformer, "_lora_state", None)
108
+ if state is None or state["enabled"] == enabled:
109
+ return state["enabled"] if state else False
110
+ import torch
111
+
112
+ sign = 1.0 if enabled else -1.0
113
+ params = dict(transformer.named_parameters())
114
+ for name, a, b, keys in state["entries"]:
115
+ delta = b.to(torch.float32) @ a.to(torch.float32)
116
+ for key, converted in zip(keys, _delta_targets(name, delta, state["inner_dim"])):
117
+ param = params[key]
118
+ param.data = (param.data.float() + sign * converted.to(param.device)).to(param.dtype)
119
+ state["enabled"] = enabled
120
+ return enabled
index.html CHANGED
@@ -180,6 +180,7 @@
180
  <div class="deck-label">Prompt</div>
181
  <textarea id="prompt" spellcheck="false">A red fox trotting through a snowy pine forest at dawn, snow crunching underfoot</textarea>
182
  </div>
 
183
  <label class="check"><input type="checkbox" id="upsample"> Upsample prompt</label>
184
 
185
  <div>
@@ -202,8 +203,8 @@
202
  <input type="range" id="duration" min="2" max="14" step="1" value="5">
203
  </div>
204
  <div>
205
- <div class="slider-row"><span>Steps</span><output id="steps-out">4</output></div>
206
- <input type="range" id="steps" min="2" max="40" step="1" value="4">
207
  </div>
208
  <div>
209
  <div class="deck-label">Seed</div>
@@ -351,6 +352,7 @@ $("run").addEventListener("click", async () => {
351
  steps: Number($("steps").value),
352
  seed: Number($("seed").value),
353
  upsample: $("upsample").checked,
 
354
  });
355
  let data = result.data;
356
  // Server mode returns the tuple as ONE Api output: unwrap [[video, report, refined]] too.
 
180
  <div class="deck-label">Prompt</div>
181
  <textarea id="prompt" spellcheck="false">A red fox trotting through a snowy pine forest at dawn, snow crunching underfoot</textarea>
182
  </div>
183
+ <label class="check"><input type="checkbox" id="use_lora" checked> Turbo LoRA (fast steps)</label>
184
  <label class="check"><input type="checkbox" id="upsample"> Upsample prompt</label>
185
 
186
  <div>
 
203
  <input type="range" id="duration" min="2" max="14" step="1" value="5">
204
  </div>
205
  <div>
206
+ <div class="slider-row"><span>Steps</span><output id="steps-out">6</output></div>
207
+ <input type="range" id="steps" min="2" max="40" step="1" value="6">
208
  </div>
209
  <div>
210
  <div class="deck-label">Seed</div>
 
352
  steps: Number($("steps").value),
353
  seed: Number($("seed").value),
354
  upsample: $("upsample").checked,
355
+ use_lora: $("use_lora").checked,
356
  });
357
  let data = result.data;
358
  // Server mode returns the tuple as ONE Api output: unwrap [[video, report, refined]] too.