aboutsummaryrefslogtreecommitdiffstats
path: root/generate.py
blob: ba437ae4b6e6dd434133b9d776980aff82d231af (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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()