someone-in-the-world Claude Sonnet 5 commited on
Commit
e809108
·
1 Parent(s): 926cf86

Quantize transformer to NF4 via bitsandbytes to fix root-cause OOM

Browse files

The 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>

Files changed (2) hide show
  1. app.py +33 -21
  2. requirements.txt +1 -0
app.py CHANGED
@@ -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. On a
408
- # generously-sized allocation, move the full ~37GB pipeline to GPU in one
409
- # shot (fast); but the 47GB 2g.48gb MIG slice this Space actually gets
410
- # measured a peak of 46.82GB and still OOM'd on that path, wasting ~40s
411
- # before falling back so only attempt it when there's real headroom
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
- if _cuda_ok and p.total_memory / 1024**3 >= _FAST_PATH_MIN_GB:
416
- try:
417
- pipe.to(device)
418
- print(f"[infer] moved full pipe to {device} — t={time.perf_counter()-t0:.1f}s")
419
- except torch.cuda.OutOfMemoryError:
420
- print(f"[infer] OOM moving full pipe to {device}, falling back to cpu offload")
421
- pipe.to("cpu")
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} (slice too small for fast path)")
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}")
requirements.txt CHANGED
@@ -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