multimodalart HF Staff commited on
Commit
f03920f
·
verified ·
1 Parent(s): ced31c4

Dynamic GPU duration from the packed sequence, cached examples, torchaudio note

Browse files
Files changed (6) hide show
  1. .gitattributes +2 -0
  2. README.md +10 -0
  3. app.py +144 -9
  4. examples/motion.mp4 +3 -0
  5. examples/subject.png +0 -0
  6. 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=GPU_DURATION, size=GPU_SIZE)
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
- run.click(
497
- generate,
498
- [prompt, *images, audio, video, canvas, match, duration, steps, seed],
499
- result,
500
- api_name="generate",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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