someone-in-the-world commited on
Commit
1a51374
·
2 Parent(s): 4585687c0f3a61

Merge main into ja: apply fp8-resident transformer fix to Japanese space

Browse files
Files changed (2) hide show
  1. app.py +179 -39
  2. requirements.txt +3 -3
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")
@@ -73,6 +74,7 @@ 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)
@@ -94,16 +96,90 @@ 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)
@@ -387,45 +463,113 @@ def infer(images_b64_json, prompt, seed, randomize_seed, guidance_scale, steps,
387
  raise
388
 
389
 
390
- @spaces.GPU(duration=lambda *a, **kw: int(a[8]) if len(a) > 8 else 60)
391
- def _infer_gpu(pil_images, prompt, seed, guidance_scale, steps, width, height, mode, gpu_duration=20):
392
- _cuda_ok = torch.cuda.is_available()
393
- timer = _InferTimer(_cuda_ok)
394
- t0 = time.perf_counter()
395
-
396
  print(f"[infer] ===== START =====")
397
  print(f"[infer] steps={steps}, guidance={guidance_scale}, seed={seed}, gpu_duration={gpu_duration}s, mode={mode}")
398
  print(f"[infer] prompt={repr(prompt[:120])}")
399
 
400
- if _cuda_ok:
401
- p = torch.cuda.get_device_properties(0)
402
- print(f"[infer] GPU: {p.name}, total={p.total_memory/1024**3:.1f}GB, cap={p.major}.{p.minor}")
403
- torch.cuda.reset_peak_memory_stats()
404
-
405
- # Sequential offload: only the component currently doing work (text_encoder,
406
- # then transformer, then vae — see model_cpu_offload_seq) sits on GPU at a
407
- # time, instead of the full ~37GB pipeline resident simultaneously. Needed
408
- # to fit on smaller MIG slices (e.g. the 47GB 2g.48gb partition) without OOM.
409
- if getattr(pipe.transformer, "_hf_hook", None) is None:
410
- pipe.enable_model_cpu_offload(device=device)
411
- print(f"[infer] enabled sequential cpu offload on {device}")
412
- print(f"[infer] {_gpu_mem_str(_cuda_ok)} — t={time.perf_counter()-t0:.1f}s")
413
-
414
- print(f"[infer] {len(pil_images)} image(s) pre-decoded, output={width}x{height}, seed={seed}")
415
-
416
- generator = torch.Generator(device=device).manual_seed(seed)
417
 
418
- _step_t = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
  def _step_cb(pipeline, step_idx, timestep, cb_kwargs):
420
  now = time.perf_counter()
421
- _step_t.append(now)
422
  if step_idx == 0:
423
  timer.mark("first_step")
424
  timer.mark("last_step") # overwritten each step; final value = end of last step
425
- delta_ms = (now - (_step_t[-2] if len(_step_t) > 1 else t0)) * 1000
426
- tag = " ← includes compile" if step_idx == 0 else ""
427
  print(f"[infer] step {step_idx+1}/{steps} done — {delta_ms:.0f}ms{tag} | t={now-t0:.1f}s")
428
  return cb_kwargs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
 
430
  timer.mark("pipe_start")
431
  print(f"[infer] calling pipe... t={time.perf_counter()-t0:.1f}s")
@@ -434,7 +578,7 @@ def _infer_gpu(pil_images, prompt, seed, guidance_scale, steps, width, height, m
434
  image=pil_images, prompt=prompt, negative_prompt=negative_prompt,
435
  height=height, width=width, num_inference_steps=steps,
436
  generator=generator, true_cfg_scale=guidance_scale,
437
- callback_on_step_end=_step_cb,
438
  callback_on_step_end_tensor_inputs=["latents"],
439
  ).images[0]
440
  timer.mark("pipe_end")
@@ -443,18 +587,14 @@ def _infer_gpu(pil_images, prompt, seed, guidance_scale, steps, width, height, m
443
  duration = timer.elapsed_ms("pipe_start", "pipe_end") / 1000.0
444
  return result_image, seed, duration
445
  except Exception as e:
446
- print(f"[infer] ERROR: {type(e).__name__}: {e} | t={time.perf_counter()-t0:.1f}s")
447
- print(traceback.format_exc())
448
- try:
449
- torch.cuda.synchronize()
450
- except Exception as cuda_err:
451
- print(f"[infer] CUDA synchronize after error: {cuda_err}")
452
- timer.print_timings()
453
  raise
454
  finally:
455
- # No manual pipe.to("cpu") that fights the offload hooks' own device
456
- # bookkeeping. Hooks already return each component to CPU after its
457
- # forward; empty_cache() just reclaims the freed GPU blocks.
 
 
458
  gc.collect()
459
  torch.cuda.empty_cache()
460
  print(f"[infer] ===== END t={time.perf_counter()-t0:.1f}s =====")
 
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")
 
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)
 
96
  return done
97
 
98
 
99
+ _FP8_DTYPES = (torch.float8_e4m3fn, torch.float8_e5m2)
100
+
101
+
102
+ def _fp8_upcast_linear_forward(self, input):
103
+ weight = self.weight.to(input.dtype) if self.weight.dtype in _FP8_DTYPES else self.weight
104
+ bias = self.bias.to(input.dtype) if (self.bias is not None and self.bias.dtype in _FP8_DTYPES) else self.bias
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
+
167
  _t0_load = time.perf_counter()
168
  print("[startup] loading transformer from_pretrained (prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V23)...", flush=True)
169
  _hb = _start_heartbeat("transformer")
170
  _transformer = QwenImageTransformer2DModel.from_pretrained(
171
  "prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V23",
172
+ torch_dtype=torch.float8_e4m3fn,
173
  device_map="cpu",
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:
182
+ print(f"[startup] transformer memory footprint: unavailable ({e})", flush=True)
183
 
184
  _t1_load = time.perf_counter()
185
  print("[startup] loading pipeline from_pretrained (FireRedTeam/FireRed-Image-Edit-1.1)...", flush=True)
 
463
  raise
464
 
465
 
466
+ def _log_infer_start(prompt, steps, guidance_scale, seed, gpu_duration, mode):
 
 
 
 
 
467
  print(f"[infer] ===== START =====")
468
  print(f"[infer] steps={steps}, guidance={guidance_scale}, seed={seed}, gpu_duration={gpu_duration}s, mode={mode}")
469
  print(f"[infer] prompt={repr(prompt[:120])}")
470
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
 
472
+ def _log_gpu_properties(cuda_ok):
473
+ if not cuda_ok:
474
+ return None
475
+ p = torch.cuda.get_device_properties(0)
476
+ print(f"[infer] GPU: {p.name}, total={p.total_memory/1024**3:.1f}GB, cap={p.major}.{p.minor}")
477
+ torch.cuda.reset_peak_memory_stats()
478
+ return p
479
+
480
+
481
+ # Each ZeroGPU call runs in a fresh worker (hooks are always unset here), so
482
+ # cpu_offload buys no cross-call reuse — it only trades one bulk to(device)
483
+ # transfer for several slower hook-managed ones. The previous bf16-everywhere
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.
491
+ _FAST_PATH_MIN_GB = 40
492
+
493
+
494
+ def _place_pipe_on_device(cuda_ok, gpu_props, t0):
495
+ if getattr(pipe.transformer, "_hf_hook", None) is not None:
496
+ return # already placed by an earlier call sharing this worker
497
+ if cuda_ok and gpu_props.total_memory / 1024**3 >= _FAST_PATH_MIN_GB:
498
+ try:
499
+ pipe.to(device)
500
+ print(f"[infer] moved full pipe to {device} — t={time.perf_counter()-t0:.1f}s")
501
+ return
502
+ except torch.cuda.OutOfMemoryError:
503
+ print(f"[infer] OOM moving full pipe to {device}, falling back to cpu offload")
504
+ pipe.to("cpu")
505
+ torch.cuda.empty_cache()
506
+ pipe.enable_model_cpu_offload(device=device)
507
+ print(f"[infer] enabled cpu offload on {device} (fallback)")
508
+ return
509
+ pipe.enable_model_cpu_offload(device=device)
510
+ print(f"[infer] enabled cpu offload on {device} (slice too small for fast path)")
511
+
512
+
513
+ def _instrument_first_touch(modules_with_names, t0):
514
+ """Install self-removing forward-pre-hooks that log the moment each module is first entered."""
515
+ def _make_hook(name, handle_box):
516
+ def _hook(mod, inputs):
517
+ print(f"[infer] first call into {name} — {_gpu_mem_str(True, sync=True)} | t={time.perf_counter()-t0:.1f}s")
518
+ handle_box["h"].remove()
519
+ return _hook
520
+ for module, name in modules_with_names:
521
+ handle_box = {}
522
+ handle_box["h"] = module.register_forward_pre_hook(_make_hook(name, handle_box))
523
+
524
+
525
+ def _make_step_callback(steps, timer, t0):
526
+ """Build the diffusers step callback that logs per-step timing and marks timer checkpoints."""
527
+ step_times = []
528
  def _step_cb(pipeline, step_idx, timestep, cb_kwargs):
529
  now = time.perf_counter()
530
+ step_times.append(now)
531
  if step_idx == 0:
532
  timer.mark("first_step")
533
  timer.mark("last_step") # overwritten each step; final value = end of last step
534
+ delta_ms = (now - (step_times[-2] if len(step_times) > 1 else t0)) * 1000
535
+ tag = " ← includes cold-start (offload hook install + first weight transfer)" if step_idx == 0 else ""
536
  print(f"[infer] step {step_idx+1}/{steps} done — {delta_ms:.0f}ms{tag} | t={now-t0:.1f}s")
537
  return cb_kwargs
538
+ return _step_cb
539
+
540
+
541
+ def _log_infer_error(e, t0, timer):
542
+ print(f"[infer] ERROR: {type(e).__name__}: {e} | t={time.perf_counter()-t0:.1f}s")
543
+ print(traceback.format_exc())
544
+ try:
545
+ torch.cuda.synchronize()
546
+ except Exception as cuda_err:
547
+ print(f"[infer] CUDA synchronize after error: {cuda_err}")
548
+ timer.print_timings()
549
+
550
+
551
+ @spaces.GPU(duration=lambda *a, **kw: int(a[8]) if len(a) > 8 else 60)
552
+ def _infer_gpu(pil_images, prompt, seed, guidance_scale, steps, width, height, mode, gpu_duration=20):
553
+ _cuda_ok = torch.cuda.is_available()
554
+ timer = _InferTimer(_cuda_ok)
555
+ t0 = time.perf_counter()
556
+
557
+ _log_infer_start(prompt, steps, guidance_scale, seed, gpu_duration, mode)
558
+ gpu_props = _log_gpu_properties(_cuda_ok)
559
+
560
+ _place_pipe_on_device(_cuda_ok, gpu_props, t0)
561
+ print(f"[infer] {_gpu_mem_str(_cuda_ok)} — t={time.perf_counter()-t0:.1f}s")
562
+
563
+ if _cuda_ok:
564
+ _instrument_first_touch(
565
+ [(pipe.text_encoder, "text_encoder"), (pipe.transformer, "transformer"), (pipe.vae, "vae")],
566
+ t0,
567
+ )
568
+
569
+ print(f"[infer] {len(pil_images)} image(s) pre-decoded, output={width}x{height}, seed={seed}")
570
+
571
+ generator = torch.Generator(device=device).manual_seed(seed)
572
+ step_cb = _make_step_callback(steps, timer, t0)
573
 
574
  timer.mark("pipe_start")
575
  print(f"[infer] calling pipe... t={time.perf_counter()-t0:.1f}s")
 
578
  image=pil_images, prompt=prompt, negative_prompt=negative_prompt,
579
  height=height, width=width, num_inference_steps=steps,
580
  generator=generator, true_cfg_scale=guidance_scale,
581
+ callback_on_step_end=step_cb,
582
  callback_on_step_end_tensor_inputs=["latents"],
583
  ).images[0]
584
  timer.mark("pipe_end")
 
587
  duration = timer.elapsed_ms("pipe_start", "pipe_end") / 1000.0
588
  return result_image, seed, duration
589
  except Exception as e:
590
+ _log_infer_error(e, t0, timer)
 
 
 
 
 
 
591
  raise
592
  finally:
593
+ # No manual pipe.to("cpu"): in the offload-fallback case that fights the
594
+ # hooks' own device bookkeeping (they return each component to CPU after
595
+ # its forward), and in the normal fast-path case the worker's GPU access
596
+ # is reclaimed by ZeroGPU when this call returns regardless — paying for
597
+ # a D2H transfer here would just be wasted GPU-billed time.
598
  gc.collect()
599
  torch.cuda.empty_cache()
600
  print(f"[infer] ===== END t={time.perf_counter()-t0:.1f}s =====")
requirements.txt CHANGED
@@ -1,6 +1,6 @@
1
- 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
 
1
+ accelerate==1.14.0
2
+ diffusers==0.39.0
3
+ peft==0.19.1
4
  transformers==4.57.1
5
  huggingface_hub
6
  pyarrow