akhaliq's picture
akhaliq HF Staff
Move pipeline load to module top (build-stage cache); collapse advanced on mobile
b202e96
Raw
History Blame Contribute Delete
4.33 kB
import os
import random
import spaces
import torch
from diffusers import QwenImagePipeline
from fastapi.responses import HTMLResponse
from gradio import Server
from gradio.data_classes import FileData
torch.set_float32_matmul_precision("high")
MODEL_ID = "nvidia/Qwen-Image-Flash"
MAX_SEED = 2**31 - 1
# Load the pipeline at module top, *not* inside @spaces.GPU. Reasons:
# 1. ZeroGPU docs explicitly say lazy-loading inside @spaces.GPU is
# discouraged — module-level placement uses PyTorch's CUDA emulation
# mode outside the decorator, so `pipe.to("cuda")` works on the
# CPU-only Space builder, and the real-CUDA transfers are optimized
# for placements done at startup rather than inside a handler.
# 2. Crucially, `from_pretrained` runs during *module import* — which the
# Space builder executes as part of the build stage, BEFORE the runtime
# container reports "Running." That means the ~20 GB checkpoint lands
# in the cache during build, so the runtime container starts up with
# the weights already on disk and the first request can begin GPU work
# immediately rather than downloading first.
pipe = QwenImagePipeline.from_pretrained(MODEL_ID, torch_dtype=torch.bfloat16)
pipe.to("cuda")
def _get_duration(
prompt: str,
negative_prompt: str | None,
width: int,
height: int,
num_inference_steps: int,
seed: int,
randomize_seed: bool,
) -> int:
"""Return the GPU time budget the ZeroGPU scheduler should grant.
4 denoising steps over 1024^2 is ~30-60 s on Blackwell. We size the budget
loosely around `steps * 15s + 30s` (clamped 60-180 s) so larger step counts
and resolutions don't get cut off, but stay under the 3-min ceiling where
queue priority starts to suffer.
"""
steps = max(1, int(num_inference_steps))
return min(180, max(60, steps * 15 + 30))
app = Server()
@app.api
@spaces.GPU(size="xlarge", duration=_get_duration)
def generate_image(
prompt: str,
negative_prompt: str | None,
width: int,
height: int,
num_inference_steps: int,
seed: int,
randomize_seed: bool,
) -> tuple[FileData, int, str]:
"""Generate an image from a text prompt with Qwen-Image-Flash.
The DMD2 student internalized CFG=4.0 during distillation, so inference runs
at ``true_cfg_scale=1.0`` with ``guidance_scale=None`` / ``negative_prompt=None``
to avoid applying guidance a second time. The shift-3 FlowMatch Euler
scheduler produces the required 4-step trajectory. Width and height must be
divisible by 16; ``1024 x 1024`` is the tested setting.
"""
if not prompt or not prompt.strip():
raise ValueError("Prompt cannot be empty.")
# Snap to a multiple of 16 (the VAE constraint) and clamp to a sane range.
width = max(256, min(2048, (int(width) // 16) * 16))
height = max(256, min(2048, (int(height) // 16) * 16))
if randomize_seed:
seed = random.randint(0, MAX_SEED)
seed = int(seed) % (MAX_SEED + 1)
negative = negative_prompt.strip() if negative_prompt and negative_prompt.strip() else None
generator = torch.Generator(device="cuda").manual_seed(seed)
result = pipe(
prompt=prompt,
negative_prompt=negative,
width=width,
height=height,
num_inference_steps=int(num_inference_steps),
true_cfg_scale=1.0,
guidance_scale=None,
generator=generator,
)
image = result.images[0]
# Persist to disk so the queue can ship it back to the JS client as a FileData URL.
out_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "outputs")
os.makedirs(out_dir, exist_ok=True)
out_path = os.path.join(out_dir, f"qwen-image-flash-{seed}-{width}x{height}.png")
image.save(out_path)
info = (
f"seed={seed} | {width}x{height} | {num_inference_steps} steps | "
f"true_cfg_scale=1.0"
)
return FileData(path=out_path), seed, info
@app.get("/")
async def homepage() -> HTMLResponse:
"""Serve the custom vanilla-HTML frontend."""
html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "index.html")
with open(html_path, "r", encoding="utf-8") as f:
return HTMLResponse(content=f.read(), media_type="text/html")
if __name__ == "__main__":
app.launch(show_error=True)