someone-in-the-world Claude Sonnet 5 commited on
Commit
8db56cb
·
1 Parent(s): d9d1b60

Keep transformer weights fp8-resident instead of upcasting at load

Browse files

The Rapid-AIO-V23 checkpoint already ships natively in fp8_e4m3fn
(~19GB for 20B params), but from_pretrained's torch_dtype=bf16 was
casting every tensor up to bf16 at load time (~38GB), which is why the
pipeline (~35-40GB transformer + ~14GB text encoder) never comfortably
fit this Space's 47GB MIG slice and needed the slow enable_model_cpu_offload
fallback (_FAST_PATH_MIN_GB=60, gating out the 47GB slice entirely).

This keeps torch_dtype=torch.float8_e4m3fn on the transformer load (a
diffusers-supported path — see modeling_utils.py's explicit float8
handling) so weights stay fp8 in memory, and patches every fp8-resident
nn.Linear to upcast its own weight to the input's dtype just before the
matmul. nn.Linear has no fp8 GEMM kernel on this GPU, so this is not a
compute speedup — it's the same bf16 upcast the old code did, just
deferred from load-time (all layers at once) to call-time (one layer's
weight transiently bf16 at a time), so resident GPU memory drops from
~38GB to ~19GB for the transformer with no change to the actual math or
output. That should bring full-pipeline residency down to ~35GB,
comfortably under the 47GB slice, so _FAST_PATH_MIN_GB is lowered from
60 to 40 to let this Space take the fast pipe.to(device) path instead
of falling back to cpu offload. The OOM->offload fallback stays in
place as a safety net in case the estimate is off.

Needs real-GPU verification on the Space before merging to main — no
local GPU available to test weight-loading/placement changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Files changed (1) hide show
  1. app.py +41 -8
app.py CHANGED
@@ -3,6 +3,7 @@ import gc
3
  import time
4
  import threading
5
  import traceback
 
6
 
7
  # cudaMallocAsync bypasses NVML memory queries that fail on MIG GPU instances
8
  os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "backend:cudaMallocAsync")
@@ -94,16 +95,46 @@ def _start_heartbeat(label: str) -> threading.Event:
94
  return done
95
 
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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,13 +435,15 @@ def _log_gpu_properties(cuda_ok):
404
 
405
  # Each ZeroGPU call runs in a fresh worker (hooks are always unset here), so
406
  # cpu_offload buys no cross-call reuse — it only trades one bulk to(device)
407
- # transfer for several slower hook-managed ones. On a generously-sized
408
- # allocation, move the full ~37GB pipeline to GPU in one shot (fast); but the
409
- # 47GB 2g.48gb MIG slice this Space actually gets measured a peak of 46.82GB
410
- # and still OOM'd on that path, wasting ~40s before falling back so only
411
- # attempt it when there's real headroom above that, otherwise go straight to
412
- # enable_model_cpu_offload.
413
- _FAST_PATH_MIN_GB = 60
 
 
414
 
415
 
416
  def _place_pipe_on_device(cuda_ok, gpu_props, t0):
 
3
  import time
4
  import threading
5
  import traceback
6
+ import types
7
 
8
  # cudaMallocAsync bypasses NVML memory queries that fail on MIG GPU instances
9
  os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "backend:cudaMallocAsync")
 
95
  return done
96
 
97
 
98
+ _FP8_DTYPES = (torch.float8_e4m3fn, torch.float8_e5m2)
99
+
100
+
101
+ def _fp8_upcast_linear_forward(self, input):
102
+ weight = self.weight.to(input.dtype) if self.weight.dtype in _FP8_DTYPES else self.weight
103
+ bias = self.bias.to(input.dtype) if (self.bias is not None and self.bias.dtype in _FP8_DTYPES) else self.bias
104
+ return torch.nn.functional.linear(input, weight, bias)
105
+
106
+
107
+ def _patch_fp8_linears(model) -> int:
108
+ # This checkpoint ships its weights natively in fp8 (torch_dtype below preserves that
109
+ # instead of upcasting to bf16 at load time, halving resident memory: ~19GB vs ~38GB).
110
+ # nn.Linear has no fp8 compute kernel on this GPU, so each patched layer upcasts its own
111
+ # weight to the input's dtype just-in-time for the matmul — mathematically identical to
112
+ # the old load-time-upcast-everything approach (same values, same target dtype), just
113
+ # deferred so only one layer's weight is transiently bf16 at a time instead of all of them.
114
+ count = 0
115
+ for module in model.modules():
116
+ if isinstance(module, torch.nn.Linear) and module.weight.dtype in _FP8_DTYPES:
117
+ module.forward = types.MethodType(_fp8_upcast_linear_forward, module)
118
+ count += 1
119
+ return count
120
+
121
+
122
  _t0_load = time.perf_counter()
123
  print("[startup] loading transformer from_pretrained (prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V23)...", flush=True)
124
  _hb = _start_heartbeat("transformer")
125
  _transformer = QwenImageTransformer2DModel.from_pretrained(
126
  "prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V23",
127
+ torch_dtype=torch.float8_e4m3fn,
128
  device_map="cpu",
129
  )
130
  _hb.set()
131
  print(f"[startup] transformer loaded in {time.perf_counter()-_t0_load:.1f}s", flush=True)
132
+ _n_fp8_patched = _patch_fp8_linears(_transformer)
133
+ print(f"[startup] patched {_n_fp8_patched} fp8-resident nn.Linear modules for just-in-time upcast", flush=True)
134
+ try:
135
+ print(f"[startup] transformer memory footprint: {_transformer.get_memory_footprint()/1024**3:.2f}GB", flush=True)
136
+ except Exception as e:
137
+ print(f"[startup] transformer memory footprint: unavailable ({e})", flush=True)
138
 
139
  _t1_load = time.perf_counter()
140
  print("[startup] loading pipeline from_pretrained (FireRedTeam/FireRed-Image-Edit-1.1)...", flush=True)
 
435
 
436
  # Each ZeroGPU call runs in a fresh worker (hooks are always unset here), so
437
  # cpu_offload buys no cross-call reuse — it only trades one bulk to(device)
438
+ # transfer for several slower hook-managed ones. The previous bf16-everywhere
439
+ # pipeline (~40GB transformer + ~14GB text encoder) peaked at 46.82GB moving
440
+ # onto this Space's 47GB 2g.48gb MIG slice and still OOM'd, wasting ~40s
441
+ # before falling back. The transformer now stays fp8-resident (see
442
+ # _patch_fp8_linears above, ~19GB instead of ~38GB), so the full pipeline
443
+ # should total roughly ~35GB — comfortably under the slice with headroom to
444
+ # spare. Lowered accordingly, but the OOM fallback below stays as a safety
445
+ # net in case that estimate is off.
446
+ _FAST_PATH_MIN_GB = 40
447
 
448
 
449
  def _place_pipe_on_device(cuda_ok, gpu_props, t0):