moose
Swap multi-image Gallery for two gr.Image slots
6af6e3c
Raw
History Blame Contribute Delete
13.2 kB
import os
import gc
import time
import threading
import traceback
import gradio as gr
import numpy as np
import spaces
import torch
import random
from PIL import Image
MAX_SEED = np.iinfo(np.int32).max
LANCZOS = getattr(Image, "Resampling", Image).LANCZOS
MAX_OUTPUT_DIM = 2048
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("CUDA_VISIBLE_DEVICES=", os.environ.get("CUDA_VISIBLE_DEVICES"), flush=True)
print("torch.__version__ =", torch.__version__, flush=True)
print("Using device:", device, flush=True)
print(f"CUDA device_count={torch.cuda.device_count()}, is_available={torch.cuda.is_available()}", flush=True)
# TF32 matmul: ~10-15% free speedup on Ampere/Hopper (bfloat16 accumulation paths benefit too)
torch.backends.cuda.matmul.allow_tf32 = True
torch.backends.cudnn.allow_tf32 = True
print("[startup] TF32 enabled", flush=True)
print("[startup] importing dimensions...", flush=True)
from dimensions import compute_output_dimensions, max_dim_for_mode
print("[startup] importing diffusers...", flush=True)
from diffusers import FlowMatchEulerDiscreteScheduler
print("[startup] importing QwenImageEditPlusPipeline...", flush=True)
from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
print("[startup] importing QwenImageTransformer2DModel...", flush=True)
from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
print("[startup] importing QwenDoubleStreamAttnProcessorFA3...", flush=True)
from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
print("[startup] all imports done", flush=True)
dtype = torch.bfloat16
def _start_heartbeat(label: str) -> threading.Event:
done = threading.Event()
t0 = time.perf_counter()
def _beat():
while not done.wait(timeout=15):
print(f"[startup] {label} still loading... ({time.perf_counter()-t0:.0f}s)", flush=True)
threading.Thread(target=_beat, daemon=True).start()
return done
_t0_load = time.perf_counter()
print("[startup] loading transformer from_pretrained (prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V23)...", flush=True)
_hb = _start_heartbeat("transformer")
_transformer = QwenImageTransformer2DModel.from_pretrained(
"prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V23",
torch_dtype=dtype,
device_map="cuda",
)
_hb.set()
print(f"[startup] transformer loaded in {time.perf_counter()-_t0_load:.1f}s", flush=True)
_t1_load = time.perf_counter()
print("[startup] loading pipeline from_pretrained (FireRedTeam/FireRed-Image-Edit-1.1)...", flush=True)
_hb = _start_heartbeat("pipeline")
pipe = QwenImageEditPlusPipeline.from_pretrained(
"FireRedTeam/FireRed-Image-Edit-1.1",
transformer=_transformer,
torch_dtype=dtype,
).to(device)
_hb.set()
print(f"[startup] pipeline loaded in {time.perf_counter()-_t1_load:.1f}s", flush=True)
print("[startup] using default attention processor.", flush=True)
negative_prompt = "worst quality, low quality, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, jpeg artifacts, signature, watermark, username, blurry, deformed"
def _load_pil_list(*filepaths):
"""Turn one or more optional filepaths (from gr.Image slots) into a list of RGB PIL Images."""
pil_images = []
for path in filepaths:
if not path:
continue
try:
pil_images.append(Image.open(path).convert("RGB"))
except Exception as e:
print(f"Error loading image {path}: {e}")
return pil_images
def update_dimensions_on_upload(image, max_dim):
if image is None:
return max_dim, max_dim
w, h = image.size
return compute_output_dimensions(w, h, max_dim)
class _InferTimer:
def __init__(self, cuda_ok: bool) -> None:
self._cuda_ok = cuda_ok
self._marks: dict = {}
def mark(self, name: str) -> None:
ev = None
if self._cuda_ok:
ev = torch.cuda.Event(enable_timing=True)
ev.record()
self._marks[name] = (ev, time.perf_counter())
def elapsed_ms(self, a: str, b: str) -> float:
ev_a, t_a = self._marks[a]
ev_b, t_b = self._marks[b]
if ev_a and ev_b:
return ev_a.elapsed_time(ev_b) # true GPU-timeline ms
return (t_b - t_a) * 1000.0
def wall_start(self, name: str) -> float:
return self._marks[name][1]
def __contains__(self, name: str) -> bool:
return name in self._marks
def print_timings(self) -> None:
if self._cuda_ok:
try:
torch.cuda.synchronize()
except Exception:
pass
rows = [
("image_load", "load_start", "load_end"),
("preprocess", "pipe_start", "first_step"),
("inference", "first_step", "last_step"),
("vae_decode", "last_step", "pipe_end"),
]
total_ms = 0.0
lines = []
for label, a, b in rows:
if a in self._marks and b in self._marks:
ms = self.elapsed_ms(a, b)
total_ms += ms
lines.append(f"[timing] {label:<14} {ms:8.1f} ms")
if "load_start" in self._marks and "pipe_end" in self._marks:
overall_ms = self.elapsed_ms("load_start", "pipe_end")
lines.append(f"[timing] {'overhead':<14} {overall_ms - total_ms:8.1f} ms")
lines.append(f"[timing] {'── total ──':<14} {overall_ms:8.1f} ms")
print("[timing] ─────────────────────────────────────")
print("\n".join(lines))
print("[timing] ─────────────────────────────────────")
def _gpu_mem_str(cuda_ok: bool, sync: bool = False) -> str:
if not cuda_ok:
return "CUDA not available"
if sync:
try:
torch.cuda.synchronize()
except Exception as se:
return f"CUDA sync failed: {se}"
alloc = torch.cuda.memory_allocated() / 1024**3
reserved = torch.cuda.memory_reserved() / 1024**3
peak = torch.cuda.max_memory_allocated() / 1024**3
return f"alloc={alloc:.2f}GB reserved={reserved:.2f}GB peak={peak:.2f}GB"
def _validate_infer_inputs(pil_images: list, prompt: str) -> None:
if not pil_images:
raise gr.Error("Please upload at least one image to edit.")
if not prompt or prompt.strip() == "":
raise gr.Error("Please enter an edit prompt.")
def _resolve_seed(seed: int, randomize_seed: bool) -> int:
return random.randint(0, MAX_SEED) if randomize_seed else seed
# ── Gradio blocks ──────────────────────────────────────────────────────────────
def infer(image_1, image_2, prompt, seed, randomize_seed, guidance_scale, steps, mode, gpu_duration=20, progress=gr.Progress(track_tqdm=True)):
# CPU-only preprocessing — GPU not yet allocated
gc.collect()
pil_images = _load_pil_list(image_1, image_2)
_validate_infer_inputs(pil_images, prompt)
seed = _resolve_seed(seed, randomize_seed)
width, height = update_dimensions_on_upload(pil_images[0], max_dim_for_mode(mode))
result_image, seed, _duration = _infer_gpu(pil_images, prompt, seed, guidance_scale, steps, width, height, mode, int(gpu_duration))
return result_image, seed
@spaces.GPU(duration=lambda *a, **kw: int(a[8]) if len(a) > 8 else 60)
def _infer_gpu(pil_images, prompt, seed, guidance_scale, steps, width, height, mode, gpu_duration=20):
_cuda_ok = torch.cuda.is_available()
timer = _InferTimer(_cuda_ok)
t0 = time.perf_counter()
print(f"[infer] ===== START =====")
print(f"[infer] steps={steps}, guidance={guidance_scale}, seed={seed}, gpu_duration={gpu_duration}s, mode={mode}")
print(f"[infer] prompt={repr(prompt[:120])}")
if _cuda_ok:
p = torch.cuda.get_device_properties(0)
print(f"[infer] GPU: {p.name}, total={p.total_memory/1024**3:.1f}GB, cap={p.major}.{p.minor}")
torch.cuda.reset_peak_memory_stats()
print(f"[infer] {_gpu_mem_str(_cuda_ok)} — t={time.perf_counter()-t0:.1f}s")
torch.cuda.empty_cache()
print(f"[infer] cache cleared — {_gpu_mem_str(_cuda_ok)}")
print(f"[infer] {len(pil_images)} image(s) pre-decoded, output={width}x{height}, seed={seed}")
generator = torch.Generator(device=device).manual_seed(seed)
_step_t = []
def _step_cb(pipeline, step_idx, timestep, cb_kwargs):
now = time.perf_counter()
_step_t.append(now)
if step_idx == 0:
timer.mark("first_step")
timer.mark("last_step") # overwritten each step; final value = end of last step
delta_ms = (now - (_step_t[-2] if len(_step_t) > 1 else t0)) * 1000
tag = " ← includes compile" if step_idx == 0 else ""
print(f"[infer] step {step_idx+1}/{steps} done — {delta_ms:.0f}ms{tag} | t={now-t0:.1f}s")
return cb_kwargs
timer.mark("pipe_start")
print(f"[infer] calling pipe... t={time.perf_counter()-t0:.1f}s")
try:
result_image = pipe(
image=pil_images, prompt=prompt, negative_prompt=negative_prompt,
height=height, width=width, num_inference_steps=steps,
generator=generator, true_cfg_scale=guidance_scale,
callback_on_step_end=_step_cb,
callback_on_step_end_tensor_inputs=["latents"],
).images[0]
timer.mark("pipe_end")
print(f"[infer] VAE decode + postprocess done — {_gpu_mem_str(_cuda_ok, sync=True)} | t={time.perf_counter()-t0:.1f}s")
timer.print_timings()
duration = timer.elapsed_ms("pipe_start", "pipe_end") / 1000.0
return result_image, seed, duration
except Exception as e:
print(f"[infer] ERROR: {type(e).__name__}: {e} | t={time.perf_counter()-t0:.1f}s")
print(traceback.format_exc())
try:
torch.cuda.synchronize()
except Exception as cuda_err:
print(f"[infer] CUDA synchronize after error: {cuda_err}")
timer.print_timings()
raise
finally:
gc.collect()
torch.cuda.empty_cache()
print(f"[infer] ===== END t={time.perf_counter()-t0:.1f}s =====")
# --- UI Layout ---
css = """
#col-container {
margin: 0 auto;
max-width: 1024px;
}
"""
with gr.Blocks(css=css) as demo:
with gr.Column(elem_id="col-container"):
gr.Markdown("FireRed Image Edit — drop in references and a prompt, get an edit out.")
with gr.Row():
with gr.Column():
with gr.Row():
image_1 = gr.Image(label="Image 1", type="filepath", interactive=True)
image_2 = gr.Image(label="Image 2 (optional)", type="filepath", interactive=True)
prompt = gr.Textbox(
label="Prompt",
placeholder="Describe the edit you want...",
)
run_button = gr.Button("Edit", variant="primary")
with gr.Accordion("Advanced Settings", open=False):
mode = gr.Dropdown(
label="Mode",
choices=["fast", "quality"],
value="fast",
info="'fast' caps the long side at 1024px; 'quality' allows up to 2048px.",
)
seed = gr.Slider(
label="Seed",
minimum=0,
maximum=MAX_SEED,
step=1,
value=0,
)
randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
guidance_scale = gr.Slider(
label="True guidance scale",
minimum=1.0,
maximum=10.0,
step=0.1,
value=1.0,
)
steps = gr.Slider(
label="Number of inference steps",
minimum=1,
maximum=50,
step=1,
value=4,
)
gpu_duration = gr.Slider(
label="GPU duration (seconds)",
minimum=10,
maximum=120,
step=5,
value=20,
)
with gr.Column():
result = gr.Image(label="Result", type="pil", format="png")
seed_out = gr.Number(label="Seed used", interactive=False)
gr.on(
triggers=[run_button.click, prompt.submit],
fn=infer,
inputs=[image_1, image_2, prompt, seed, randomize_seed, guidance_scale, steps, mode, gpu_duration],
outputs=[result, seed_out],
)
if __name__ == "__main__":
demo.queue(max_size=30).launch(
mcp_server=True,
ssr_mode=False,
show_error=True,
)