Spaces:
Sleeping
Sleeping
Commit ·
39867e3
1
Parent(s): f30b43a
Skip text_encoder CPU offload in fast mode; type mode as an enum
Browse filesLogged phase timing showed the text_encoder-to-CPU offload before VAE
decode costs ~9.7s, while decode itself takes ~113ms. That offload was
freeing memory decode never needed at fast mode's small resolution
(measured peak stayed well under the MIG slice's headroom) — it exists
to prevent OOM at high-detail's much larger 2048px decode, so it's kept
there and only skipped for fast mode.
Replaced the raw "fast"/"high_detail" string comparisons scattered
across dimensions.py and app.py with a Mode enum (mode.py) that owns
both behaviors (max_dim, offloads_text_encoder_before_decode) as
properties on the variant.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- app.py +22 -16
- dimensions.py +0 -3
- mode.py +31 -0
- tests/test_dimensions.py +1 -12
- tests/test_mode.py +40 -0
app.py
CHANGED
|
@@ -70,7 +70,8 @@ torch.backends.cudnn.allow_tf32 = True
|
|
| 70 |
print("[startup] TF32 enabled", flush=True)
|
| 71 |
|
| 72 |
print("[startup] importing dimensions...", flush=True)
|
| 73 |
-
from dimensions import compute_output_dimensions
|
|
|
|
| 74 |
print("[startup] importing diffusers...", flush=True)
|
| 75 |
from diffusers import FlowMatchEulerDiscreteScheduler
|
| 76 |
from diffusers.models.normalization import RMSNorm
|
|
@@ -189,7 +190,7 @@ pipe = QwenImageEditPlusPipeline.from_pretrained(
|
|
| 189 |
torch_dtype=dtype,
|
| 190 |
)
|
| 191 |
_hb.set()
|
| 192 |
-
pipe.vae.enable_tiling(tile_sample_min_height=
|
| 193 |
print(f"[startup] pipeline loaded in {time.perf_counter()-_t1_load:.1f}s", flush=True)
|
| 194 |
|
| 195 |
print("[startup] setting cuDNN SDPA attention processor...", flush=True)
|
|
@@ -448,10 +449,11 @@ with open("templates/app.html") as _f:
|
|
| 448 |
def infer(images_b64_json, prompt, seed, randomize_seed, guidance_scale, steps, mode, gpu_duration=20, progress=gr.Progress(track_tqdm=True)):
|
| 449 |
# CPU-only preprocessing — GPU not yet allocated
|
| 450 |
gc.collect()
|
|
|
|
| 451 |
pil_images = b64_to_pil_list(images_b64_json)
|
| 452 |
_validate_infer_inputs(pil_images, prompt)
|
| 453 |
seed = _resolve_seed(seed, randomize_seed)
|
| 454 |
-
width, height = update_dimensions_on_upload(pil_images[0],
|
| 455 |
t0 = time.perf_counter()
|
| 456 |
try:
|
| 457 |
result_image, seed, duration = _infer_gpu(pil_images, prompt, seed, guidance_scale, steps, width, height, mode, int(gpu_duration))
|
|
@@ -472,9 +474,9 @@ def infer(images_b64_json, prompt, seed, randomize_seed, guidance_scale, steps,
|
|
| 472 |
raise
|
| 473 |
|
| 474 |
|
| 475 |
-
def _log_infer_start(prompt, steps, guidance_scale, seed, gpu_duration, mode):
|
| 476 |
print(f"[infer] ===== START =====")
|
| 477 |
-
print(f"[infer] steps={steps}, guidance={guidance_scale}, seed={seed}, gpu_duration={gpu_duration}s, mode={mode}")
|
| 478 |
print(f"[infer] prompt={repr(prompt[:120])}")
|
| 479 |
|
| 480 |
|
|
@@ -531,7 +533,7 @@ def _instrument_first_touch(modules_with_names, t0):
|
|
| 531 |
handle_box["h"] = module.register_forward_pre_hook(_make_hook(name, handle_box))
|
| 532 |
|
| 533 |
|
| 534 |
-
def _make_step_callback(steps, timer, t0):
|
| 535 |
"""Build the diffusers step callback that logs per-step timing and marks timer checkpoints."""
|
| 536 |
step_times = []
|
| 537 |
def _step_cb(pipeline, step_idx, timestep, cb_kwargs):
|
|
@@ -541,15 +543,19 @@ def _make_step_callback(steps, timer, t0):
|
|
| 541 |
timer.mark("first_step")
|
| 542 |
timer.mark("last_step") # overwritten each step; final value = end of last step
|
| 543 |
# Text encoder is done after prompt encoding, before the denoising loop starts.
|
| 544 |
-
#
|
| 545 |
-
#
|
| 546 |
-
#
|
|
|
|
| 547 |
if step_idx == steps - 1 and getattr(pipeline.text_encoder, "_hf_hook", None) is None:
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
|
|
|
|
|
|
|
|
|
| 553 |
delta_ms = (now - (step_times[-2] if len(step_times) > 1 else t0)) * 1000
|
| 554 |
tag = " ← includes cold-start (offload hook install + first weight transfer)" if step_idx == 0 else ""
|
| 555 |
print(f"[infer] step {step_idx+1}/{steps} done — {delta_ms:.0f}ms{tag} | t={now-t0:.1f}s")
|
|
@@ -568,7 +574,7 @@ def _log_infer_error(e, t0, timer):
|
|
| 568 |
|
| 569 |
|
| 570 |
@spaces.GPU(duration=lambda *a, **kw: int(a[8]) if len(a) > 8 else 60)
|
| 571 |
-
def _infer_gpu(pil_images, prompt, seed, guidance_scale, steps, width, height, mode, gpu_duration=20):
|
| 572 |
_cuda_ok = torch.cuda.is_available()
|
| 573 |
timer = _InferTimer(_cuda_ok)
|
| 574 |
t0 = time.perf_counter()
|
|
@@ -588,7 +594,7 @@ def _infer_gpu(pil_images, prompt, seed, guidance_scale, steps, width, height, m
|
|
| 588 |
print(f"[infer] {len(pil_images)} image(s) pre-decoded, output={width}x{height}, seed={seed}")
|
| 589 |
|
| 590 |
generator = torch.Generator(device=device).manual_seed(seed)
|
| 591 |
-
step_cb = _make_step_callback(steps, timer, t0)
|
| 592 |
|
| 593 |
timer.mark("pipe_start")
|
| 594 |
print(f"[infer] calling pipe... t={time.perf_counter()-t0:.1f}s")
|
|
|
|
| 70 |
print("[startup] TF32 enabled", flush=True)
|
| 71 |
|
| 72 |
print("[startup] importing dimensions...", flush=True)
|
| 73 |
+
from dimensions import compute_output_dimensions
|
| 74 |
+
from mode import Mode
|
| 75 |
print("[startup] importing diffusers...", flush=True)
|
| 76 |
from diffusers import FlowMatchEulerDiscreteScheduler
|
| 77 |
from diffusers.models.normalization import RMSNorm
|
|
|
|
| 190 |
torch_dtype=dtype,
|
| 191 |
)
|
| 192 |
_hb.set()
|
| 193 |
+
pipe.vae.enable_tiling(tile_sample_min_height=Mode.FAST.max_dim, tile_sample_min_width=Mode.FAST.max_dim)
|
| 194 |
print(f"[startup] pipeline loaded in {time.perf_counter()-_t1_load:.1f}s", flush=True)
|
| 195 |
|
| 196 |
print("[startup] setting cuDNN SDPA attention processor...", flush=True)
|
|
|
|
| 449 |
def infer(images_b64_json, prompt, seed, randomize_seed, guidance_scale, steps, mode, gpu_duration=20, progress=gr.Progress(track_tqdm=True)):
|
| 450 |
# CPU-only preprocessing — GPU not yet allocated
|
| 451 |
gc.collect()
|
| 452 |
+
mode = Mode.from_value(mode)
|
| 453 |
pil_images = b64_to_pil_list(images_b64_json)
|
| 454 |
_validate_infer_inputs(pil_images, prompt)
|
| 455 |
seed = _resolve_seed(seed, randomize_seed)
|
| 456 |
+
width, height = update_dimensions_on_upload(pil_images[0], mode.max_dim)
|
| 457 |
t0 = time.perf_counter()
|
| 458 |
try:
|
| 459 |
result_image, seed, duration = _infer_gpu(pil_images, prompt, seed, guidance_scale, steps, width, height, mode, int(gpu_duration))
|
|
|
|
| 474 |
raise
|
| 475 |
|
| 476 |
|
| 477 |
+
def _log_infer_start(prompt, steps, guidance_scale, seed, gpu_duration, mode: Mode):
|
| 478 |
print(f"[infer] ===== START =====")
|
| 479 |
+
print(f"[infer] steps={steps}, guidance={guidance_scale}, seed={seed}, gpu_duration={gpu_duration}s, mode={mode.value}")
|
| 480 |
print(f"[infer] prompt={repr(prompt[:120])}")
|
| 481 |
|
| 482 |
|
|
|
|
| 533 |
handle_box["h"] = module.register_forward_pre_hook(_make_hook(name, handle_box))
|
| 534 |
|
| 535 |
|
| 536 |
+
def _make_step_callback(steps, timer, t0, mode: Mode):
|
| 537 |
"""Build the diffusers step callback that logs per-step timing and marks timer checkpoints."""
|
| 538 |
step_times = []
|
| 539 |
def _step_cb(pipeline, step_idx, timestep, cb_kwargs):
|
|
|
|
| 543 |
timer.mark("first_step")
|
| 544 |
timer.mark("last_step") # overwritten each step; final value = end of last step
|
| 545 |
# Text encoder is done after prompt encoding, before the denoising loop starts.
|
| 546 |
+
# Dropping it (~15GB) ahead of VAE decode's fp32-upcast memory spike only pays off
|
| 547 |
+
# when that spike is big enough to need the headroom (see Mode.offloads_text_encoder_before_decode).
|
| 548 |
+
# Also skipped when accelerate hooks are managing placement (offload-fallback path)
|
| 549 |
+
# to avoid fighting their own device bookkeeping.
|
| 550 |
if step_idx == steps - 1 and getattr(pipeline.text_encoder, "_hf_hook", None) is None:
|
| 551 |
+
if mode.offloads_text_encoder_before_decode:
|
| 552 |
+
_offload_t0 = time.perf_counter()
|
| 553 |
+
pipeline.text_encoder.to("cpu")
|
| 554 |
+
torch.cuda.empty_cache()
|
| 555 |
+
_offload_ms = (time.perf_counter() - _offload_t0) * 1000
|
| 556 |
+
print(f"[infer] text_encoder offload to cpu — {_offload_ms:.0f}ms | t={time.perf_counter()-t0:.1f}s")
|
| 557 |
+
else:
|
| 558 |
+
print(f"[infer] skipping text_encoder offload for mode={mode.value} (ample headroom at this resolution)")
|
| 559 |
delta_ms = (now - (step_times[-2] if len(step_times) > 1 else t0)) * 1000
|
| 560 |
tag = " ← includes cold-start (offload hook install + first weight transfer)" if step_idx == 0 else ""
|
| 561 |
print(f"[infer] step {step_idx+1}/{steps} done — {delta_ms:.0f}ms{tag} | t={now-t0:.1f}s")
|
|
|
|
| 574 |
|
| 575 |
|
| 576 |
@spaces.GPU(duration=lambda *a, **kw: int(a[8]) if len(a) > 8 else 60)
|
| 577 |
+
def _infer_gpu(pil_images, prompt, seed, guidance_scale, steps, width, height, mode: Mode, gpu_duration=20):
|
| 578 |
_cuda_ok = torch.cuda.is_available()
|
| 579 |
timer = _InferTimer(_cuda_ok)
|
| 580 |
t0 = time.perf_counter()
|
|
|
|
| 594 |
print(f"[infer] {len(pil_images)} image(s) pre-decoded, output={width}x{height}, seed={seed}")
|
| 595 |
|
| 596 |
generator = torch.Generator(device=device).manual_seed(seed)
|
| 597 |
+
step_cb = _make_step_callback(steps, timer, t0, mode)
|
| 598 |
|
| 599 |
timer.mark("pipe_start")
|
| 600 |
print(f"[infer] calling pipe... t={time.perf_counter()-t0:.1f}s")
|
dimensions.py
CHANGED
|
@@ -2,9 +2,6 @@ MAX_OUTPUT_DIM = 2048
|
|
| 2 |
MAX_OUTPUT_DIM_FAST = 768
|
| 3 |
|
| 4 |
|
| 5 |
-
def max_dim_for_mode(mode): return MAX_OUTPUT_DIM_FAST if mode == "fast" else MAX_OUTPUT_DIM
|
| 6 |
-
|
| 7 |
-
|
| 8 |
def compute_output_dimensions(w, h, max_dim=MAX_OUTPUT_DIM):
|
| 9 |
# Pin the long side to max_dim and scale the short side proportionally.
|
| 10 |
# We snap to the nearest multiple of 8 (not floor) to minimise aspect ratio
|
|
|
|
| 2 |
MAX_OUTPUT_DIM_FAST = 768
|
| 3 |
|
| 4 |
|
|
|
|
|
|
|
|
|
|
| 5 |
def compute_output_dimensions(w, h, max_dim=MAX_OUTPUT_DIM):
|
| 6 |
# Pin the long side to max_dim and scale the short side proportionally.
|
| 7 |
# We snap to the nearest multiple of 8 (not floor) to minimise aspect ratio
|
mode.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from enum import Enum
|
| 2 |
+
|
| 3 |
+
from dimensions import MAX_OUTPUT_DIM, MAX_OUTPUT_DIM_FAST
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Mode(str, Enum):
|
| 7 |
+
"""UI speed/quality presets. Behavior that depends on the mode lives here as a
|
| 8 |
+
property on the variant, rather than call sites branching on a raw mode string."""
|
| 9 |
+
|
| 10 |
+
FAST = "fast"
|
| 11 |
+
HIGH_DETAIL = "high_detail"
|
| 12 |
+
|
| 13 |
+
@classmethod
|
| 14 |
+
def from_value(cls, value) -> "Mode":
|
| 15 |
+
try:
|
| 16 |
+
return cls(value)
|
| 17 |
+
except ValueError:
|
| 18 |
+
return cls.HIGH_DETAIL
|
| 19 |
+
|
| 20 |
+
@property
|
| 21 |
+
def max_dim(self) -> int:
|
| 22 |
+
return MAX_OUTPUT_DIM_FAST if self is Mode.FAST else MAX_OUTPUT_DIM
|
| 23 |
+
|
| 24 |
+
@property
|
| 25 |
+
def offloads_text_encoder_before_decode(self) -> bool:
|
| 26 |
+
# Fast mode's small decode (<=768px) fits comfortably in the headroom already
|
| 27 |
+
# freed by the fp8-resident transformer, so evicting the ~15GB text encoder
|
| 28 |
+
# first — measured at ~9.7s on this Space's MIG slice — buys nothing there.
|
| 29 |
+
# High-detail's decode is up to ~8.5x more pixels (2048px), where that
|
| 30 |
+
# headroom margin is unverified, so it keeps the safety net.
|
| 31 |
+
return self is Mode.HIGH_DETAIL
|
tests/test_dimensions.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import pytest
|
| 2 |
-
from dimensions import compute_output_dimensions,
|
| 3 |
|
| 4 |
|
| 5 |
def aspect_ratio_error(w_in, h_in, w_out, h_out):
|
|
@@ -97,14 +97,3 @@ def test_custom_max_dim():
|
|
| 97 |
nw, nh = compute_output_dimensions(1920, 1080, max_dim=1024)
|
| 98 |
assert nw == 1024
|
| 99 |
assert nh % 8 == 0
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
# --- mode -> max dimension mapping ---
|
| 103 |
-
|
| 104 |
-
def test_fast_mode_uses_fast_max_dim():
|
| 105 |
-
assert max_dim_for_mode("fast") == MAX_OUTPUT_DIM_FAST
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
@pytest.mark.parametrize("mode", ["high_detail", "anything_else", None])
|
| 109 |
-
def test_non_fast_modes_use_default_max_dim(mode):
|
| 110 |
-
assert max_dim_for_mode(mode) == MAX_OUTPUT_DIM
|
|
|
|
| 1 |
import pytest
|
| 2 |
+
from dimensions import compute_output_dimensions, MAX_OUTPUT_DIM
|
| 3 |
|
| 4 |
|
| 5 |
def aspect_ratio_error(w_in, h_in, w_out, h_out):
|
|
|
|
| 97 |
nw, nh = compute_output_dimensions(1920, 1080, max_dim=1024)
|
| 98 |
assert nw == 1024
|
| 99 |
assert nh % 8 == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_mode.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
from dimensions import MAX_OUTPUT_DIM, MAX_OUTPUT_DIM_FAST
|
| 3 |
+
from mode import Mode
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
# --- from_value ---
|
| 7 |
+
|
| 8 |
+
def test_from_value_accepts_known_strings():
|
| 9 |
+
assert Mode.from_value("fast") is Mode.FAST
|
| 10 |
+
assert Mode.from_value("high_detail") is Mode.HIGH_DETAIL
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@pytest.mark.parametrize("value", ["anything_else", None, "", "FAST"])
|
| 14 |
+
def test_from_value_falls_back_to_high_detail(value):
|
| 15 |
+
assert Mode.from_value(value) is Mode.HIGH_DETAIL
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def test_from_value_is_idempotent_on_mode_instances():
|
| 19 |
+
assert Mode.from_value(Mode.FAST) is Mode.FAST
|
| 20 |
+
assert Mode.from_value(Mode.HIGH_DETAIL) is Mode.HIGH_DETAIL
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# --- max_dim ---
|
| 24 |
+
|
| 25 |
+
def test_fast_mode_uses_fast_max_dim():
|
| 26 |
+
assert Mode.FAST.max_dim == MAX_OUTPUT_DIM_FAST
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def test_high_detail_mode_uses_default_max_dim():
|
| 30 |
+
assert Mode.HIGH_DETAIL.max_dim == MAX_OUTPUT_DIM
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# --- offloads_text_encoder_before_decode ---
|
| 34 |
+
|
| 35 |
+
def test_fast_mode_skips_text_encoder_offload():
|
| 36 |
+
assert Mode.FAST.offloads_text_encoder_before_decode is False
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def test_high_detail_mode_keeps_text_encoder_offload():
|
| 40 |
+
assert Mode.HIGH_DETAIL.offloads_text_encoder_before_decode is True
|