Spaces:
Running on Zero
Keep transformer weights fp8-resident instead of upcasting at load
Browse filesThe 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>
|
@@ -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=
|
| 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.
|
| 408 |
-
#
|
| 409 |
-
# 47GB 2g.48gb MIG slice
|
| 410 |
-
#
|
| 411 |
-
#
|
| 412 |
-
#
|
| 413 |
-
|
|
|
|
|
|
|
| 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):
|