multimodalart HF Staff commited on
Commit
0453841
·
verified ·
1 Parent(s): 9a03d09

Sync the split MiniMax-H3 Spaces

Browse files
Files changed (2) hide show
  1. README.md +36 -2
  2. app.py +65 -16
README.md CHANGED
@@ -31,11 +31,15 @@ reason alone. Cut the `MiniMaxH3Blocks` sequence at its `text_encoder` step and
31
  | Space | Subfolders | Download | Resident |
32
  |---|---|---|---|
33
  | [`minimax-h3-conditioner`](https://huggingface.co/spaces/diffusers-internal-dev/minimax-h3-conditioner) | `text_encoder/` + `tokenizer/` + `processor/` | 66.7 GB | 62.15 GiB bf16 |
34
- | this one | `transformer/` + `vae/` + `audio_vae/` | 77.3 GB | 61.73 GiB bf16 + ~20.5 GiB float32 |
35
 
36
  Besides the quality argument, unquantized weights are the ones AoTI can export; an NVFP4 checkpoint cannot be
37
  exported at all.
38
 
 
 
 
 
39
  ## How the split is expressed
40
 
41
  `MiniMaxH3Blocks` is a `SequentialPipelineBlocks` of eight steps:
@@ -71,17 +75,47 @@ before any GPU is attached, and these are plain bfloat16 tensors, so ZeroGPU's s
71
  torchao `Float8Tensor` that cannot be packed, and there is none here). The conditioner round trip is a network call
72
  on this Space's CPU. A `@spaces.GPU` call is therefore only the denoise loop and the two decoders.
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  ## Generation constraints
75
 
76
  Fixed by the checkpoint: 24 fps, a 768 pixel short edge, 5 to 15 s, `num_frames` snapped up to the next `17 * n + 5`,
77
  no CFG and no negative prompt (it is guidance-distilled, so every step is one forward pass).
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  ## Space variables
80
 
81
  | Variable | Default | Meaning |
82
  |---|---|---|
83
  | `H3_CONDITIONER` | `diffusers-internal-dev/minimax-h3-conditioner` | The Space this one asks for embeddings. |
84
- | `H3_PLACEMENT` | `resident` | `resident` keeps all 82.3 GiB of weights on the 95.0 GiB card; `offload` hands placement to `ComponentsManager.enable_auto_cpu_offload`. |
85
  | `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. |
86
  | `H3_GPU_DURATION` | `900` | Seconds per request; the pool applies a 1.5 duration factor. |
87
  | `H3_GPU_SIZE` | `xlarge` | ZeroGPU allocation size. `large` does not fit. |
 
31
  | Space | Subfolders | Download | Resident |
32
  |---|---|---|---|
33
  | [`minimax-h3-conditioner`](https://huggingface.co/spaces/diffusers-internal-dev/minimax-h3-conditioner) | `text_encoder/` + `tokenizer/` + `processor/` | 66.7 GB | 62.15 GiB bf16 |
34
+ | this one | `transformer/` + `vae/` + `audio_vae/` | 77.3 GB | 61.73 GiB bf16 + 10.43 GiB float32 |
35
 
36
  Besides the quality argument, unquantized weights are the ones AoTI can export; an NVFP4 checkpoint cannot be
37
  exported at all.
38
 
39
+ At 72.16 GiB of weights on a 95.0 GiB card there is no offloading in the request path at all, which is why an
40
+ *unquantized* MiniMax-H3 is also the fastest one measured here: **10.6 s/step** against the 19–21 s/step the 4 bit
41
+ Space pays, where the whole cost is the traffic auto-offload has to move.
42
+
43
  ## How the split is expressed
44
 
45
  `MiniMaxH3Blocks` is a `SequentialPipelineBlocks` of eight steps:
 
75
  torchao `Float8Tensor` that cannot be packed, and there is none here). The conditioner round trip is a network call
76
  on this Space's CPU. A `@spaces.GPU` call is therefore only the denoise loop and the two decoders.
77
 
78
+ ### The 150 GB quota, not the 95 GiB card, is what limits startup placement
79
+
80
+ `spaces`' startup `torch.pack()` writes every startup-resident CUDA tensor to a **second copy on disk**, and only
81
+ deletes the downloaded originals afterwards (`Cleaned 62.13GB of tensor files ... after packing`). Moving all
82
+ 77.3 GB onto the card therefore needs 154.6 GB at once, and the Space is evicted mid-pack:
83
+
84
+ ```
85
+ ZeroGPU tensors packing: 0%| | 0.00/77.3G
86
+ OSError: [Errno 28] No space left on device # os.posix_fallocate in spaces/zero/torch/packing.py
87
+ ```
88
+
89
+ So only the transformer is preplaced — a 66.3 GB pack — and `app.py` unlinks the downloaded shards itself *before*
90
+ `launch()` rather than after, which `.to("cuda")` has already made unreferenced. The autoencoders stay on the host
91
+ and cross PCIe on the first request, 10.43 GiB and a no-op on every request after it.
92
+
93
  ## Generation constraints
94
 
95
  Fixed by the checkpoint: 24 fps, a 768 pixel short edge, 5 to 15 s, `num_frames` snapped up to the next `17 * n + 5`,
96
  no CFG and no negative prompt (it is guidance-distilled, so every step is one forward pass).
97
 
98
+ ## Measured
99
+
100
+ An `rtx-pro-6000` Job — the same silicon as the ZeroGPU pool (RTX PRO 6000 Blackwell, sm120, 95.0 GiB) — running
101
+ exactly this blockset over the wire format, 1344x768, 124 frames, 30 steps, bfloat16, cuDNN attention, everything
102
+ resident:
103
+
104
+ | | |
105
+ |---|---|
106
+ | `load_components` (77.3 GB, warm Xet) | 43 s |
107
+ | `.to("cuda")` | 10 s |
108
+ | resident weights | 72.16 GiB |
109
+ | denoise + decode | 317 s, **10.58 s/step** |
110
+ | peak allocated / reserved | 78.54 / 85.37 GiB |
111
+ | output | h264 1344x768 @ 24 fps, 5.167 s + stereo AAC @ 32 kHz |
112
+
113
  ## Space variables
114
 
115
  | Variable | Default | Meaning |
116
  |---|---|---|
117
  | `H3_CONDITIONER` | `diffusers-internal-dev/minimax-h3-conditioner` | The Space this one asks for embeddings. |
118
+ | `H3_PLACEMENT` | `preplace` | `preplace` puts the transformer on the card at startup and the autoencoders on the first request; `offload` hands placement to `ComponentsManager.enable_auto_cpu_offload`. |
119
  | `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. |
120
  | `H3_GPU_DURATION` | `900` | Seconds per request; the pool applies a 1.5 duration factor. |
121
  | `H3_GPU_SIZE` | `xlarge` | ZeroGPU allocation size. `large` does not fit. |
app.py CHANGED
@@ -30,9 +30,11 @@ import gradio as gr
30
 
31
  MODEL_REPO = os.environ.get("H3_MODEL_REPO", "diffusers-internal-dev/MiniMax-H3")
32
  CONDITIONER_SPACE = os.environ.get("H3_CONDITIONER", "diffusers-internal-dev/minimax-h3-conditioner")
33
- # `resident` keeps the 61.73 GiB transformer and the ~20.5 GiB of float32 VAEs on the card at once (82.3 of 95.0 GiB,
34
- # leaving ~12.7 GiB for activations); `offload` hands placement to `ComponentsManager.enable_auto_cpu_offload`.
35
- PLACEMENT = os.environ.get("H3_PLACEMENT", "resident").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()
@@ -113,10 +115,12 @@ def load_models() -> str | None:
113
  pipe.load_components(dtype=torch.bfloat16, token=token)
114
  pipe.transformer.set_attention_backend(ATTENTION)
115
 
116
- if PLACEMENT == "resident":
117
  # Plain bfloat16 tensors, so ZeroGPU's startup packing handles them — the thing that cannot be moved at
118
- # startup is a torchao `Float8Tensor`, whose `aten.empty_like(pin_memory=True)` is unimplemented.
119
- pipe.to("cuda")
 
 
120
  else:
121
  manager.enable_auto_cpu_offload(device="cuda")
122
  _arm_decode_hooks(pipe)
@@ -130,6 +134,46 @@ def load_models() -> str | None:
130
  return LOAD_ERROR
131
 
132
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  def _arm_decode_hooks(pipe):
134
  """Make the offload hooks fire for the two VAEs.
135
 
@@ -180,10 +224,20 @@ def encode_remote(prompt, image_path, last_image_path, canvas, num_frames):
180
 
181
  @spaces.GPU(duration=GPU_DURATION, size=GPU_SIZE)
182
  def _generate(prompt_embeds, text_token_tags, image, last_image, height, width, num_frames, steps, seed):
183
- """The only thing on GPU time: the packed-sequence denoise loop and the two decoders."""
 
 
 
 
184
  import torch
185
 
186
- return PIPE(
 
 
 
 
 
 
187
  prompt_embeds=prompt_embeds.to("cuda"),
188
  text_token_tags=text_token_tags,
189
  image=image,
@@ -194,6 +248,7 @@ def _generate(prompt_embeds, text_token_tags, image, last_image, height, width,
194
  num_inference_steps=int(steps),
195
  generator=torch.Generator("cpu").manual_seed(int(seed)),
196
  )
 
197
 
198
 
199
  def generate(prompt, image_path, last_image_path, canvas, duration, steps, seed, progress=gr.Progress()):
@@ -220,7 +275,7 @@ def generate(prompt, image_path, last_image_path, canvas, duration, steps, seed,
220
 
221
  progress(0.1, desc=f"Denoising {steps} steps at {width}x{height}, {num_frames} frames ...")
222
  started = time.time()
223
- state = _generate(
224
  prompt_embeds,
225
  text_token_tags,
226
  Image.open(image_path) if image_path else None,
@@ -236,13 +291,7 @@ def generate(prompt, image_path, last_image_path, canvas, duration, steps, seed,
236
  directory = os.path.join(tempfile.gettempdir(), "h3-outputs")
237
  os.makedirs(directory, exist_ok=True)
238
  path = os.path.join(directory, f"h3-{int(time.time() * 1000)}.mp4")
239
- encode_video(
240
- state.get("videos")[0],
241
- fps=FPS,
242
- output_path=path,
243
- audio=state.get("audio")[0],
244
- audio_sample_rate=state.get("sampling_rate"),
245
- )
246
 
247
  report = (
248
  f"`{width}x{height}`, {num_frames} frames ({num_frames / FPS:.3f} s), {int(steps)} steps · "
 
30
 
31
  MODEL_REPO = os.environ.get("H3_MODEL_REPO", "diffusers-internal-dev/MiniMax-H3")
32
  CONDITIONER_SPACE = os.environ.get("H3_CONDITIONER", "diffusers-internal-dev/minimax-h3-conditioner")
33
+ # `preplace` moves the 61.73 GiB transformer onto the card at startup and leaves the 10.43 GiB of float32
34
+ # autoencoders for the first GPU call; `offload` hands the whole thing to
35
+ # `ComponentsManager.enable_auto_cpu_offload` instead. See `_drop_cached_weight_blobs` for why the *transformer
36
+ # alone* rather than everything: the 150 GB storage quota, not the 95 GiB card, is the binding constraint at startup.
37
+ PLACEMENT = os.environ.get("H3_PLACEMENT", "preplace").lower()
38
  # cuDNN's fused attention is 10-20% faster than the SDPA default on this pool and needs nothing installed.
39
  # flash-attention 3 is sm90-only and this card is sm120 (the `zero-a10g` flavour name is legacy).
40
  ATTENTION = os.environ.get("H3_ATTENTION", "_native_cudnn").lower()
 
115
  pipe.load_components(dtype=torch.bfloat16, token=token)
116
  pipe.transformer.set_attention_backend(ATTENTION)
117
 
118
+ if PLACEMENT == "preplace":
119
  # Plain bfloat16 tensors, so ZeroGPU's startup packing handles them — the thing that cannot be moved at
120
+ # startup is a torchao `Float8Tensor`, whose `aten.empty_like(pin_memory=True)` is unimplemented. The
121
+ # autoencoders deliberately stay behind; `_generate` moves them on the first request.
122
+ pipe.transformer.to("cuda")
123
+ _log_disk(f"freed {_drop_cached_weight_blobs():.1f} GiB of downloaded weights")
124
  else:
125
  manager.enable_auto_cpu_offload(device="cuda")
126
  _arm_decode_hooks(pipe)
 
134
  return LOAD_ERROR
135
 
136
 
137
+ def _log_disk(label: str) -> None:
138
+ import shutil
139
+
140
+ from huggingface_hub.constants import HF_HUB_CACHE
141
+
142
+ probe = HF_HUB_CACHE if os.path.isdir(HF_HUB_CACHE) else os.path.expanduser("~")
143
+ _total, used, free = shutil.disk_usage(probe)
144
+ print(f"[gen] {label} · disk {used / 2**30:.1f} used, {free / 2**30:.1f} free", flush=True)
145
+
146
+
147
+ def _drop_cached_weight_blobs() -> float:
148
+ """Delete the downloaded weight files once their tensors are copies in (fake-)CUDA memory.
149
+
150
+ A ZeroGPU Space is evicted at **150 GB of storage**, and `spaces`' startup `torch.pack()` writes every
151
+ startup-resident CUDA tensor to a *second* copy on disk before deleting the originals itself. This half
152
+ downloads 77.3 GB, so packing all of it needs 154.6 GB and the Space dies with `OSError: [Errno 28] No space
153
+ left on device` in `os.posix_fallocate` — which is exactly how its first bring-up died.
154
+
155
+ Two things keep it under the cap. Only the transformer is moved onto the card, so the pack is 66.3 GB rather
156
+ than 77.3 GB; and its shards are dropped here, before `launch()`, rather than after the pack. `.to("cuda")`
157
+ copies each parameter out of the memory-mapped shard, so those blobs are unreferenced by then and unlinking
158
+ them frees the space for real. The autoencoders are still mapped, so their ~11 GB survives the unlink — which
159
+ is fine, 66.3 + 11 is nowhere near the cap, and it is the arithmetic this Space is designed around:
160
+
161
+ worst case, nothing frees 77.3 (download) + 66.3 (pack) = 143.6 GB
162
+ expected 11 (still-mapped VAEs) + 66.3 (pack) = 77.3 GB
163
+ """
164
+ import glob
165
+
166
+ from huggingface_hub.constants import HF_HUB_CACHE
167
+
168
+ blobs = os.path.join(HF_HUB_CACHE, "models--" + MODEL_REPO.replace("/", "--"), "blobs", "*")
169
+ freed = 0
170
+ for blob in glob.glob(blobs):
171
+ size = os.path.getsize(blob)
172
+ os.unlink(blob)
173
+ freed += size
174
+ return freed / 2**30
175
+
176
+
177
  def _arm_decode_hooks(pipe):
178
  """Make the offload hooks fire for the two VAEs.
179
 
 
224
 
225
  @spaces.GPU(duration=GPU_DURATION, size=GPU_SIZE)
226
  def _generate(prompt_embeds, text_token_tags, image, last_image, height, width, num_frames, steps, seed):
227
+ """The only thing on GPU time: the packed-sequence denoise loop and the two decoders.
228
+
229
+ Only the three generated outputs come back. A `@spaces.GPU` return crosses a process boundary by pickling, and
230
+ the full `PipelineState` still holds the packed latents, the rotary grid and the row indices on the card.
231
+ """
232
  import torch
233
 
234
+ if PLACEMENT == "preplace":
235
+ # The two autoencoders were left on the host so that ZeroGPU's startup pack stayed inside the storage quota;
236
+ # 10.43 GiB across PCIe on the first request, and a no-op walk on every one after it.
237
+ PIPE.vae.to("cuda")
238
+ PIPE.audio_vae.to("cuda")
239
+
240
+ state = PIPE(
241
  prompt_embeds=prompt_embeds.to("cuda"),
242
  text_token_tags=text_token_tags,
243
  image=image,
 
248
  num_inference_steps=int(steps),
249
  generator=torch.Generator("cpu").manual_seed(int(seed)),
250
  )
251
+ return state.get("videos")[0], state.get("audio")[0].cpu(), state.get("sampling_rate")
252
 
253
 
254
  def generate(prompt, image_path, last_image_path, canvas, duration, steps, seed, progress=gr.Progress()):
 
275
 
276
  progress(0.1, desc=f"Denoising {steps} steps at {width}x{height}, {num_frames} frames ...")
277
  started = time.time()
278
+ frames, audio, sampling_rate = _generate(
279
  prompt_embeds,
280
  text_token_tags,
281
  Image.open(image_path) if image_path else None,
 
291
  directory = os.path.join(tempfile.gettempdir(), "h3-outputs")
292
  os.makedirs(directory, exist_ok=True)
293
  path = os.path.join(directory, f"h3-{int(time.time() * 1000)}.mp4")
294
+ encode_video(frames, fps=FPS, output_path=path, audio=audio, audio_sample_rate=sampling_rate)
 
 
 
 
 
 
295
 
296
  report = (
297
  f"`{width}x{height}`, {num_frames} frames ({num_frames / FPS:.3f} s), {int(steps)} steps · "