aboutsummaryrefslogtreecommitdiffstats
path: root/generate.py
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-07-06 18:34:31 +0200
committerDanilo M. <danix@danix.xyz>2026-07-06 18:34:31 +0200
commitfc78b51e29497703a65f125e844f7da558dbc577 (patch)
tree5e5e3eb3c79b823971d4577d86684d154d127341 /generate.py
downloadimggen-fc78b51e29497703a65f125e844f7da558dbc577.tar.gz
imggen-fc78b51e29497703a65f125e844f7da558dbc577.zip
Initial commit: local SDXL image generation on Intel Arc B580
Containerized SDXL-base generation targeting a 12GB Arc B580 under Slackware. The container carries the Intel Level Zero runtime the host distro does not package; the fp16-fix VAE removes the fp32 upcast that would otherwise OOM at decode. - imggen: host wrapper (GPU passthrough, /out bind-mount, model cache) - generate.py: in-container XPU pipeline, scope-bounded to SDXL base - Dockerfile: pinned to the known-good torch-XPU / diffusers stack - README.md, CLAUDE.md: rationale, limits, and version-pinning notes Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'generate.py')
-rw-r--r--generate.py128
1 files changed, 128 insertions, 0 deletions
diff --git a/generate.py b/generate.py
new file mode 100644
index 0000000..ba437ae
--- /dev/null
+++ b/generate.py
@@ -0,0 +1,128 @@
+#!/usr/bin/env python3
+# Copyright (C) 2026 Danilo
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# Local SDXL-base image generation for Intel Arc B580 (XPU backend).
+# Bounded scope by design: SDXL base, single 1024x1024 image, no refiner,
+# no Flux, no batching. This is what fits comfortably in 12GB VRAM once the
+# fp32 VAE upcast is removed via the fp16-fix VAE. See README.md for limits.
+
+import argparse
+import sys
+import time
+from datetime import datetime
+from pathlib import Path
+
+import torch
+from diffusers import StableDiffusionXLPipeline, AutoencoderKL
+
+MODEL = "stabilityai/stable-diffusion-xl-base-1.0"
+# fp16-fix VAE avoids the fp32 upcast that OOMs a 12GB card at decode time.
+VAE = "madebyollin/sdxl-vae-fp16-fix"
+OUT_DIR = Path("/out")
+
+
+def build_pipe():
+ if not torch.xpu.is_available():
+ # Fail loud and specific. A silent CPU fallback would run but take
+ # minutes per image, which is worse than a clear error.
+ print(
+ "ERROR: torch.xpu is not available. The Level Zero runtime is not "
+ "visible inside this container. Check that you passed --device "
+ "/dev/dri and that the base image ships the Intel GPU runtime.",
+ file=sys.stderr,
+ )
+ sys.exit(2)
+
+ vae = AutoencoderKL.from_pretrained(VAE, torch_dtype=torch.float16)
+ pipe = StableDiffusionXLPipeline.from_pretrained(
+ MODEL,
+ vae=vae,
+ torch_dtype=torch.float16,
+ use_safetensors=True,
+ variant="fp16",
+ ).to("xpu")
+ return pipe
+
+
+def generate(pipe, prompt, negative, steps, guidance, seed):
+ generator = None
+ if seed is not None:
+ generator = torch.Generator(device="xpu").manual_seed(seed)
+
+ t = time.time()
+ image = pipe(
+ prompt,
+ negative_prompt=negative,
+ num_inference_steps=steps,
+ guidance_scale=guidance,
+ generator=generator,
+ ).images[0]
+ torch.xpu.synchronize()
+ elapsed = time.time() - t
+ return image, elapsed
+
+
+def main():
+ ap = argparse.ArgumentParser(
+ description="Generate a single SDXL-base image locally on Intel Arc (XPU)."
+ )
+ ap.add_argument("prompt", help="Text prompt for the image.")
+ ap.add_argument(
+ "-n", "--negative", default="", help="Negative prompt (default: none)."
+ )
+ ap.add_argument(
+ "-s", "--steps", type=int, default=25, help="Inference steps (default: 25)."
+ )
+ ap.add_argument(
+ "-g",
+ "--guidance",
+ type=float,
+ default=7.0,
+ help="Guidance scale / CFG (default: 7.0).",
+ )
+ ap.add_argument(
+ "--seed",
+ type=int,
+ default=None,
+ help="Seed for reproducible output (default: random).",
+ )
+ ap.add_argument(
+ "-o",
+ "--out",
+ default=None,
+ help="Output filename inside /out (default: timestamped).",
+ )
+ args = ap.parse_args()
+
+ OUT_DIR.mkdir(parents=True, exist_ok=True)
+
+ if args.out:
+ out_path = OUT_DIR / args.out
+ else:
+ stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
+ out_path = OUT_DIR / f"sdxl-{stamp}.png"
+
+ print(f"Loading SDXL base + fp16-fix VAE on XPU ...", file=sys.stderr)
+ t0 = time.time()
+ pipe = build_pipe()
+ print(f"Loaded in {time.time() - t0:.1f}s", file=sys.stderr)
+
+ image, elapsed = generate(
+ pipe, args.prompt, args.negative, args.steps, args.guidance, args.seed
+ )
+ image.save(out_path)
+ print(f"Generated in {elapsed:.2f}s")
+ print(f"Saved {out_path}")
+
+
+if __name__ == "__main__":
+ main()