Spaces:
Running on Zero
Running on Zero
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 +7 -6
- __pycache__/app.cpython-314.pyc +0 -0
- __pycache__/h3_lora.cpython-314.pyc +0 -0
- app.py +14 -7
- h3_lora.py +35 -3
- index.html +4 -2
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 **
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
`
|
|
|
|
| 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` | `
|
| 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=
|
| 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) ·
|
|
|
|
| 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 =
|
| 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
|
| 31 |
-
LORA_FILE = os.environ.get("H3_LORA", "
|
| 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 |
-
|
|
|
|
| 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">
|
| 206 |
-
<input type="range" id="steps" min="2" max="40" step="1" value="
|
| 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.
|