- diffusion_memory: when group offload is unavailable and the plan falls back to whole-module offload, enable VAE tiling (the group plan left it off, but the fallback is the low-VRAM path where the decode spike can OOM). Covers both the group and sequential fallback branches. - perf_verify: include the balanced-vs-off PSNR in the pass/fail condition, so a balanced bit-identity regression actually fails the check instead of exiting 0. - compare_engines: --vae/--llm default to None (were author-absolute /mnt paths), and the load-progress poll has a 30 min deadline instead of looping forever on a hang. - test for the group->model fallback enabling VAE tiling.
174 lines
6.6 KiB
Python
174 lines
6.6 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
|
|
"""Head-to-head: PyTorch (diffusers GGUF) vs native stable-diffusion.cpp.
|
|
|
|
Same Z-Image GGUF transformer, same VAE + text encoder, same resolution / steps /
|
|
seed, both resident (no CPU offload) on the same GPU. Reports per-engine compute
|
|
latency (model already loaded) so the denoise + VAE + TE work is compared fairly;
|
|
for sd.cpp it also reports the one-shot wall time (compute + the per-call model
|
|
reload, which a persistent sd-server would remove).
|
|
|
|
PyTorch runs first (load / warmup / median), is unloaded, then sd.cpp runs.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
_BACKEND_ROOT = Path(__file__).resolve().parent.parent / "studio" / "backend"
|
|
if str(_BACKEND_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(_BACKEND_ROOT))
|
|
|
|
PROMPT = "A cinematic photograph of a red fox in a snowy forest at dawn, highly detailed"
|
|
_DONE_RE = re.compile(r"generate_image completed in ([0-9.]+)s")
|
|
|
|
|
|
def _median(xs):
|
|
return sorted(xs)[len(xs) // 2]
|
|
|
|
|
|
def bench_pytorch(repo, gguf, resolutions, steps, seed, iters):
|
|
import torch
|
|
from core.inference.diffusion import DiffusionBackend
|
|
|
|
rows = []
|
|
backend = DiffusionBackend()
|
|
for speed in ("off", "default"):
|
|
backend.begin_load(repo, gguf_filename = gguf, speed_mode = speed)
|
|
deadline = time.time() + 1800 # 30 min: a stuck download/load must not hang forever
|
|
while backend.load_progress().get("phase") != "ready":
|
|
prog = backend.load_progress()
|
|
if prog.get("phase") == "error":
|
|
raise RuntimeError(prog)
|
|
if time.time() > deadline:
|
|
raise TimeoutError(f"load timed out (last progress: {prog})")
|
|
time.sleep(0.5)
|
|
for res in resolutions:
|
|
|
|
def gen():
|
|
torch.cuda.synchronize()
|
|
t0 = time.time()
|
|
backend.generate(
|
|
prompt = PROMPT,
|
|
width = res,
|
|
height = res,
|
|
steps = steps,
|
|
guidance = 0.0,
|
|
seed = seed,
|
|
batch_size = 1,
|
|
)
|
|
torch.cuda.synchronize()
|
|
return time.time() - t0
|
|
|
|
gen() # warmup (compiles for `default`)
|
|
med = _median([gen() for _ in range(iters)])
|
|
rows.append(("pytorch", speed, res, med, None))
|
|
print(f" pytorch speed={speed:7s} {res}px compute={med:.3f}s", flush = True)
|
|
backend.unload()
|
|
return rows
|
|
|
|
|
|
def bench_sdcpp(binary, gguf, vae, llm, resolutions, steps, seed, iters):
|
|
from core.inference.sd_cpp_args import SdCppGenParams, SdCppModelFiles
|
|
from core.inference.sd_cpp_engine import SdCppEngine
|
|
|
|
engine = SdCppEngine(binary = binary)
|
|
if not engine.is_available():
|
|
print(" sd.cpp binary not available; skipping", flush = True)
|
|
return []
|
|
files = SdCppModelFiles(diffusion_model = gguf, vae = vae, llm = llm)
|
|
rows = []
|
|
out_dir = Path("outputs/compare_engines")
|
|
out_dir.mkdir(parents = True, exist_ok = True)
|
|
for native in (None, "default"): # resident-no-fa vs resident+--diffusion-fa
|
|
for res in resolutions:
|
|
params = SdCppGenParams(
|
|
prompt = PROMPT, width = res, height = res, steps = steps, cfg_scale = 1.0, seed = seed
|
|
)
|
|
computes, walls = [], []
|
|
for _ in range(iters):
|
|
captured = {"c": None}
|
|
|
|
def _log(ln):
|
|
m = _DONE_RE.search(ln)
|
|
if m:
|
|
captured["c"] = float(m.group(1))
|
|
|
|
t0 = time.time()
|
|
engine.generate(
|
|
files,
|
|
params,
|
|
output_path = str(out_dir / f"sd_{native}_{res}.png"),
|
|
offload = [],
|
|
native_speed = native,
|
|
on_log = _log,
|
|
)
|
|
walls.append(time.time() - t0)
|
|
if captured["c"] is not None:
|
|
computes.append(captured["c"])
|
|
med_c = _median(computes) if computes else None
|
|
med_w = _median(walls)
|
|
tag = "default(+fa)" if native == "default" else "off"
|
|
rows.append(("sdcpp", tag, res, med_c, med_w))
|
|
print(
|
|
f" sdcpp speed={tag:12s} {res}px compute={med_c}s wall={med_w:.3f}s",
|
|
flush = True,
|
|
)
|
|
return rows
|
|
|
|
|
|
def main(argv = None) -> int:
|
|
p = argparse.ArgumentParser()
|
|
p.add_argument("--repo", default = "unsloth/Z-Image-Turbo-GGUF")
|
|
p.add_argument("--gguf-name", default = "z-image-turbo-Q4_K_M.gguf")
|
|
p.add_argument("--sd-binary", default = None)
|
|
p.add_argument(
|
|
"--sd-gguf", default = None, help = "local gguf for sd.cpp (default: same as pytorch via cache)"
|
|
)
|
|
p.add_argument(
|
|
"--vae",
|
|
default = None,
|
|
help = "VAE safetensors for sd.cpp (required when benchmarking the sd.cpp engine)",
|
|
)
|
|
p.add_argument(
|
|
"--llm",
|
|
default = None,
|
|
help = "text-encoder GGUF for sd.cpp (required when benchmarking the sd.cpp engine)",
|
|
)
|
|
p.add_argument("--resolutions", default = "512,1024")
|
|
p.add_argument("--steps", type = int, default = 8)
|
|
p.add_argument("--seed", type = int, default = 42)
|
|
p.add_argument("--iters", type = int, default = 3)
|
|
args = p.parse_args(argv)
|
|
|
|
from huggingface_hub import hf_hub_download
|
|
from core.inference.sd_cpp_engine import find_sd_cpp_binary
|
|
|
|
resolutions = [int(x) for x in args.resolutions.split(",")]
|
|
sd_gguf = args.sd_gguf or hf_hub_download(args.repo, args.gguf_name)
|
|
binary = args.sd_binary or find_sd_cpp_binary()
|
|
|
|
print("== PyTorch (diffusers GGUF) ==", flush = True)
|
|
pt = bench_pytorch(args.repo, args.gguf_name, resolutions, args.steps, args.seed, args.iters)
|
|
print("== stable-diffusion.cpp (native) ==", flush = True)
|
|
sd = bench_sdcpp(
|
|
binary, sd_gguf, args.vae, args.llm, resolutions, args.steps, args.seed, args.iters
|
|
)
|
|
|
|
print("\n==== COMPARISON (Z-Image-Turbo Q4, fixed seed, resident) ====", flush = True)
|
|
print(f"{'engine':9s} {'config':13s} {'res':>5s} {'compute_s':>10s} {'wall_s':>8s}", flush = True)
|
|
for eng, cfg, res, c, w in pt + sd:
|
|
cs = f"{c:.3f}" if c is not None else "n/a"
|
|
ws = f"{w:.3f}" if w is not None else "-"
|
|
print(f"{eng:9s} {cfg:13s} {res:5d} {cs:>10s} {ws:>8s}", flush = True)
|
|
print("COMPARE-DONE", flush = True)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|