Spaces:
Running on Zero
Running on Zero
Dynamic GPU duration from the packed sequence, cached examples, torchaudio note
Browse files- .gitattributes +2 -0
- README.md +10 -0
- app.py +144 -9
- examples/motion.mp4 +3 -0
- examples/subject.png +0 -0
- examples/voice.wav +3 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
examples/motion.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
examples/voice.wav filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -151,3 +151,13 @@ up to 362, i.e. 15.083 s, and is refused.
|
|
| 151 |
MiniMax-H3 is modular-only and not in a released `diffusers`, so the integration branch's `src/diffusers` tree is
|
| 152 |
vendored here as a top-level `diffusers/` package; the working directory comes first on `sys.path`, so there is no
|
| 153 |
install step. `requirements.txt` only carries what that tree imports.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
MiniMax-H3 is modular-only and not in a released `diffusers`, so the integration branch's `src/diffusers` tree is
|
| 152 |
vendored here as a top-level `diffusers/` package; the working directory comes first on `sys.path`, so there is no
|
| 153 |
install step. `requirements.txt` only carries what that tree imports.
|
| 154 |
+
|
| 155 |
+
Two of those are `ref2va`-only and easy to miss. PyAV decodes a reference video or audio file as the reference is
|
| 156 |
+
built, and **`torchaudio`** resamples a soundtrack that is not already at the audio VAE's 32 kHz — a 32 kHz
|
| 157 |
+
reference skips the resample entirely, so the dependency only shows up once someone brings audio at another rate:
|
| 158 |
+
|
| 159 |
+
```
|
| 160 |
+
ImportError: Resampling a MiniMax-H3 reference soundtrack from 24000 Hz to 32000 Hz needs `torchaudio`.
|
| 161 |
+
```
|
| 162 |
+
|
| 163 |
+
The conditioner Space needs it as well: its `setup` step prepares the very same waveforms this one does.
|
app.py
CHANGED
|
@@ -36,8 +36,12 @@ PLACEMENT = os.environ.get("H3_PLACEMENT", "lazy").lower()
|
|
| 36 |
# cuDNN's fused attention is 10-20% faster than the SDPA default on this pool and needs nothing installed.
|
| 37 |
# flash-attention 3 is sm90-only and this card is sm120 (the `zero-a10g` flavour name is legacy).
|
| 38 |
ATTENTION = os.environ.get("H3_ATTENTION", "_native_cudnn").lower()
|
| 39 |
-
GPU_DURATION = int(os.environ.get("H3_GPU_DURATION", "900"))
|
| 40 |
GPU_SIZE = os.environ.get("H3_GPU_SIZE", "xlarge")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
# MiniMax-H3's own canvases, i.e. `resolve_canvas_size` from `diffusers.modular_pipelines.minimax_h3.packing`
|
| 43 |
# evaluated for the six released aspect ratios. Hardcoded so the UI renders before `diffusers` is importable.
|
|
@@ -70,6 +74,28 @@ MIN_REFERENCE_VIDEO, MAX_REFERENCE_VIDEO = 2.0, 15.0
|
|
| 70 |
# built up front and revealed one at a time, because a demo asking for two subjects should not open with nine boxes.
|
| 71 |
MAX_IMAGE_SLOTS, OPEN_IMAGE_SLOTS = 9, 2
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
def snap_frames(seconds: float) -> int:
|
| 75 |
"""The frame count MiniMax-H3's video VAE can decode: the next `17 * n + 5` at 24 fps."""
|
|
@@ -79,6 +105,86 @@ def snap_frames(seconds: float) -> int:
|
|
| 79 |
return frames
|
| 80 |
|
| 81 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
PIPE = None
|
| 83 |
MANAGER = None
|
| 84 |
LOAD_ERROR: str | None = None
|
|
@@ -301,7 +407,7 @@ def encode_remote(prompt, references, canvas, num_frames):
|
|
| 301 |
return handle.get_tensor("prompt_embeds"), handle.get_tensor("text_token_tags"), handle.metadata(), plan
|
| 302 |
|
| 303 |
|
| 304 |
-
@spaces.GPU(duration=
|
| 305 |
def _generate(prompt_embeds, text_token_tags, references, height, width, num_frames, steps, seed):
|
| 306 |
"""The only thing on GPU time: the two reference encoders, the packed-sequence denoise loop and the decoders.
|
| 307 |
|
|
@@ -335,8 +441,13 @@ def _generate(prompt_embeds, text_token_tags, references, height, width, num_fra
|
|
| 335 |
|
| 336 |
|
| 337 |
def generate(
|
|
|
|
|
|
|
|
|
|
| 338 |
prompt,
|
| 339 |
image_1=None,
|
|
|
|
|
|
|
| 340 |
image_2=None,
|
| 341 |
image_3=None,
|
| 342 |
image_4=None,
|
|
@@ -345,8 +456,6 @@ def generate(
|
|
| 345 |
image_7=None,
|
| 346 |
image_8=None,
|
| 347 |
image_9=None,
|
| 348 |
-
audio_path=None,
|
| 349 |
-
video_path=None,
|
| 350 |
canvas=DEFAULT_CANVAS,
|
| 351 |
match=True,
|
| 352 |
duration=5,
|
|
@@ -493,13 +602,39 @@ with gr.Blocks(title="MiniMax-H3 Reference") as demo:
|
|
| 493 |
duration_controls, [audio, video, match], [match, duration], show_progress="hidden", api_name=False
|
| 494 |
)
|
| 495 |
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 501 |
)
|
| 502 |
|
|
|
|
|
|
|
| 503 |
|
| 504 |
if __name__ == "__main__":
|
| 505 |
demo.launch(show_error=True, theme=gr.themes.Citrus(), css=CSS)
|
|
|
|
| 36 |
# cuDNN's fused attention is 10-20% faster than the SDPA default on this pool and needs nothing installed.
|
| 37 |
# flash-attention 3 is sm90-only and this card is sm120 (the `zero-a10g` flavour name is legacy).
|
| 38 |
ATTENTION = os.environ.get("H3_ATTENTION", "_native_cudnn").lower()
|
|
|
|
| 39 |
GPU_SIZE = os.environ.get("H3_GPU_SIZE", "xlarge")
|
| 40 |
+
# Bounds on what `get_duration` may ask the pool to reserve. A request that runs out of GPU time is a total loss, so
|
| 41 |
+
# the estimate is deliberately generous — but a *fixed* 900 s ceiling for every request is what makes the account hit
|
| 42 |
+
# "too many ZeroGPU credits allocated to running tasks", because the pool reserves the number it is given.
|
| 43 |
+
MIN_GPU_DURATION = int(os.environ.get("H3_GPU_DURATION_MIN", "120"))
|
| 44 |
+
MAX_GPU_DURATION = int(os.environ.get("H3_GPU_DURATION_MAX", "1500"))
|
| 45 |
|
| 46 |
# MiniMax-H3's own canvases, i.e. `resolve_canvas_size` from `diffusers.modular_pipelines.minimax_h3.packing`
|
| 47 |
# evaluated for the six released aspect ratios. Hardcoded so the UI renders before `diffusers` is importable.
|
|
|
|
| 74 |
# built up front and revealed one at a time, because a demo asking for two subjects should not open with nine boxes.
|
| 75 |
MAX_IMAGE_SLOTS, OPEN_IMAGE_SLOTS = 9, 2
|
| 76 |
|
| 77 |
+
# --- What a request costs, for `get_duration` -------------------------------------------------------------------
|
| 78 |
+
#
|
| 79 |
+
# MiniMax-H3 attends over one packed sequence, so the cost of a step is a function of its length `S` alone. Fitted
|
| 80 |
+
# on the `t2va` half (`minimax-h3`, AoTI on, same pool and same silicon) over two canvases at 124 frames:
|
| 81 |
+
#
|
| 82 |
+
# 544x544, S = 10693 -> 2.4 s/step
|
| 83 |
+
# 960x544, S = 18870 -> 4.4 s/step
|
| 84 |
+
#
|
| 85 |
+
# through `s = LINEAR * S + QUADRATIC * S**2` — linear for the matmuls, quadratic for the attention. Checked against
|
| 86 |
+
# two live `ref2va` requests on this Space, which is the regime the reference rows actually put it in:
|
| 87 |
+
#
|
| 88 |
+
# one 1344x768 image reference, S ~= 33232 -> 8.3 predicted, ~8.5 measured
|
| 89 |
+
# that image plus a 2.5 s video reference, S ~= 54039 -> 14.6 predicted, ~16.1 measured
|
| 90 |
+
#
|
| 91 |
+
# so the fit holds to about 10% three times past the canvas it was taken from, and `SAFETY` covers the rest.
|
| 92 |
+
STEP_LINEAR, STEP_QUADRATIC, SAFETY = 2.13e-4, 1.069e-9, 1.3
|
| 93 |
+
# The lazy 72.16 GiB `PIPE.to("cuda")` a cold worker pays inside its first GPU call. Measured at ~45 s; every request
|
| 94 |
+
# has to carry it, because nothing on this side knows whether the worker it lands on is cold.
|
| 95 |
+
PLACEMENT_ALLOWANCE = int(os.environ.get("H3_PLACEMENT_ALLOWANCE", "90"))
|
| 96 |
+
AUDIO_LATENTS_PER_SECOND, AUDIO_CHANNELS = 40, 2
|
| 97 |
+
REFERENCE_IMAGE_SHORT_EDGE, CANVAS_MULTIPLE = 2048, 32
|
| 98 |
+
|
| 99 |
|
| 100 |
def snap_frames(seconds: float) -> int:
|
| 101 |
"""The frame count MiniMax-H3's video VAE can decode: the next `17 * n + 5` at 24 fps."""
|
|
|
|
| 105 |
return frames
|
| 106 |
|
| 107 |
|
| 108 |
+
def video_latent_frames(num_frames: int) -> int:
|
| 109 |
+
"""`17 * n + 5` frames become `5 * n + 2` video latents."""
|
| 110 |
+
return 5 * ((num_frames - LATENTS_PER_CHUNK) // FRAMES_PER_CHUNK) + 2
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def target_rows(height: int, width: int, num_frames: int) -> int:
|
| 114 |
+
"""The generated rows of the packed sequence: video patched `(1, 2, 2)`, plus two audio rows per latent."""
|
| 115 |
+
video = video_latent_frames(num_frames) * (height // CANVAS_MULTIPLE) * (width // CANVAS_MULTIPLE)
|
| 116 |
+
return video + round(num_frames / FPS * AUDIO_LATENTS_PER_SECOND) * AUDIO_CHANNELS
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def reference_rows(references: list[tuple[str, str]], num_frames: int) -> int:
|
| 120 |
+
"""The rows the reference blocks add, from metadata alone — no decode.
|
| 121 |
+
|
| 122 |
+
This mirrors what `MiniMaxH3Ref2VASetupStep` and the reference encoder will do, closely enough to size a GPU
|
| 123 |
+
reservation with. An image is resized to a 2048 pixel short edge and encoded as a single frame; a video is put on
|
| 124 |
+
the canvas *its own* aspect ratio resolves to, truncated to the generated frame count and then snapped **down**
|
| 125 |
+
to a `17 * n + 5` the VAE encodes without padding; a soundtrack contributes two rows per 1/40 s.
|
| 126 |
+
"""
|
| 127 |
+
from PIL import Image
|
| 128 |
+
|
| 129 |
+
from diffusers.modular_pipelines.minimax_h3.packing import resolve_canvas_size
|
| 130 |
+
|
| 131 |
+
rows = 0
|
| 132 |
+
for kind, path in references:
|
| 133 |
+
if kind == "image":
|
| 134 |
+
width, height = Image.open(path).size
|
| 135 |
+
scale = REFERENCE_IMAGE_SHORT_EDGE / min(width, height)
|
| 136 |
+
resolved = [
|
| 137 |
+
max(CANVAS_MULTIPLE, round(edge * scale / CANVAS_MULTIPLE) * CANVAS_MULTIPLE)
|
| 138 |
+
for edge in (height, width)
|
| 139 |
+
]
|
| 140 |
+
rows += (resolved[0] // CANVAS_MULTIPLE) * (resolved[1] // CANVAS_MULTIPLE)
|
| 141 |
+
continue
|
| 142 |
+
|
| 143 |
+
video_seconds, audio_seconds = probe(path)
|
| 144 |
+
if kind == "video" and video_seconds is not None:
|
| 145 |
+
import av
|
| 146 |
+
|
| 147 |
+
with av.open(path) as container:
|
| 148 |
+
stream = container.streams.video[0]
|
| 149 |
+
source_height, source_width = stream.height, stream.width
|
| 150 |
+
canvas_height, canvas_width = resolve_canvas_size(source_width, source_height)
|
| 151 |
+
# Resampled onto 24 fps and capped at the generated length, then snapped down to `17 * n + 5`.
|
| 152 |
+
frames = min(round(video_seconds * FPS), num_frames)
|
| 153 |
+
snapped = max(1, (frames - LATENTS_PER_CHUNK) // FRAMES_PER_CHUNK) * FRAMES_PER_CHUNK + LATENTS_PER_CHUNK
|
| 154 |
+
rows += (
|
| 155 |
+
video_latent_frames(snapped)
|
| 156 |
+
* (canvas_height // CANVAS_MULTIPLE)
|
| 157 |
+
* (canvas_width // CANVAS_MULTIPLE)
|
| 158 |
+
)
|
| 159 |
+
if audio_seconds is not None:
|
| 160 |
+
seconds = min(audio_seconds, num_frames / FPS)
|
| 161 |
+
rows += round(seconds * AUDIO_LATENTS_PER_SECOND) * AUDIO_CHANNELS
|
| 162 |
+
return rows
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def get_duration(prompt_embeds, text_token_tags, references, height, width, num_frames, steps, seed, **_):
|
| 166 |
+
"""Seconds of GPU to reserve for one request, from the packed sequence it is about to build.
|
| 167 |
+
|
| 168 |
+
Takes the arguments of the `@spaces.GPU` function it decorates — and tolerates the `gr.Progress` `spaces`
|
| 169 |
+
injects — so it can price the request rather than reserve a flat ceiling for all of them.
|
| 170 |
+
|
| 171 |
+
The text rows are exact: `text_token_tags` is the conditioner's own answer, already on this side. The reference
|
| 172 |
+
and target rows come from `reference_rows` and `target_rows`.
|
| 173 |
+
"""
|
| 174 |
+
sequence = int(text_token_tags.shape[0]) + reference_rows(references, num_frames) + target_rows(
|
| 175 |
+
height, width, num_frames
|
| 176 |
+
)
|
| 177 |
+
denoise = int(steps) * (STEP_LINEAR * sequence + STEP_QUADRATIC * sequence**2) * SAFETY
|
| 178 |
+
# The two reference encoders, ahead of the loop, and the two decoders plus the mux after it. Both scale with what
|
| 179 |
+
# they are handed rather than with the step count.
|
| 180 |
+
encode = 5 + reference_rows(references, num_frames) * 1e-3
|
| 181 |
+
decode = 15 + 25 * (height * width * num_frames) / (960 * 544 * 124)
|
| 182 |
+
total = PLACEMENT_ALLOWANCE + encode + denoise + decode + 10
|
| 183 |
+
duration = max(MIN_GPU_DURATION, min(MAX_GPU_DURATION, int(total)))
|
| 184 |
+
print(f"[ref2va] S={sequence} -> reserving {duration}s ({denoise:.0f}s of denoise at {steps} steps)", flush=True)
|
| 185 |
+
return duration
|
| 186 |
+
|
| 187 |
+
|
| 188 |
PIPE = None
|
| 189 |
MANAGER = None
|
| 190 |
LOAD_ERROR: str | None = None
|
|
|
|
| 407 |
return handle.get_tensor("prompt_embeds"), handle.get_tensor("text_token_tags"), handle.metadata(), plan
|
| 408 |
|
| 409 |
|
| 410 |
+
@spaces.GPU(duration=get_duration, size=GPU_SIZE)
|
| 411 |
def _generate(prompt_embeds, text_token_tags, references, height, width, num_frames, steps, seed):
|
| 412 |
"""The only thing on GPU time: the two reference encoders, the packed-sequence denoise loop and the decoders.
|
| 413 |
|
|
|
|
| 441 |
|
| 442 |
|
| 443 |
def generate(
|
| 444 |
+
# The first four are the columns `gr.Examples` varies, and they lead the signature for that reason: an example
|
| 445 |
+
# row is applied to `inputs` positionally, so the exampled components have to be the leading parameters. Every
|
| 446 |
+
# parameter has a default, which is what lets a four-column row call this at all.
|
| 447 |
prompt,
|
| 448 |
image_1=None,
|
| 449 |
+
audio_path=None,
|
| 450 |
+
video_path=None,
|
| 451 |
image_2=None,
|
| 452 |
image_3=None,
|
| 453 |
image_4=None,
|
|
|
|
| 456 |
image_7=None,
|
| 457 |
image_8=None,
|
| 458 |
image_9=None,
|
|
|
|
|
|
|
| 459 |
canvas=DEFAULT_CANVAS,
|
| 460 |
match=True,
|
| 461 |
duration=5,
|
|
|
|
| 602 |
duration_controls, [audio, video, match], [match, duration], show_progress="hidden", api_name=False
|
| 603 |
)
|
| 604 |
|
| 605 |
+
# Same order as `generate`'s signature: the exampled four first, then the remaining image slots.
|
| 606 |
+
request = [prompt, images[0], audio, video, *images[1:], canvas, match, duration, steps, seed]
|
| 607 |
+
|
| 608 |
+
gr.Examples(
|
| 609 |
+
examples=[
|
| 610 |
+
[
|
| 611 |
+
"The character walks through a neon-lit street in the rain, humming to themselves",
|
| 612 |
+
"examples/subject.png",
|
| 613 |
+
None,
|
| 614 |
+
None,
|
| 615 |
+
],
|
| 616 |
+
[
|
| 617 |
+
"The character speaks to camera in a quiet room, lips matching every word",
|
| 618 |
+
"examples/subject.png",
|
| 619 |
+
"examples/voice.wav",
|
| 620 |
+
None,
|
| 621 |
+
],
|
| 622 |
+
[
|
| 623 |
+
"The character moves with the same camera push, down a rainy alley at night",
|
| 624 |
+
"examples/subject.png",
|
| 625 |
+
None,
|
| 626 |
+
"examples/motion.mp4",
|
| 627 |
+
],
|
| 628 |
+
],
|
| 629 |
+
inputs=[prompt, images[0], audio, video],
|
| 630 |
+
outputs=result,
|
| 631 |
+
fn=generate,
|
| 632 |
+
cache_examples=True,
|
| 633 |
+
cache_mode="lazy",
|
| 634 |
)
|
| 635 |
|
| 636 |
+
run.click(generate, request, result, api_name="generate")
|
| 637 |
+
|
| 638 |
|
| 639 |
if __name__ == "__main__":
|
| 640 |
demo.launch(show_error=True, theme=gr.themes.Citrus(), css=CSS)
|
examples/motion.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cb7e2f675639e86235afd8ead114b4bfbea19ee69719225b57c26fc90ff72363
|
| 3 |
+
size 762637
|
examples/subject.png
ADDED
|
examples/voice.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d041cbaa771ee3fa0295d09e79a08cd306579850d4a698ba2593cc9fa693647a
|
| 3 |
+
size 768044
|