Spaces:
Running on Zero
Running on Zero
Sync the split MiniMax-H3 Spaces
Browse files
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 |
-
|
| 74 |
-
|
| 75 |
-
|
| 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
|
| 79 |
|
| 80 |
-
`spaces`' startup `torch.pack()` writes every
|
| 81 |
-
|
| 82 |
-
|
|
|
|
| 83 |
|
| 84 |
```
|
| 85 |
ZeroGPU tensors packing: 0%| | 0.00/77.3G
|
| 86 |
-
OSError: [Errno 28] No space left on device # os.posix_fallocate
|
| 87 |
```
|
| 88 |
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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` | `
|
| 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 |
-
# `
|
| 34 |
-
#
|
| 35 |
-
#
|
| 36 |
-
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 == "
|
| 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 == "
|
| 235 |
-
#
|
| 236 |
-
#
|
| 237 |
-
PIPE.
|
| 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"),
|