multimodalart HF Staff commited on
Commit
5ca1dce
·
verified ·
1 Parent(s): 0453841

Sync the split MiniMax-H3 Spaces

Browse files
Files changed (2) hide show
  1. README.md +18 -14
  2. app.py +20 -59
README.md CHANGED
@@ -70,25 +70,29 @@ it resolved so this Space pins the same canvas rather than re-deriving it.
70
 
71
  ## Nothing is paid for with GPU time
72
 
73
- Download, load and the move onto the card happen at **startup**: `import spaces` at module top patches `torch.cuda`
74
- before any GPU is attached, and these are plain bfloat16 tensors, so ZeroGPU's startup packing handles them (it is a
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
 
@@ -104,7 +108,7 @@ resident:
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 |
@@ -115,7 +119,7 @@ resident:
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. |
 
70
 
71
  ## Nothing is paid for with GPU time
72
 
73
+ The 77.3 GB download and the load happen at **startup**: `import spaces` at module top patches `torch.cuda` before
74
+ any GPU is attached, so nothing about the load needs a card. The conditioner round trip is a network call on this
75
+ Space's CPU. A `@spaces.GPU` call is therefore only the placement (once) and the denoise loop and the two decoders.
 
76
 
77
+ ### The 150 GB quota, not the 95 GiB card, is what rules out startup placement
78
 
79
+ One thing does *not* happen at startup: the move onto the card. `spaces`' startup `torch.pack()` writes every
80
+ startup-resident CUDA tensor to a **second copy on disk** and only deletes the downloaded originals afterwards
81
+ (`Cleaned 62.13GB of tensor files ... after packing`, which is what keeps the conditioner half comfortable at
82
+ 66.7 GB). Packing 77.3 GB needs 154.6 GB at once, and this 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, spaces/zero/torch/packing.py
87
  ```
88
 
89
+ Unlinking the shards first does not rescue it. `.to("cuda")` under the startup patch does not release the
90
+ memory-mapped safetensors, so nothing is freed and the pack's own cleanup walks those still-open mappings and
91
+ `lstat`s them, so a deleted blob becomes `FileNotFoundError: .../blobs/3d449... (deleted)`.
92
+
93
+ Placement therefore happens on the **first GPU call**, `PIPE.to("cuda")` at the top of the `@spaces.GPU` function:
94
+ about 10 s of PCIe once, then a no-op walk, and the denoise loop runs with everything resident and no offloading at
95
+ all. It is the same trick the 4 bit Space uses, for the same reason.
96
 
97
  ## Generation constraints
98
 
 
108
  | | |
109
  |---|---|
110
  | `load_components` (77.3 GB, warm Xet) | 43 s |
111
+ | `.to("cuda")`, once | 10 s |
112
  | resident weights | 72.16 GiB |
113
  | denoise + decode | 317 s, **10.58 s/step** |
114
  | peak allocated / reserved | 78.54 / 85.37 GiB |
 
119
  | Variable | Default | Meaning |
120
  |---|---|---|
121
  | `H3_CONDITIONER` | `diffusers-internal-dev/minimax-h3-conditioner` | The Space this one asks for embeddings. |
122
+ | `H3_PLACEMENT` | `lazy` | `lazy` moves all 72.16 GiB onto the card on the first GPU call and leaves it there; `offload` hands placement to `ComponentsManager.enable_auto_cpu_offload` instead. |
123
  | `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. |
124
  | `H3_GPU_DURATION` | `900` | Seconds per request; the pool applies a 1.5 duration factor. |
125
  | `H3_GPU_SIZE` | `xlarge` | ZeroGPU allocation size. `large` does not fit. |
app.py CHANGED
@@ -30,11 +30,10 @@ 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
- # `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()
@@ -82,14 +81,23 @@ def status() -> str:
82
 
83
 
84
  def load_models() -> str | None:
85
- """Load the denoising half. At **startup**.
86
 
87
  `MiniMaxH3GeneratorBlocks` declares `transformer`, `vae`, `audio_vae`, `scheduler`, `audio_scheduler` and
88
  `video_processor`, so `load_components` fetches exactly those subfolders out of the shared
89
  `modular_model_index.json` — `text_encoder/` and `transformer_ref/` are never touched.
90
 
91
  Both autoencoders carry `_keep_in_fp32_modules` over every module, so the `dtype` below is refused for them and
92
- they load float32 (~20.5 GiB rather than 10.26): a bfloat16 audio VAE decodes the soundtrack ~20 dB too quiet.
 
 
 
 
 
 
 
 
 
93
  """
94
  global PIPE, MANAGER, LOAD_ERROR, LOADED_IN
95
 
@@ -115,13 +123,7 @@ def load_models() -> str | None:
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)
127
 
@@ -134,46 +136,6 @@ def load_models() -> str | None:
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
 
@@ -231,11 +193,10 @@ def _generate(prompt_embeds, text_token_tags, image, last_image, height, width,
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"),
 
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
+ # `lazy` moves all 72.16 GiB onto the card on the first GPU call and leaves it there; `offload` hands placement to
34
+ # `ComponentsManager.enable_auto_cpu_offload` instead. Neither puts anything on the card at *startup*, which is
35
+ # deliberate see `load_models`: the 150 GB storage quota, not the 95 GiB card, is what rules that out here.
36
+ PLACEMENT = os.environ.get("H3_PLACEMENT", "lazy").lower()
 
37
  # cuDNN's fused attention is 10-20% faster than the SDPA default on this pool and needs nothing installed.
38
  # flash-attention 3 is sm90-only and this card is sm120 (the `zero-a10g` flavour name is legacy).
39
  ATTENTION = os.environ.get("H3_ATTENTION", "_native_cudnn").lower()
 
81
 
82
 
83
  def load_models() -> str | None:
84
+ """Load the denoising half. At **startup**, but *not* onto the card.
85
 
86
  `MiniMaxH3GeneratorBlocks` declares `transformer`, `vae`, `audio_vae`, `scheduler`, `audio_scheduler` and
87
  `video_processor`, so `load_components` fetches exactly those subfolders out of the shared
88
  `modular_model_index.json` — `text_encoder/` and `transformer_ref/` are never touched.
89
 
90
  Both autoencoders carry `_keep_in_fp32_modules` over every module, so the `dtype` below is refused for them and
91
+ they stay float32: a bfloat16 audio VAE decodes the soundtrack roughly 20 dB too quiet.
92
+
93
+ Nothing is moved onto the card here, which is the one place this Space departs from the ZeroGPU idiom, and the
94
+ reason is storage rather than memory. `spaces`' startup `torch.pack()` writes every startup-resident CUDA tensor
95
+ to a **second copy on disk** and only deletes the downloaded originals afterwards; 77.3 GB of weights plus a
96
+ 77.3 GB pack is 154.6 GB against a 150 GB quota, and the Space is evicted mid-pack with `OSError: [Errno 28] No
97
+ space left on device` out of `os.posix_fallocate`. Deleting the shards first does not help either: the pack's own
98
+ cleanup walks the still-open mappings and `lstat`s them, so an unlinked blob turns into `FileNotFoundError:
99
+ ... (deleted)`. Placement therefore happens on the first GPU call, where it costs about 10 s of PCIe and then
100
+ persists across every later request in the same worker.
101
  """
102
  global PIPE, MANAGER, LOAD_ERROR, LOADED_IN
103
 
 
123
  pipe.load_components(dtype=torch.bfloat16, token=token)
124
  pipe.transformer.set_attention_backend(ATTENTION)
125
 
126
+ if PLACEMENT == "offload":
 
 
 
 
 
 
127
  manager.enable_auto_cpu_offload(device="cuda")
128
  _arm_decode_hooks(pipe)
129
 
 
136
  return LOAD_ERROR
137
 
138
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  def _arm_decode_hooks(pipe):
140
  """Make the offload hooks fire for the two VAEs.
141
 
 
193
  """
194
  import torch
195
 
196
+ if PLACEMENT == "lazy":
197
+ # 72.16 GiB across PCIe on the first request of a worker, a no-op walk on every one after it. Startup
198
+ # placement is not an option here see `load_models` — and this is what buys the offload-free denoise loop.
199
+ PIPE.to("cuda")
 
200
 
201
  state = PIPE(
202
  prompt_embeds=prompt_embeds.to("cuda"),