Spaces:
Running on Zero
Patch RMSNorm for fp8-resident weights; fix crash in prior fp8 commit
Browse files8db56cb kept the transformer's weights fp8-resident (instead of upcasting
to bf16 at load) to fix the memory OOM that was forcing this Space onto
the slow enable_model_cpu_offload path. It patched nn.Linear to upcast
its weight just-in-time for the matmul, but this model also uses RMSNorm
extensively (attn.norm_q/norm_k/norm_added_q/norm_added_k in every
transformer block, plus self.txt_norm) with real learnable weights —
confirmed via the checkpoint's safetensors headers, which show 100% of
tensors across all 3 shards as F8_E4M3, including norm gains and biases,
no mixed precision.
diffusers 0.39.0's RMSNorm.forward only upcasts its weight for
float16/bfloat16, not float8. Left unpatched, `hidden_states * self.weight`
multiplies a bf16 activation by an fp8 weight directly, which PyTorch
does not support via type promotion. Verified this reproduces exactly:
RuntimeError: Promotion for Float8 Types is not supported, attempted
to promote Float and Float8_e4m3fn
This adds _fp8_upcast_rmsnorm_forward, a copy of diffusers' RMSNorm.forward
with the same just-in-time upcast idea applied to RMSNorm's weight/bias,
and renames _patch_fp8_linears to _patch_fp8_modules to patch both module
types. Also adds a safety-net scan that logs a warning for any other
fp8-resident parameter neither patcher covers, so a future gap in this
allowlist surfaces at startup instead of mid-inference.
Verified locally (CPU, diffusers 0.39.0 installed in .venv): the
unpatched RMSNorm reproduces the RuntimeError above on an fp8 weight;
the patched version runs and matches a real-bf16-weight reference within
~0.003 max abs diff, consistent with fp8's own quantization noise rather
than a logic error. Still needs a real end-to-end run on the Space's GPU
before merging — this only verifies the patch in isolation, not the full
pipeline.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
@@ -74,6 +74,7 @@ print("[startup] importing dimensions...", flush=True)
|
|
| 74 |
from dimensions import compute_output_dimensions, max_dim_for_mode
|
| 75 |
print("[startup] importing diffusers...", flush=True)
|
| 76 |
from diffusers import FlowMatchEulerDiscreteScheduler
|
|
|
|
| 77 |
print("[startup] importing QwenImageEditPlusPipeline...", flush=True)
|
| 78 |
from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
|
| 79 |
print("[startup] importing QwenImageTransformer2DModel...", flush=True)
|
|
@@ -104,18 +105,62 @@ def _fp8_upcast_linear_forward(self, input):
|
|
| 104 |
return torch.nn.functional.linear(input, weight, bias)
|
| 105 |
|
| 106 |
|
| 107 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 111 |
-
# weight
|
| 112 |
-
#
|
| 113 |
-
#
|
|
|
|
|
|
|
| 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 |
|
|
@@ -129,8 +174,8 @@ _transformer = QwenImageTransformer2DModel.from_pretrained(
|
|
| 129 |
)
|
| 130 |
_hb.set()
|
| 131 |
print(f"[startup] transformer loaded in {time.perf_counter()-_t0_load:.1f}s", flush=True)
|
| 132 |
-
_n_fp8_patched =
|
| 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:
|
|
@@ -439,7 +484,7 @@ def _log_gpu_properties(cuda_ok):
|
|
| 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 |
-
#
|
| 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.
|
|
|
|
| 74 |
from dimensions import compute_output_dimensions, max_dim_for_mode
|
| 75 |
print("[startup] importing diffusers...", flush=True)
|
| 76 |
from diffusers import FlowMatchEulerDiscreteScheduler
|
| 77 |
+
from diffusers.models.normalization import RMSNorm
|
| 78 |
print("[startup] importing QwenImageEditPlusPipeline...", flush=True)
|
| 79 |
from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
|
| 80 |
print("[startup] importing QwenImageTransformer2DModel...", flush=True)
|
|
|
|
| 105 |
return torch.nn.functional.linear(input, weight, bias)
|
| 106 |
|
| 107 |
|
| 108 |
+
def _fp8_upcast_rmsnorm_forward(self, hidden_states):
|
| 109 |
+
# Mirrors diffusers 0.39.0's RMSNorm.forward (CUDA path, models/normalization.py), extended
|
| 110 |
+
# so an fp8-resident weight/bias gets upcast to the activation's dtype before use instead of
|
| 111 |
+
# being silently skipped — the stock implementation only special-cases float16/bfloat16, so
|
| 112 |
+
# an fp8 weight would otherwise reach `hidden_states * self.weight` unconverted and error
|
| 113 |
+
# (no elementwise op supports bf16 x fp8 operands).
|
| 114 |
+
input_dtype = hidden_states.dtype
|
| 115 |
+
variance = hidden_states.to(torch.float32).pow(2).mean(-1, keepdim=True)
|
| 116 |
+
hidden_states = hidden_states * torch.rsqrt(variance + self.eps)
|
| 117 |
+
|
| 118 |
+
if self.weight is not None:
|
| 119 |
+
weight = self.weight.to(input_dtype) if self.weight.dtype in _FP8_DTYPES else self.weight
|
| 120 |
+
if weight.dtype in (torch.float16, torch.bfloat16):
|
| 121 |
+
hidden_states = hidden_states.to(weight.dtype)
|
| 122 |
+
hidden_states = hidden_states * weight
|
| 123 |
+
if self.bias is not None:
|
| 124 |
+
bias = self.bias.to(hidden_states.dtype) if self.bias.dtype in _FP8_DTYPES else self.bias
|
| 125 |
+
hidden_states = hidden_states + bias
|
| 126 |
+
else:
|
| 127 |
+
hidden_states = hidden_states.to(input_dtype)
|
| 128 |
+
|
| 129 |
+
return hidden_states
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def _patch_fp8_modules(model) -> int:
|
| 133 |
# This checkpoint ships its weights natively in fp8 (torch_dtype below preserves that
|
| 134 |
# instead of upcasting to bf16 at load time, halving resident memory: ~19GB vs ~38GB).
|
| 135 |
+
# Neither nn.Linear nor RMSNorm (the two module types in this model that own their own
|
| 136 |
+
# weight/bias, per the checkpoint's safetensors headers — every tensor is fp8, including
|
| 137 |
+
# norm gains) have an fp8 compute kernel on this GPU, so each patched instance upcasts its
|
| 138 |
+
# own weight to the input's dtype just-in-time for the op — mathematically identical to the
|
| 139 |
+
# old load-time-upcast-everything approach (same values, same target dtype), just deferred
|
| 140 |
+
# so only one layer's weight is transiently bf16 at a time instead of all of them.
|
| 141 |
count = 0
|
| 142 |
for module in model.modules():
|
| 143 |
if isinstance(module, torch.nn.Linear) and module.weight.dtype in _FP8_DTYPES:
|
| 144 |
module.forward = types.MethodType(_fp8_upcast_linear_forward, module)
|
| 145 |
count += 1
|
| 146 |
+
elif isinstance(module, RMSNorm) and module.weight is not None and module.weight.dtype in _FP8_DTYPES:
|
| 147 |
+
module.forward = types.MethodType(_fp8_upcast_rmsnorm_forward, module)
|
| 148 |
+
count += 1
|
| 149 |
+
|
| 150 |
+
# Safety net: flag any other fp8-resident parameter that wasn't patched above, so a gap in
|
| 151 |
+
# this allowlist surfaces as a startup log line instead of a mid-inference crash — an
|
| 152 |
+
# unpatched fp8 parameter can't participate in ops with the bf16 activations around it.
|
| 153 |
+
patched_types = (torch.nn.Linear, RMSNorm)
|
| 154 |
+
for name, module in model.named_modules():
|
| 155 |
+
if isinstance(module, patched_types):
|
| 156 |
+
continue
|
| 157 |
+
for pname, param in module.named_parameters(recurse=False):
|
| 158 |
+
if param.dtype in _FP8_DTYPES:
|
| 159 |
+
print(
|
| 160 |
+
f"[startup] WARNING: unpatched fp8 parameter {name}.{pname} "
|
| 161 |
+
f"({type(module).__name__}) — will likely error at inference",
|
| 162 |
+
flush=True,
|
| 163 |
+
)
|
| 164 |
return count
|
| 165 |
|
| 166 |
|
|
|
|
| 174 |
)
|
| 175 |
_hb.set()
|
| 176 |
print(f"[startup] transformer loaded in {time.perf_counter()-_t0_load:.1f}s", flush=True)
|
| 177 |
+
_n_fp8_patched = _patch_fp8_modules(_transformer)
|
| 178 |
+
print(f"[startup] patched {_n_fp8_patched} fp8-resident nn.Linear/RMSNorm modules for just-in-time upcast", flush=True)
|
| 179 |
try:
|
| 180 |
print(f"[startup] transformer memory footprint: {_transformer.get_memory_footprint()/1024**3:.2f}GB", flush=True)
|
| 181 |
except Exception as e:
|
|
|
|
| 484 |
# pipeline (~40GB transformer + ~14GB text encoder) peaked at 46.82GB moving
|
| 485 |
# onto this Space's 47GB 2g.48gb MIG slice and still OOM'd, wasting ~40s
|
| 486 |
# before falling back. The transformer now stays fp8-resident (see
|
| 487 |
+
# _patch_fp8_modules above, ~19GB instead of ~38GB), so the full pipeline
|
| 488 |
# should total roughly ~35GB — comfortably under the slice with headroom to
|
| 489 |
# spare. Lowered accordingly, but the OOM fallback below stays as a safety
|
| 490 |
# net in case that estimate is off.
|