Spaces:
Running on Zero
Quantize transformer to NF4 via bitsandbytes to fix root-cause OOM
Browse filesThe last several commits kept rearranging when the ~54GB bf16 pipeline
(20B transformer + Qwen2.5-VL-7B text encoder) touches the GPU, but that
footprint never comfortably fit the 47GB 2g.48gb MIG slice regardless of
ordering. NF4-quantizing the transformer (the biggest contributor) drops
it from ~40GB to ~11GB, bringing full-pipeline GPU residency down to
~25-30GB with real headroom, so the fast pipe.to(device) path should now
succeed every time instead of needing the OOM/cpu-offload dance.
bitsandbytes rejects device_map="cpu" for a fresh (non-prequantized)
config, so the transformer now loads straight onto cuda:0 at startup
(paying the "ZeroGPU disk packing" cost once at boot) instead of the
CPU-first pattern still used for the rest of the pipeline. Removed the
now-obsolete _FAST_PATH_MIN_GB size gate in _infer_gpu since the
quantized pipeline should fit regardless of the exact slice size.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- app.py +33 -21
- requirements.txt +1 -0
|
@@ -72,7 +72,7 @@ print("[startup] TF32 enabled", flush=True)
|
|
| 72 |
print("[startup] importing dimensions...", flush=True)
|
| 73 |
from dimensions import compute_output_dimensions, max_dim_for_mode
|
| 74 |
print("[startup] importing diffusers...", flush=True)
|
| 75 |
-
from diffusers import FlowMatchEulerDiscreteScheduler
|
| 76 |
print("[startup] importing QwenImageEditPlusPipeline...", flush=True)
|
| 77 |
from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
|
| 78 |
print("[startup] importing QwenImageTransformer2DModel...", flush=True)
|
|
@@ -97,13 +97,31 @@ def _start_heartbeat(label: str) -> threading.Event:
|
|
| 97 |
_t0_load = time.perf_counter()
|
| 98 |
print("[startup] loading transformer from_pretrained (prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V23)...", flush=True)
|
| 99 |
_hb = _start_heartbeat("transformer")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
_transformer = QwenImageTransformer2DModel.from_pretrained(
|
| 101 |
"prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V23",
|
| 102 |
torch_dtype=dtype,
|
| 103 |
-
device_map="cpu",
|
|
|
|
| 104 |
)
|
| 105 |
_hb.set()
|
| 106 |
print(f"[startup] transformer loaded in {time.perf_counter()-_t0_load:.1f}s", flush=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
_t1_load = time.perf_counter()
|
| 109 |
print("[startup] loading pipeline from_pretrained (FireRedTeam/FireRed-Image-Edit-1.1)...", flush=True)
|
|
@@ -404,27 +422,21 @@ def _infer_gpu(pil_images, prompt, seed, guidance_scale, steps, width, height, m
|
|
| 404 |
|
| 405 |
# Each ZeroGPU call runs in a fresh worker (hooks are always unset here),
|
| 406 |
# so cpu_offload buys no cross-call reuse — it only trades one bulk
|
| 407 |
-
# to(device) transfer for several slower hook-managed ones.
|
| 408 |
-
#
|
| 409 |
-
#
|
| 410 |
-
#
|
| 411 |
-
#
|
| 412 |
-
# above that, otherwise go straight to enable_model_cpu_offload.
|
| 413 |
-
_FAST_PATH_MIN_GB = 60
|
| 414 |
if getattr(pipe.transformer, "_hf_hook", None) is None:
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
torch.cuda.empty_cache()
|
| 423 |
-
pipe.enable_model_cpu_offload(device=device)
|
| 424 |
-
print(f"[infer] enabled cpu offload on {device} (fallback)")
|
| 425 |
-
else:
|
| 426 |
pipe.enable_model_cpu_offload(device=device)
|
| 427 |
-
print(f"[infer] enabled cpu offload on {device} (
|
| 428 |
print(f"[infer] {_gpu_mem_str(_cuda_ok)} — t={time.perf_counter()-t0:.1f}s")
|
| 429 |
|
| 430 |
print(f"[infer] {len(pil_images)} image(s) pre-decoded, output={width}x{height}, seed={seed}")
|
|
|
|
| 72 |
print("[startup] importing dimensions...", flush=True)
|
| 73 |
from dimensions import compute_output_dimensions, max_dim_for_mode
|
| 74 |
print("[startup] importing diffusers...", flush=True)
|
| 75 |
+
from diffusers import BitsAndBytesConfig, FlowMatchEulerDiscreteScheduler
|
| 76 |
print("[startup] importing QwenImageEditPlusPipeline...", flush=True)
|
| 77 |
from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
|
| 78 |
print("[startup] importing QwenImageTransformer2DModel...", flush=True)
|
|
|
|
| 97 |
_t0_load = time.perf_counter()
|
| 98 |
print("[startup] loading transformer from_pretrained (prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V23)...", flush=True)
|
| 99 |
_hb = _start_heartbeat("transformer")
|
| 100 |
+
# NF4-quantize the transformer (~20B params, ~40GB in bf16) to fit the pipeline
|
| 101 |
+
# comfortably inside this Space's 47GB MIG slice. bitsandbytes only quantizes
|
| 102 |
+
# on an actual CUDA device (it rejects device_map="cpu"/"disk" for a fresh,
|
| 103 |
+
# not-yet-quantized config), so this loads straight onto cuda:0 instead of the
|
| 104 |
+
# CPU-first pattern used below for the rest of the pipeline. That pays the
|
| 105 |
+
# once-per-boot "ZeroGPU disk packing" cost the CPU-first load was added to
|
| 106 |
+
# avoid, but only once at startup rather than never.
|
| 107 |
+
_quant_config = BitsAndBytesConfig(
|
| 108 |
+
load_in_4bit=True,
|
| 109 |
+
bnb_4bit_quant_type="nf4",
|
| 110 |
+
bnb_4bit_compute_dtype=dtype,
|
| 111 |
+
bnb_4bit_use_double_quant=True,
|
| 112 |
+
) if torch.cuda.is_available() else None
|
| 113 |
_transformer = QwenImageTransformer2DModel.from_pretrained(
|
| 114 |
"prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V23",
|
| 115 |
torch_dtype=dtype,
|
| 116 |
+
device_map={"": 0} if _quant_config else "cpu",
|
| 117 |
+
quantization_config=_quant_config,
|
| 118 |
)
|
| 119 |
_hb.set()
|
| 120 |
print(f"[startup] transformer loaded in {time.perf_counter()-_t0_load:.1f}s", flush=True)
|
| 121 |
+
try:
|
| 122 |
+
print(f"[startup] transformer memory footprint: {_transformer.get_memory_footprint()/1024**3:.2f}GB", flush=True)
|
| 123 |
+
except Exception as e:
|
| 124 |
+
print(f"[startup] transformer memory footprint: unavailable ({e})", flush=True)
|
| 125 |
|
| 126 |
_t1_load = time.perf_counter()
|
| 127 |
print("[startup] loading pipeline from_pretrained (FireRedTeam/FireRed-Image-Edit-1.1)...", flush=True)
|
|
|
|
| 422 |
|
| 423 |
# Each ZeroGPU call runs in a fresh worker (hooks are always unset here),
|
| 424 |
# so cpu_offload buys no cross-call reuse — it only trades one bulk
|
| 425 |
+
# to(device) transfer for several slower hook-managed ones. The
|
| 426 |
+
# transformer is now NF4-quantized (~40GB bf16 -> ~11GB), so the full
|
| 427 |
+
# pipeline (~25-30GB total) should comfortably fit even on the 47GB
|
| 428 |
+
# 2g.48gb MIG slice this Space actually gets — always attempt full-GPU
|
| 429 |
+
# residency first and only fall back to offload on an actual OOM.
|
|
|
|
|
|
|
| 430 |
if getattr(pipe.transformer, "_hf_hook", None) is None:
|
| 431 |
+
try:
|
| 432 |
+
pipe.to(device)
|
| 433 |
+
print(f"[infer] moved full pipe to {device} — t={time.perf_counter()-t0:.1f}s")
|
| 434 |
+
except torch.cuda.OutOfMemoryError:
|
| 435 |
+
print(f"[infer] OOM moving full pipe to {device}, falling back to cpu offload")
|
| 436 |
+
pipe.to("cpu")
|
| 437 |
+
torch.cuda.empty_cache()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
pipe.enable_model_cpu_offload(device=device)
|
| 439 |
+
print(f"[infer] enabled cpu offload on {device} (fallback)")
|
| 440 |
print(f"[infer] {_gpu_mem_str(_cuda_ok)} — t={time.perf_counter()-t0:.1f}s")
|
| 441 |
|
| 442 |
print(f"[infer] {len(pil_images)} image(s) pre-decoded, output={width}x{height}, seed={seed}")
|
|
@@ -2,6 +2,7 @@ git+https://github.com/huggingface/accelerate.git
|
|
| 2 |
git+https://github.com/huggingface/diffusers.git
|
| 3 |
git+https://github.com/huggingface/peft.git
|
| 4 |
transformers==4.57.1
|
|
|
|
| 5 |
huggingface_hub
|
| 6 |
pyarrow
|
| 7 |
sentencepiece
|
|
|
|
| 2 |
git+https://github.com/huggingface/diffusers.git
|
| 3 |
git+https://github.com/huggingface/peft.git
|
| 4 |
transformers==4.57.1
|
| 5 |
+
bitsandbytes
|
| 6 |
huggingface_hub
|
| 7 |
pyarrow
|
| 8 |
sentencepiece
|