Studio diffusion (Phase 7): accuracy-preserving speed pass (2.2x via GGUF compile) (#6690)

* Studio diffusion: cross-platform device policy, fp16 guard, lock split, validate-before-evict

Phase 1 of porting the richer diffusion stack onto the image-generation backend.

- Add a compartmentalized device/dtype policy module (diffusion_device.py)
  resolving CUDA/ROCm/XPU/MPS/CPU with capability flags. Keeps the NVIDIA
  capability-based bf16 choice; ROCm and XPU are isolated; MPS uses bf16 or
  fp32, never a silent fp16 that renders a black image.
- Add a per-family fp16_incompatible flag (Z-Image) and promote a resolved
  float16 to float32 for those families so they do not produce black images.
- Split the backend locks: a generation holds only _generate_lock, so status,
  unload, and a new load are never blocked by a long denoise. Add per-generation
  cancellation via callback_on_step_end so an eviction or a superseding load
  preempts a running generation; a replacement load waits for it to stop before
  allocating, so two pipelines never sit in VRAM at once.
- Validate a load request before the GPU handoff so an unloadable pick never
  evicts a working chat model, and reject missing local paths up front.
- Add CPU-only tests for the device policy, dtype guard, lock split and
  cancellation, and validate-before-evict, plus a GPU benchmark/regression
  script (scripts/diffusion_bench.py) measuring latency, peak VRAM, and PSNR
  against a saved reference.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio diffusion (Phase 2A): measured-budget memory planner + offload/VAE policy

Add a lean, backend-agnostic memory policy that picks a CPU-offload policy and
VAE tiling/slicing from measured free device memory vs the model's estimated
resident footprint, then applies it to the built pipeline. auto stays resident
when the model fits (byte-identical to the prior resident path), and falls to
whole-module offload when tight; fast/balanced/low_vram are explicit overrides.
Sequential submodule offload is unreliable for GGUF transformers on diffusers
0.38, so it falls back to whole-module offload and status reports the policy
actually engaged.

Verified on Z-Image-Turbo Q4_K_M (B200): auto reproduces the resident image with
no VRAM/latency regression (PSNR inf); balanced/low_vram cut generation peak VRAM
47.9% (15951 -> 8318 MB) with byte-identical output, at the expected latency cost.

73 prior + 35 new CPU tests pass.

* Studio diffusion (Phase 2D): streamed block-level offload + functional VAE tiling

Add a streamed 'group' offload tier (diffusers apply_group_offloading, block_level,
use_stream) that keeps the transformer flowing through the GPU a few blocks at a
time while the text encoder / VAE stay resident, and fix VAE tiling to drive the
VAE submodule (pipelines like Z-Image expose enable_tiling on pipe.vae, not the
pipeline). apply_memory_plan now returns the (policy, tiling) actually engaged so
status never overstates either, and group falls back to whole-module offload when
the transformer can't be streamed.

Measured on Z-Image (B200), all lossless (PSNR inf vs resident): balanced/group
cuts generation peak VRAM 32% (15951 -> 10840 MB) at near-resident speed (2.07 ->
2.99s); low_vram/model cuts it 48% (-> 8318 MB) but is slower (7.99s). Mode names
now match that tradeoff: balanced = stream the transformer, low_vram = offload
every component. auto picks group when the companions fit resident, else model.

112 CPU tests pass.

* Studio diffusion (Phase 5): image quality-vs-quant accuracy harness

Add scripts/diffusion_quality.py, the accuracy analogue of the KLD workflow: hold
prompt + seed fixed, render a grid with a reference quant (default BF16), then render
each candidate quant and measure drift from the reference. Records mean PSNR + SSIM
(pure-numpy, no skimage/scipy) and optional CLIP text-alignment + image-similarity
(transformers, --clip), plus file size, latency, and peak VRAM, then prints a
quality-vs-cost table and recommends the smallest quant within a quality budget.
--selftest validates the metrics on synthetic images with no GPU or model.

Verified on Z-Image (B200): the table degrades monotonically with quant size
(Q8 -> Q4 -> Q2: PSNR 21.7 -> 15.5, SSIM 0.82 -> 0.61), while CLIP-text stays flat
(~0.34) -- quantization erodes fine detail far more than prompt adherence.

* Studio diffusion (Phase 3): opt-in speed layer (channels_last / compile / TF32)

Add a speed_mode knob (off by default, so the render path stays bit-identical):
default applies channels_last VAE + regional torch.compile of the denoiser's
repeated block where eligible; max also enables TF32 matmul and fused QKV. Regional
compile is gated off for the GGUF transformer (dequantises per-op) and for families
flagged not compile-friendly (a new supports_torch_compile flag, False for Z-Image),
so it activates automatically only once a non-GGUF bf16 transformer is loaded. Speed
optims run before placement/offload, per the diffusers composition order. status now
reports speed_mode + the optims actually engaged.

Verified on Z-Image (B200): default -> ['channels_last'], max -> ['channels_last',
'tf32'], compile correctly skipped for GGUF; generation works in every mode.

121 CPU tests pass.

* Studio diffusion (Phase 2B): opt-in fp8 text-encoder layerwise casting

Add a text_encoder_fp8 knob that casts the companion text encoder(s) to fp8 (e4m3)
storage via diffusers apply_layerwise_casting, upcasting per layer to the bf16
compute dtype while normalisations and embeddings stay full precision. Applied
before placement, gated to CUDA + bf16, best-effort (a failure leaves the encoder
dense). status reports which encoders were cast.

Verified on Z-Image (B200, balanced/group mode where the encoder stays resident):
generation peak VRAM dropped 37% (10840 -> 6791 MB, below the lowest-VRAM offload)
at near-resident speed. It is a memory-vs-quality tradeoff, not free -- ~20 dB PSNR
vs the bf16 encoder, a larger shift than one transformer quant step -- so it is off
by default and documented as such, with the Phase 5 harness to size the cost.

127 CPU tests pass.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio diffusion (Phase 2C): NVFP4 text-encoder quant (+ generalise fp8 knob)

Generalise the text-encoder precision knob from a fp8 bool to text_encoder_quant
(fp8 | nvfp4). nvfp4 quantises the companion text encoder to 4-bit via torchao
NVFP4 weight-only (two-level microscaling) on Blackwell's FP4 tensor cores; fp8
stays the broader-hardware path (cc>=8.9). Both are gated, best-effort, and run
before placement; status reports the mode actually engaged. This is the lean
realisation of GGUF-native text-encoder quant: 4-bit on the encoder without the
3045-line port.

Verified on Z-Image (B200, balanced/group where the encoder stays resident), vs the
bf16 encoder: nvfp4 cut generation peak VRAM 48% (10840 -> 5593 MB, the lowest TE
option, below whole-model offload) at near-fp8 quality (16.4 vs 17.1 dB PSNR), and
both quants ran faster than bf16. A memory-vs-quality tradeoff (off by default);
size it per model with the Phase 5 quality harness. diffusion_bench gains
--text-encoder-quant.

129 CPU tests pass.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio diffusion (Phase 4): native stable-diffusion.cpp engine for CPU/Mac

Adds the CPU / Apple-Silicon tier of the two-engine strategy, mirroring the
chat backend's llama.cpp shell-out. Diffusers stays the default on CUDA / ROCm
/ XPU; this covers the hardware diffusers serves poorly, consuming the same
split GGUF assets Studio already curates.

- sd_cpp_args.py: pure sd-cli command builder. Maps the family to its
  text-encoder flag (Z-Image Qwen3 to --llm, Qwen-Image to --qwen2vl, FLUX.1
  CLIP-L + T5), and the diffusers memory policy (none/group/model/sequential)
  to sd.cpp's offload flags (--offload-to-cpu / --clip-on-cpu / --vae-on-cpu /
  --vae-tiling / --diffusion-fa), so one user knob drives both engines.
- sd_cpp_engine.py: SdCppEngine over a located sd-cli. find_sd_cpp_binary()
  with the same precedence as the llama finder (env override, then the Studio
  install root, then in-tree, then PATH), an is_available/version probe, and a
  one-shot subprocess generate that streams progress and returns the PNG.
  runtime_env() prepends the binary's directory to the platform library path
  so a prebuilt's bundled libstable-diffusion.so resolves.
  select_diffusion_engine() is the pure routing decision (GPU backends to
  diffusers, CPU/MPS to native when present).
- install_sd_cpp_prebuilt.py: resolve + download the per-host prebuilt
  (macOS-arm64/Metal, Linux x86_64 CPU, Vulkan/ROCm/Windows variants) into the
  Studio install root. resolve_release_asset() is a pure, unit-tested
  host-to-asset matrix.
- scripts/sd_cpp_smoke.py: end-to-end native generation harness.

Tests (CPU-only, subprocess/filesystem stubbed): 49 new across args, engine,
routing, runtime env, and the installer resolver. Full diffusion suite 166
passing.

Verified on a B200 box: built sd-cli (CUDA) and the prebuilt (CPU) both
generate Z-Image-Turbo Q4_K end to end through SdCppEngine: balanced (group
offload, 5.0s gen), low_vram (full CPU offload + VAE tiling, 13.4s), and the
dynamically-linked CPU prebuilt (50.4s on CPU), all producing coherent images.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio diffusion (Phase 6): img2img / inpaint / edit / LoRA / upscale on the native engine

Builds on Phase 4's native stable-diffusion.cpp engine, extending it from
text-to-image to the wider feature surface, since sd.cpp supports all of these
through the binary already. Pure command-builder additions plus one engine
method, so the txt2img path is unchanged.

- sd_cpp_args.py: SdCppGenParams gains image-conditioning fields. init_img +
  strength make a run img2img, adding mask makes it inpaint, ref_images drives
  FLUX-Kontext / Qwen-Image-Edit style editing (repeated --ref-image), and
  lora_dir + the <lora:name:weight> prompt syntax select LoRAs. New
  SdCppUpscaleParams + build_sd_cpp_upscale_command for the ESRGAN upscale run
  mode (input image + esrgan model, no prompt / text encoders).
- sd_cpp_engine.py: the subprocess runner is factored into a shared _run() so
  generate() (now carrying the conditioning flags) and a new upscale() reuse
  the same streaming / error / output-check path.
- scripts/sd_cpp_smoke.py: --task {txt2img,img2img,upscale} with --init-img /
  --strength / --upscale-model / --upscale-repeats.

Tests: 10 new across the img2img / inpaint / edit / LoRA flag construction, the
upscale builder and its validation, and the engine's img2img + upscale paths.
Full diffusion suite 176 passing.

Verified on a B200 box through SdCppEngine: img2img (Z-Image-Turbo Q4_K, the
init image conditioned at strength 0.6, 4.8s) and ESRGAN upscale
(512x512 -> 2048x2048 via RealESRGAN_x4plus_anime_6B, 2.7s), both producing
coherent images. Video and the diffusers-path feature wiring are deferred.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio diffusion (Phase 7): accuracy-preserving speed pass

Re-review of the diffusion stack (#6675/#6679/#6680) surfaced one real accuracy
bug and a dead-on-arrival speed path; this fixes both and adds the lossless /
near-lossless wins, all measured on a B200.

Correctness:
- TF32 global-state leak (fix). speed_mode=max flipped torch.backends.*.allow_tf32
  process-wide and never restored them, so a later `off` load silently inherited
  TF32 and was no longer bit-identical. Added snapshot_backend_flags /
  restore_backend_flags (TF32 + cudnn.benchmark), captured before the speed layer
  runs and restored on unload. Verified: load max -> unload -> load off is now
  byte-identical (PSNR inf) to a fresh off.
- sd-cli timeout could hang forever. _run() blocked in `for line in stdout` and
  only checked the timeout after EOF, so a child stuck in model load / GPU init
  with no output ignored the timeout. Drained stdout on a reader thread with a
  wall-clock deadline. Added a silent-hang regression test.

Speed (diffusers path), near-lossless, opt-in tiers:
- Regional torch.compile now runs on the GGUF transformer. The is_gguf gate (and
  Z-Image's supports_torch_compile=False) were stale: compile_repeated_blocks
  compiles and runs ~2.2x faster on the GGUF Z-Image transformer on
  torch 2.9.1 / diffusers 0.38 (the per-op dequant stays eager, the rest of the
  block compiles). Measured: off 1.80s -> default 0.82s/gen (+54.7%), PSNR 37.7 dB
  vs eager -- far above the Q4 quant noise floor (~21 dB), so it does not move
  output quality. Gate relaxed; default tier delivers it.
- cudnn.benchmark added to the default tier (autotunes the fixed-shape VAE convs).
- torch.inference_mode() around the pipeline call (lossless, strictly faster than
  the no_grad diffusers uses internally).

Memory path:
- VAE tiling (not bit-identical >1MP) restricted to the model/sequential/CPU tiers;
  the balanced (group) tier keeps exact slicing only, so it is now bit-identical to
  the resident image (verified PSNR inf) and slightly faster.
- Group offload adds non_blocking + record_stream on the CUDA stream path to
  overlap each block's H2D copy with compute (lossless; gated on the installed
  diffusers signature so older versions still work).

Native (sd.cpp) path:
- native_speed_flags: a first-class speed knob (default -> --diffusion-fa, a
  near-lossless CUDA win that was previously only added on offload tiers; max also
  -> --diffusion-conv-direct). conv-direct stays opt-in: measured +45% on CUDA, so
  it is never auto-on. Engine generate() merges it, de-duped against offload flags.

Default profile: a GGUF model with no explicit speed_mode now resolves to the
`default` profile (resolve_speed_mode), since compile's perturbation sits below the
quantisation noise floor and so does not reduce quality versus the dense reference;
out of the box a GGUF Z-Image generation drops from 1.80s to 0.81s. Dense models
stay `off` / bit-identical, and an explicit speed_mode -- including "off" -- is
always honored, so the byte-identical path remains one flag away and is the
regression reference.

Tooling: scripts/compile_probe.py (eager vs compiled GGUF probe), scripts/
perf_verify.py (the B200 verification above), and diffusion_bench.py gains
--speed-mode so the speed tiers are benchmarkable.

Tests: 183 passing (was 166); new coverage for the backend-flag snapshot/restore,
GGUF compile eligibility, the balanced tiling/slicing split, native_speed_flags +
the engine de-dup, and the sd-cli silent-hang timeout.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio diffusion (Phase 7): max tier uses max-autotune-no-cudagraphs + engine/lever benchmarks

The opt-in `max` speed tier now compiles the repeated block with
mode=max-autotune-no-cudagraphs (dynamic=False) instead of the default mode:
Triton autotuning for GEMM/conv-heavier models, gated to the tier where a longer
cold compile is acceptable. CUDA-graph modes (reduce-overhead / max-autotune) are
deliberately avoided -- both crash on the regionally-compiled block (its static
output buffer is overwritten across denoise steps), measured.

Adds two reproducible benchmarks used to validate the optimization research:
- scripts/compare_engines.py: PyTorch (diffusers GGUF) vs native sd.cpp head-to-head.
- scripts/leverage_probe.py: coordinate_descent_tuning + FirstBlockCache probes.

Measured on B200 (Z-Image Q4_K_M, 1024px, 8 steps): default compile 0.80s/gen;
coordinate_descent_tuning 0.79s (within noise, already covered by max-autotune);
FirstBlockCache does not run on Z-Image (diffusers 0.38 block-detection / Dynamo).

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio diffusion (Phase 7): robust backend-flag snapshot/restore and restore on failed speeded load

- snapshot_backend_flags reads each flag defensively (getattr + hasattr), so a build/platform
  missing one (no cuda.matmul on CPU/MPS) still captures the rest instead of skipping the
  whole snapshot. restore_backend_flags restores each flag independently so one failure can't
  leave the others leaked process-wide.
- load_pipeline restores the flags (and clears the GPU cache) when the build fails after
  apply_speed_optims mutated the process-wide flags but before _state captured them for unload
  to restore -- otherwise a failed default/max load left cudnn.benchmark/TF32 on and
  contaminated later off generations.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio diffusion (Phase 4): enforce the sd-cli timeout while reading output

Iterating proc.stdout directly blocks until the stream closes, so a sd-cli that hangs
without producing output (or without closing stdout) would never reach proc.wait and the
wall-clock timeout was silently bypassed. Drain stdout on a daemon thread and wait on the
PROCESS, so the main thread always enforces the timeout and kills a hung process (which
closes the pipe and ends the reader). Add a test that times out even when stdout blocks,
and make the no-binary test hermetic so a host-installed sd-cli can't leak in.

* Studio diffusion (Phase 7) review fixes: offload fallback + bench scripts

- 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.

* Studio diffusion (Phase 4) review fixes: sd.cpp installer + engine hardening

- install_sd_cpp_prebuilt: download the release archive with urlopen + an explicit
  timeout + copyfileobj (urlretrieve has no timeout and hangs on a stalled socket);
  extract through a per-member containment check (Zip-Slip guard); expanduser the
  --install-dir so a tilde path is not taken literally; and on Windows CUDA also fetch
  the separately-published cudart runtime DLL archive so sd-cli.exe can start.
- sd_cpp_engine: find_sd_cpp_binary honors UNSLOTH_STUDIO_HOME / STUDIO_HOME like the
  installer, so a custom-root install is discovered without UNSLOTH_SD_CPP_PATH; start
  sd-cli with the parent-death child_popen_kwargs so it is not orphaned on a backend
  crash; reap the SIGKILLed child (proc.wait) so a cancel/timeout does not leave a zombie.
- tests: Zip-Slip rejection, normal extraction, studio-home discovery.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio diffusion (Phase 4) review round 2: collect sd-cli batch outputs

Codex review: when batch_count > 1, stable-diffusion.cpp's save_results() writes
the numbered files <stem>_<idx><suffix> (base_0.png, base_1.png, ...) instead of
the literal --output path. SdCppEngine.generate checked only the literal path, so
a batch generation would exit 0 and then raise 'no image' (or return a stale
file). generate now returns the literal path when present and otherwise falls
back to the numbered siblings; single-image behavior is unchanged.

Test: a fake sd-cli that writes img_0.png/img_1.png (not img.png) is collected
without error.

* Studio diffusion (Phase 6) review round 2: img2img source dims + upscale repeats

Codex review on the native engine arg builder:

- build_sd_cpp_command emitted --width/--height unconditionally, so an
  img2img/inpaint/edit run that left dims unset forced a 1024x1024 resize/crop of
  the input. width/height are now Optional (None = unset): an image-conditioned
  run (init_img or ref_images) with unset dims omits the flags so sd.cpp derives
  the size from the input image (set_width_and_height_if_unset); a plain txt2img
  run with unset dims keeps the prior 1024x1024 default; explicit dims are always
  honored. width/height are read only by the builder, so the type change is local.

- build_sd_cpp_upscale_command used a truthiness guard (params.repeats and ...)
  that silently swallowed repeats=0 into sd-cli's default of one pass, turning an
  explicit no-op into a real upscale. It now rejects repeats < 1 with ValueError
  and emits the flag for any explicit value != 1.

Tests: img2img unset dims omit width/height (init_img and ref_images), explicit
dims emitted, txt2img keeps 1024; upscale rejects repeats=0 and omits the flag at
the default. (Two pre-existing binary-discovery tests fail only because a real
sd-cli is installed in this dev environment; unrelated to this change.)

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: oobabooga <112222186+oobabooga@users.noreply.github.com>
This commit is contained in:
Daniel Han 2026-07-01 11:31:50 -07:00 committed by GitHub
commit 6b9b1c72d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1262 additions and 129 deletions

174
scripts/compare_engines.py Normal file
View file

@ -0,0 +1,174 @@
# 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())

189
scripts/compile_probe.py Normal file
View file

@ -0,0 +1,189 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""Probe: does regional ``torch.compile`` work on the GGUF diffusion transformer?
The speed layer gates ``compile_repeated_blocks`` OFF for GGUF (it dequantises
per-op). Since the backend is GGUF-only, that makes regional compile dead on
every shipping model. This probe loads a GGUF transformer exactly as
``diffusion.py`` does, runs an eager generation, then compiles the repeated
denoiser block and runs the same seed again, reporting: whether compile raised,
per-generation latency eager vs compiled, and PSNR(compiled vs eager). If compile
is clean and PSNR is high, the gate can be relaxed for this family.
Run on one CUDA GPU. Read-only w.r.t. the backend (does not import the gate).
"""
from __future__ import annotations
import argparse
import sys
import time
from pathlib import Path
import numpy as np
def _psnr(a: "np.ndarray", b: "np.ndarray") -> float:
a = a.astype(np.float64)
b = b.astype(np.float64)
mse = float(np.mean((a - b) ** 2))
if mse == 0.0:
return float("inf")
return float(10.0 * np.log10((255.0**2) / mse))
def _gen(pipe, prompt, *, steps, seed, width, height, guidance):
import torch
gen = torch.Generator(device = "cuda").manual_seed(seed)
torch.cuda.synchronize()
t0 = time.time()
image = pipe(
prompt = prompt,
width = width,
height = height,
num_inference_steps = steps,
guidance_scale = guidance,
generator = gen,
).images[0]
torch.cuda.synchronize()
return image, time.time() - t0
def main(argv = None) -> int:
p = argparse.ArgumentParser()
p.add_argument("--repo", default = "unsloth/Z-Image-Turbo-GGUF")
p.add_argument("--gguf", default = "z-image-turbo-Q4_K_M.gguf")
p.add_argument("--base-repo", default = "Tongyi-MAI/Z-Image-Turbo")
p.add_argument("--transformer-class", default = "ZImageTransformer2DModel")
p.add_argument("--pipeline-class", default = "ZImagePipeline")
p.add_argument(
"--prompt",
default = "A cinematic photograph of a red fox in a snowy forest at dawn, highly detailed",
)
p.add_argument("--steps", type = int, default = 8)
p.add_argument("--seed", type = int, default = 42)
p.add_argument("--width", type = int, default = 1024)
p.add_argument("--height", type = int, default = 1024)
p.add_argument("--guidance", type = float, default = 0.0)
p.add_argument(
"--mode", default = "default", help = "compile mode: default | max-autotune-no-cudagraphs"
)
p.add_argument(
"--dynamic", action = "store_true", help = "dynamic=True (default False here for speed)"
)
p.add_argument("--out-dir", default = "outputs/compile_probe")
args = p.parse_args(argv)
import torch
import diffusers
from huggingface_hub import hf_hub_download
out = Path(args.out_dir)
out.mkdir(parents = True, exist_ok = True)
dtype = torch.bfloat16
gguf_path = hf_hub_download(args.repo, args.gguf)
print(f"gguf: {gguf_path}", flush = True)
transformer_cls = getattr(diffusers, args.transformer_class)
transformer = transformer_cls.from_single_file(
gguf_path,
quantization_config = diffusers.GGUFQuantizationConfig(compute_dtype = dtype),
torch_dtype = dtype,
config = args.base_repo,
subfolder = "transformer",
)
pipeline_cls = getattr(diffusers, args.pipeline_class)
pipe = pipeline_cls.from_pretrained(args.base_repo, torch_dtype = dtype, transformer = transformer)
pipe.to("cuda")
print("pipeline loaded on cuda", flush = True)
# warm the eager path once (allocator / cudnn), then time eager.
_gen(
pipe,
args.prompt,
steps = args.steps,
seed = args.seed,
width = args.width,
height = args.height,
guidance = args.guidance,
)
eager_img, eager_t = _gen(
pipe,
args.prompt,
steps = args.steps,
seed = args.seed,
width = args.width,
height = args.height,
guidance = args.guidance,
)
eager_img.save(out / "eager.png")
eager_arr = np.array(eager_img)
print(f"EAGER: {eager_t:.2f}s/gen", flush = True)
# compile the repeated denoiser block.
fn = getattr(pipe.transformer, "compile_repeated_blocks", None)
if not callable(fn):
print("RESULT: transformer has no compile_repeated_blocks -> N/A", flush = True)
return 3
compile_kwargs = {"fullgraph": True, "dynamic": bool(args.dynamic)}
if args.mode and args.mode != "default":
compile_kwargs["mode"] = args.mode
print(f"compiling repeated blocks: {compile_kwargs} ...", flush = True)
try:
t0 = time.time()
fn(**compile_kwargs)
print(
f" compile_repeated_blocks() returned in {time.time()-t0:.1f}s (compilation is lazy)",
flush = True,
)
except Exception as exc: # noqa: BLE001
print(f"RESULT: compile_repeated_blocks RAISED: {type(exc).__name__}: {exc}", flush = True)
return 1
# first compiled gen triggers the actual compilation (untimed warmup).
try:
t0 = time.time()
_gen(
pipe,
args.prompt,
steps = args.steps,
seed = args.seed,
width = args.width,
height = args.height,
guidance = args.guidance,
)
print(f" first compiled gen (compilation) took {time.time()-t0:.1f}s", flush = True)
except Exception as exc: # noqa: BLE001
print(f"RESULT: first compiled generation RAISED: {type(exc).__name__}: {exc}", flush = True)
return 2
comp_img, comp_t = _gen(
pipe,
args.prompt,
steps = args.steps,
seed = args.seed,
width = args.width,
height = args.height,
guidance = args.guidance,
)
comp_img.save(out / "compiled.png")
psnr = _psnr(eager_arr, np.array(comp_img))
speedup = (eager_t - comp_t) / eager_t * 100.0
print("\n==== COMPILE PROBE RESULT ====", flush = True)
print(f" eager: {eager_t:.2f}s/gen", flush = True)
print(f" compiled: {comp_t:.2f}s/gen ({speedup:+.1f}% vs eager)", flush = True)
print(f" PSNR(compiled vs eager): {psnr:.1f} dB", flush = True)
print(
f" verdict: {'COMPILE-WORKS' if psnr >= 30 else 'COMPILE-DIVERGES'} "
f"{'FASTER' if comp_t < eager_t else 'NOT-FASTER'}",
flush = True,
)
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -216,6 +216,7 @@ def _run(args: argparse.Namespace) -> dict[str, Any]:
hf_token = os.environ.get("HF_TOKEN"),
cpu_offload = args.cpu_offload,
memory_mode = args.memory_mode,
speed_mode = args.speed_mode,
text_encoder_quant = args.text_encoder_quant,
)
_wait_for_load(backend)
@ -295,6 +296,7 @@ def _run(args: argparse.Namespace) -> dict[str, Any]:
"seed": args.seed,
"batch_size": args.batch_size,
"memory_mode": args.memory_mode,
"speed_mode": args.speed_mode,
"cpu_offload": args.cpu_offload,
"text_encoder_quant": args.text_encoder_quant,
},
@ -450,6 +452,13 @@ def _build_parser() -> argparse.ArgumentParser:
choices = ["auto", "fast", "balanced", "low_vram"],
help = "memory policy (default: backend auto)",
)
p.add_argument(
"--speed-mode",
default = None,
choices = ["off", "default", "max"],
help = "speed profile: off is bit-identical; default adds compile + "
"cudnn.benchmark (near-lossless); max also adds TF32 + fused QKV",
)
p.add_argument(
"--text-encoder-quant",
default = None,

146
scripts/leverage_probe.py Normal file
View file

@ -0,0 +1,146 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""Probe two candidate levers from the optimization research, on the real GGUF path:
* `coordinate_descent_tuning` (Inductor) -- lossless extra kernel autotuning.
* FirstBlockCache (diffusers `apply_first_block_cache`) -- step-skip cache, lossy,
evaluated at a low (8) step count where its ceiling is lower.
Each config is a fresh pipeline load (so Inductor config / compile artifacts don't
cross-contaminate). Reports latency + PSNR vs the eager reference. Run on one CUDA GPU.
"""
from __future__ import annotations
import argparse
import sys
import time
from pathlib import Path
import numpy as np
REPO = "unsloth/Z-Image-Turbo-GGUF"
GGUF = "z-image-turbo-Q4_K_M.gguf"
BASE = "Tongyi-MAI/Z-Image-Turbo"
PROMPT = "A cinematic photograph of a red fox in a snowy forest at dawn, highly detailed"
def _psnr(a, b):
mse = float(np.mean((a.astype(np.float64) - b.astype(np.float64)) ** 2))
return float("inf") if mse == 0 else float(10 * np.log10(255.0**2 / mse))
def _load():
import torch
import diffusers
from huggingface_hub import hf_hub_download
t = diffusers.ZImageTransformer2DModel.from_single_file(
hf_hub_download(REPO, GGUF),
quantization_config = diffusers.GGUFQuantizationConfig(compute_dtype = torch.bfloat16),
torch_dtype = torch.bfloat16,
config = BASE,
subfolder = "transformer",
)
pipe = diffusers.ZImagePipeline.from_pretrained(BASE, torch_dtype = torch.bfloat16, transformer = t)
pipe.to("cuda")
return pipe
def _gen(pipe, steps, seed, res):
import torch
g = torch.Generator(device = "cuda").manual_seed(seed)
torch.cuda.synchronize()
t0 = time.time()
img = pipe(
prompt = PROMPT,
width = res,
height = res,
num_inference_steps = steps,
guidance_scale = 0.0,
generator = g,
).images[0]
torch.cuda.synchronize()
return img, time.time() - t0
def main(argv = None) -> int:
p = argparse.ArgumentParser()
p.add_argument("--steps", type = int, default = 8)
p.add_argument("--res", type = int, default = 1024)
p.add_argument("--seed", type = int, default = 42)
args = p.parse_args(argv)
steps, res, seed = args.steps, args.res, args.seed
import torch
def compile_blocks(pipe, *, cdt = False):
if cdt:
import torch._inductor.config as ic
ic.coordinate_descent_tuning = True
pipe.transformer.compile_repeated_blocks(fullgraph = True, dynamic = True)
def run(
tag,
*,
compile = False,
cdt = False,
fbc = None,
):
# reset inductor config between runs
import torch._inductor.config as ic
ic.coordinate_descent_tuning = False
torch.compiler.reset()
pipe = _load()
if fbc is not None:
from diffusers.hooks import FirstBlockCacheConfig, apply_first_block_cache
apply_first_block_cache(pipe.transformer, FirstBlockCacheConfig(threshold = fbc))
if compile:
compile_blocks(pipe, cdt = cdt)
_gen(pipe, steps, seed, res) # warmup / compilation
else:
_gen(pipe, steps, seed, res) # allocator warmup
img, dt = _gen(pipe, steps, seed, res)
del pipe
torch.cuda.empty_cache()
return tag, np.array(img), dt
results = []
print(f"== leverage probe (Z-Image Q4_K_M, {res}px, {steps} steps) ==", flush = True)
_, eager, eager_t = run("eager")
print(f" eager: {eager_t:.3f}s", flush = True)
results.append(("eager", eager_t, 0.0))
for tag, kw in [
("compile(default)", dict(compile = True)),
("compile+coord_desc", dict(compile = True, cdt = True)),
("fbc0.12+compile", dict(compile = True, fbc = 0.12)),
("fbc0.20+compile", dict(compile = True, fbc = 0.20)),
("fbc0.20(no compile)", dict(fbc = 0.20)),
]:
try:
t, img, dt = run(tag, **kw)
ps = _psnr(eager, img)
results.append((tag, dt, ps))
print(
f" {tag:22s} {dt:.3f}s ({(eager_t-dt)/eager_t*100:+.0f}% vs eager) PSNR={ps:.1f} dB",
flush = True,
)
except Exception as exc: # noqa: BLE001
print(f" {tag:22s} FAILED: {type(exc).__name__}: {str(exc)[:140]}", flush = True)
print("\n==== SUMMARY ====", flush = True)
for tag, dt, ps in results:
sp = f"{(results[0][1]-dt)/results[0][1]*100:+.0f}%" if tag != "eager" else "ref"
pss = f"{ps:.1f}dB" if ps else "ref"
print(f" {tag:24s} {dt:.3f}s {sp:>6s} {pss:>8s}", flush = True)
print("LEVERAGE-PROBE-DONE", flush = True)
return 0
if __name__ == "__main__":
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "studio" / "backend"))
sys.exit(main())

165
scripts/perf_verify.py Normal file
View file

@ -0,0 +1,165 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""GPU verification for the diffusion performance pass (Phase 7).
Drives the real ``DiffusionBackend`` through several loads in one process and
checks, at a fixed seed:
1. speed: ``default`` (compile + cudnn.benchmark + channels_last) vs ``off``
-- expect a large denoise speedup at high PSNR (near-lossless).
2. the TF32-leak fix: load ``max`` (flips global TF32 / cudnn.benchmark), unload,
then load ``off`` -- the ``off`` image must be byte-identical (PSNR inf) to a
fresh ``off`` baseline, proving the globals were restored on unload.
3. ``balanced`` is now bit-identical: with VAE tiling restricted to the low tiers,
streamed (group) offload should match the resident image (PSNR inf).
Run on one CUDA GPU with the GGUF + base repo cached.
"""
from __future__ import annotations
import argparse
import sys
import time
from pathlib import Path
import numpy as np
_BACKEND_ROOT = Path(__file__).resolve().parent.parent / "studio" / "backend"
if str(_BACKEND_ROOT) not in sys.path:
sys.path.insert(0, str(_BACKEND_ROOT))
def _psnr(a: "np.ndarray", b: "np.ndarray") -> float:
a = a.astype(np.float64)
b = b.astype(np.float64)
mse = float(np.mean((a - b) ** 2))
return float("inf") if mse == 0.0 else float(10.0 * np.log10((255.0**2) / mse))
def main(argv = None) -> int:
p = argparse.ArgumentParser()
p.add_argument("--model", default = "unsloth/Z-Image-Turbo-GGUF")
p.add_argument("--gguf", default = "z-image-turbo-Q4_K_M.gguf")
p.add_argument(
"--prompt",
default = "A cinematic photograph of a red fox in a snowy forest at dawn, highly detailed",
)
p.add_argument("--steps", type = int, default = 8)
p.add_argument("--seed", type = int, default = 42)
p.add_argument("--width", type = int, default = 1024)
p.add_argument("--height", type = int, default = 1024)
p.add_argument("--out-dir", default = "outputs/perf_verify")
args = p.parse_args(argv)
import os
import torch
from core.inference.diffusion import DiffusionBackend
out = Path(args.out_dir)
out.mkdir(parents = True, exist_ok = True)
backend = DiffusionBackend()
token = os.environ.get("HF_TOKEN")
def load(mode_speed = None, mode_mem = None):
backend.begin_load(
args.model,
gguf_filename = args.gguf,
hf_token = token,
speed_mode = mode_speed,
memory_mode = mode_mem,
)
deadline = time.time() + 2400
while time.time() < deadline:
ph = backend.load_progress().get("phase")
if ph == "ready":
return backend.status()
if ph == "error":
raise RuntimeError(f"load error: {backend.load_progress()}")
time.sleep(0.5)
raise RuntimeError("load timed out")
def gen():
torch.cuda.synchronize()
t0 = time.time()
img = backend.generate(
prompt = args.prompt,
width = args.width,
height = args.height,
steps = args.steps,
guidance = 0.0,
seed = args.seed,
batch_size = 1,
)["images"][0]
torch.cuda.synchronize()
return img, time.time() - t0
def timed(
mode_speed,
*,
warmup,
iters,
mem = None,
tag = "",
):
st = load(mode_speed, mem)
for _ in range(warmup):
gen()
lats = []
img = None
for _ in range(iters):
img, dt = gen()
lats.append(dt)
img.save(out / f"{tag}.png")
backend.unload()
med = sorted(lats)[len(lats) // 2]
print(
f" [{tag}] speed={mode_speed} mem={mem} optims={st.get('speed_optims')} "
f"tiling={st.get('vae_tiling')} median={med:.3f}s",
flush = True,
)
return np.array(img), med
print("== 1. speed: off vs default ==", flush = True)
off_img, off_t = timed("off", warmup = 1, iters = 3, tag = "off")
def_img, def_t = timed("default", warmup = 1, iters = 3, tag = "default")
print(f" PSNR(default vs off) = {_psnr(off_img, def_img):.1f} dB", flush = True)
print(
f" speedup: off {off_t:.3f}s -> default {def_t:.3f}s "
f"({(off_t-def_t)/off_t*100:+.1f}%)",
flush = True,
)
print("== 2. TF32-leak fix: max then off must be byte-identical ==", flush = True)
timed("max", warmup = 0, iters = 1, tag = "max") # flips + should restore globals
off2_img, _ = timed("off", warmup = 0, iters = 1, tag = "off2")
leak_psnr = _psnr(off_img, off2_img)
print(
f" PSNR(off-after-max vs off) = {leak_psnr:.1f} dB "
f"({'OK byte-identical' if leak_psnr == float('inf') else 'LEAK! globals not restored'})",
flush = True,
)
print("== 3. balanced is bit-identical (tiling off) ==", flush = True)
bal_img, bal_t = timed("off", warmup = 0, iters = 1, mem = "balanced", tag = "balanced")
bal_psnr = _psnr(off_img, bal_img)
print(
f" PSNR(balanced vs off) = {bal_psnr:.1f} dB "
f"({'OK bit-identical' if bal_psnr == float('inf') else 'differs'})",
flush = True,
)
ok = (
(leak_psnr == float("inf"))
and (bal_psnr == float("inf")) # check 3: balanced must be bit-identical to off
and (def_t < off_t)
and (_psnr(off_img, def_img) >= 30)
)
print(f"\nPERF-VERIFY {'OK' if ok else 'CHECK'}", flush = True)
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())

View file

@ -44,7 +44,13 @@ from .diffusion_memory import (
plan_diffusion_memory,
snapshot_device_memory,
)
from .diffusion_speed import SPEED_OFF, apply_speed_optims
from .diffusion_speed import (
SPEED_OFF,
apply_speed_optims,
resolve_speed_mode,
restore_backend_flags,
snapshot_backend_flags,
)
from .diffusion_precision import quantize_text_encoders
logger = get_logger(__name__)
@ -69,6 +75,10 @@ class _LoadState:
# The opt-in speed profile (Phase 3).
speed_mode: str = SPEED_OFF
speed_optims: tuple = ()
# Process-wide torch backend flags (TF32 / cudnn.benchmark) captured before the
# speed layer mutated them, restored on unload so a later `off` load is not
# contaminated by this one's globals. None when nothing was changed.
backend_flags_before: Optional[dict] = None
# Text-encoder quantisation actually engaged: "fp8" | "nvfp4" | None (Phase 2B/2C).
text_encoder_quant: Optional[str] = None
@ -495,56 +505,77 @@ class DiffusionBackend:
pipeline_cls = getattr(diffusers, fam.pipeline_class)
pipe = pipeline_cls.from_pretrained(base, **pipe_kwargs)
# Resolve the effective speed mode: GGUF models default to the
# near-lossless `default` profile (compile is ~2.2x and sits below
# the quant noise floor), dense models stay bit-identical `off`. An
# explicit speed_mode (incl. "off") is honored verbatim.
effective_speed = resolve_speed_mode(speed_mode, is_gguf = bool(gguf_filename))
# Opt-in speed optims run BEFORE placement (channels_last / compile
# must precede CPU offload). Off by default -> bit-identical output.
speed_applied = apply_speed_optims(
pipe,
target,
is_gguf = bool(gguf_filename),
family = fam,
speed_mode = speed_mode or SPEED_OFF,
logger = logger,
)
# Quantise the dense companion text encoder(s) (opt-in fp8 / nvfp4),
# also before placement so the offload hooks move the smaller weights.
te_quant = quantize_text_encoders(
pipe,
target,
mode = text_encoder_quant,
logger = logger,
)
# must precede CPU offload). Snapshot the process-wide backend flags
# first so unload can restore them: TF32 / cudnn.benchmark are global,
# and a later `off` load must not inherit this load's settings.
backend_flags_before = snapshot_backend_flags()
# apply_speed_optims mutates PROCESS-WIDE flags (TF32 / cudnn.benchmark);
# they are only restored via _LoadState.backend_flags_before on unload. If
# the build fails after this but before _state commits (e.g. an OOM in
# apply_memory_plan / pipe.to), nothing would restore them and a later `off`
# generation would be contaminated, so restore on any non-committed exit.
committed = False
try:
speed_applied = apply_speed_optims(
pipe,
target,
is_gguf = bool(gguf_filename),
family = fam,
speed_mode = effective_speed,
logger = logger,
)
# Quantise the dense companion text encoder(s) (opt-in fp8 / nvfp4),
# also before placement so the offload hooks move the smaller weights.
te_quant = quantize_text_encoders(
pipe,
target,
mode = text_encoder_quant,
logger = logger,
)
# Decide placement from MEASURED free device memory vs the model's
# estimated resident size (transformer GGUF dequantised + the
# companion text-encoder / VAE already cached for `base`), then
# apply it. Computed here, after the build but before placement,
# because the weights are still on CPU so free VRAM is the real
# budget. `cpu_offload=True` stays an explicit override.
plan = self._plan_memory(
target, gguf_path, gguf_filename, base, fam, memory_mode, cpu_offload
)
# apply_memory_plan returns the (policy, tiling) ACTUALLY engaged (it
# may fall back to whole-module offload, and tiling is a no-op on a
# pipeline with no tiling control), so status stays honest.
effective_policy, effective_tiling = apply_memory_plan(
pipe, plan, device = device, logger = logger
)
# Decide placement from MEASURED free device memory vs the model's
# estimated resident size (transformer GGUF dequantised + the
# companion text-encoder / VAE already cached for `base`), then
# apply it. Computed here, after the build but before placement,
# because the weights are still on CPU so free VRAM is the real
# budget. `cpu_offload=True` stays an explicit override.
plan = self._plan_memory(
target, gguf_path, gguf_filename, base, fam, memory_mode, cpu_offload
)
# apply_memory_plan returns the (policy, tiling) ACTUALLY engaged (it
# may fall back to whole-module offload, and tiling is a no-op on a
# pipeline with no tiling control), so status stays honest.
effective_policy, effective_tiling = apply_memory_plan(
pipe, plan, device = device, logger = logger
)
self._state = _LoadState(
pipe = pipe,
family = fam,
repo_id = repo_id,
base_repo = base,
device = device,
dtype = str(dtype).replace("torch.", ""),
cpu_offload = effective_policy != OFFLOAD_NONE,
offload_policy = effective_policy,
vae_tiling = effective_tiling,
memory_mode = plan.requested_mode,
speed_mode = (speed_mode or SPEED_OFF),
speed_optims = tuple(k for k, v in speed_applied.items() if v),
text_encoder_quant = te_quant,
)
self._state = _LoadState(
pipe = pipe,
family = fam,
repo_id = repo_id,
base_repo = base,
device = device,
dtype = str(dtype).replace("torch.", ""),
cpu_offload = effective_policy != OFFLOAD_NONE,
offload_policy = effective_policy,
vae_tiling = effective_tiling,
memory_mode = plan.requested_mode,
speed_mode = effective_speed,
speed_optims = tuple(k for k, v in speed_applied.items() if v),
backend_flags_before = backend_flags_before,
text_encoder_quant = te_quant,
)
committed = True
finally:
if not committed:
restore_backend_flags(backend_flags_before)
clear_gpu_cache()
logger.info(
"diffusion.loaded: repo=%s base=%s device=%s offload=%s tiling=%s reasons=%s",
@ -679,7 +710,10 @@ class DiffusionBackend:
self._gen = gen
try:
images = state.pipe(**kwargs).images
# inference_mode is strictly faster than the no_grad diffusers
# uses internally and numerically identical for inference.
with torch.inference_mode():
images = state.pipe(**kwargs).images
finally:
self._gen = None
# A cancelled denoise returns early with a partial/garbage image;
@ -738,6 +772,9 @@ class DiffusionBackend:
state = self._state
if state is None:
return
# Restore the process-wide backend flags (TF32 / cudnn.benchmark) this load
# may have flipped, so the next `off` load is bit-identical again.
restore_backend_flags(state.backend_flags_before)
self._state = None
del state
clear_gpu_cache()

View file

@ -35,9 +35,9 @@ class DiffusionFamily:
# (~6.5e4) and produce inf -> NaN latents -> a black image. The backend
# promotes a resolved float16 to float32 for these at load time.
fp16_incompatible: bool = False
# False for families whose denoiser block doesn't compile cleanly with
# regional torch.compile (Z-Image). Only consulted on the non-GGUF path; the
# GGUF transformer is never compiled regardless.
# Set False only for a family whose denoiser block does not compile cleanly with
# regional torch.compile. Now consulted on the GGUF path too (compile runs on the
# GGUF transformer); all current families compile, so this stays True.
supports_torch_compile: bool = True
@ -81,8 +81,6 @@ _FAMILIES: tuple[DiffusionFamily, ...] = (
aliases = ("zimage", "z_image"),
# Z-Image's MLP down-projections peak near 9e5, which overflows float16.
fp16_incompatible = True,
# Z-Image's denoiser block is excluded from regional torch.compile.
supports_torch_compile = False,
),
)

View file

@ -400,17 +400,21 @@ def plan_diffusion_memory(
policy = OFFLOAD_MODEL
reasons.append("explicit cpu_offload overrides resident placement")
# VAE tiling/slicing decode the image in chunks, capping the decode-time spike
# that often dominates peak VRAM at high resolution. Turn it on whenever weights
# are being offloaded (the device is already tight) or the backend has no spare
# device pool (MPS/CPU). On a roomy discrete GPU it stays off so output is
# bit-identical to a plain resident run.
tile = policy != OFFLOAD_NONE or device_memory.backend in ("mps", "cpu")
# VAE savers cap the decode-time spike that dominates peak VRAM at high res.
# Slicing (decode a batch one image at a time) is EXACT, so enable it on any
# offload tier / non-discrete backend. Tiling (spatial chunks) is only bit-
# identical for a single tile (<=1MP), so restrict it to the lowest tiers where
# the VAE itself is offloaded (model / sequential) or there is no spare device
# pool (MPS / CPU). Under group offload the transformer streams but the VAE stays
# resident and fits, so it keeps exact full-image decode -> balanced is both
# faster and bit-identical. On a roomy discrete GPU both stay off.
any_offload = policy != OFFLOAD_NONE or device_memory.backend in ("mps", "cpu")
tile = policy in (OFFLOAD_MODEL, OFFLOAD_SEQUENTIAL) or device_memory.backend in ("mps", "cpu")
return MemoryPlan(
requested_mode = mode,
offload_policy = policy,
vae_tiling = tile,
vae_slicing = tile,
vae_slicing = any_offload,
device_memory = device_memory,
estimates = estimates,
reasons = tuple(reasons),
@ -442,12 +446,22 @@ def apply_memory_plan(
if plan.vae_slicing:
_enable_vae_saver(pipe, "enable_vae_slicing", "enable_slicing", logger)
def _fallback_to_model_offload() -> None:
# Group offload keeps the VAE resident, so the GROUP plan set vae_tiling=False.
# When group offload is unavailable and we drop to whole-module offload, the card
# is in the low-VRAM situation where the decode-time spike can OOM, so turn VAE
# tiling on now (if not already engaged) to cap it.
nonlocal tiling_engaged
pipe.enable_model_cpu_offload()
if not tiling_engaged:
tiling_engaged = _enable_vae_saver(pipe, "enable_vae_tiling", "enable_tiling", logger)
policy = plan.offload_policy
if policy == OFFLOAD_MODEL:
pipe.enable_model_cpu_offload()
elif policy == OFFLOAD_GROUP:
if not _apply_group_offload(pipe, device, logger):
pipe.enable_model_cpu_offload()
_fallback_to_model_offload()
policy = OFFLOAD_MODEL
elif policy == OFFLOAD_SEQUENTIAL:
try:
@ -459,7 +473,7 @@ def apply_memory_plan(
"falling back to whole-module offload",
exc,
)
pipe.enable_model_cpu_offload()
_fallback_to_model_offload()
policy = OFFLOAD_MODEL
else:
pipe.to(device)
@ -491,32 +505,40 @@ def _apply_group_offload(pipe: Any, device: str, logger: Any) -> bool:
if transformer is None:
return False
try:
import inspect
import torch
from diffusers.hooks import apply_group_offloading
onload = torch.device(device)
use_stream = onload.type == "cuda" # overlap H2D copies with compute on CUDA
# Place the smaller components resident FIRST: moving them onto the device is
# the only step here that can OOM on a tight GPU. Doing it before
# apply_group_offloading means a failure leaves NO group-offload hooks on the
# transformer, so the caller's whole-module-offload fallback gets a clean
# pipeline -- diffusers refuses enable_model_cpu_offload() while group hooks
# are attached, which would otherwise turn the fallback into a hard crash.
gkwargs: dict[str, Any] = {
"onload_device": onload,
"offload_device": torch.device("cpu"),
"offload_type": "block_level",
"num_blocks_per_group": DEFAULT_GROUP_BLOCKS,
"use_stream": use_stream,
}
# On the CUDA stream path, overlap each block's host->device copy with
# compute: non_blocking issues the copy asynchronously and record_stream
# defers the free until the copy's stream is done. Lossless (only transfer
# scheduling changes). Safe for the group tier specifically, where the
# companions stay resident; gated on the installed signature so an older
# diffusers that lacks these kwargs still works (no hard fallback).
if use_stream:
_params = inspect.signature(apply_group_offloading).parameters
if "non_blocking" in _params:
gkwargs["non_blocking"] = True
if "record_stream" in _params:
gkwargs["record_stream"] = True
apply_group_offloading(transformer, **gkwargs)
# Place the remaining (smaller) components resident; the streamed
# transformer manages its own placement via the offloading hooks.
for name, comp in getattr(pipe, "components", {}).items():
if name == "transformer":
continue
if isinstance(comp, torch.nn.Module):
comp.to(onload)
# Stream the transformer a few blocks at a time; it manages its own placement
# via the offloading hooks.
apply_group_offloading(
transformer,
onload_device = onload,
offload_device = torch.device("cpu"),
offload_type = "block_level",
num_blocks_per_group = DEFAULT_GROUP_BLOCKS,
use_stream = use_stream,
)
return True
except Exception as exc: # noqa: BLE001 — fall back to whole-module offload
if logger is not None:

View file

@ -5,19 +5,29 @@
Off by default, so the default render path stays bit-identical to a plain run (the
property the regression harness checks). When the operator opts in, this applies the
lossless-to-near-lossless speedups in the order the diffusers guides recommend
(channels_last -> regional compile, with TF32 / fused-QKV under "max"):
near-lossless speedups in the order the diffusers guides recommend
(channels_last + cudnn.benchmark -> regional compile, with TF32 / fused-QKV under
"max"):
off - nothing (default).
default - lossless: channels_last VAE memory format + regional torch.compile of
the denoiser's repeated block WHERE eligible (non-GGUF, bf16, CUDA, and
a compile-friendly family).
off - nothing (default; bit-identical reference).
default - near-lossless: channels_last VAE memory format + cudnn.benchmark conv
autotune + regional torch.compile of the denoiser's repeated block WHERE
eligible (bf16, CUDA, a compile-friendly family). Compile is the big win
(~2.3x denoise on the GGUF Z-Image transformer, PSNR ~36 dB vs eager,
well above the Q4 quantisation noise floor, so it does not meaningfully
move output quality).
max - default plus near-lossless TF32 matmul and fused QKV projections.
Regional compile is gated off for the GGUF transformer (it dequantises per-op and
doesn't compile cleanly) and for families flagged not compile-friendly (Z-Image), so
on today's GGUF path only channels_last / TF32 engage; the compile path activates
automatically once a non-GGUF bf16 transformer is loaded. torch is imported lazily.
Regional compile used to be gated off for the GGUF transformer, but it compiles and
runs faster on the current diffusers/torch (measured; the GGUF dequant ops stay
eager and the rest of the repeated block compiles), so the GGUF gate is removed; the
per-family ``supports_torch_compile`` flag and the bf16/CUDA checks still apply.
The backend flags this layer flips (TF32, cudnn.benchmark) are PROCESS-WIDE, so
``snapshot_backend_flags`` / ``restore_backend_flags`` let the caller capture the
prior values at load and restore them at unload, keeping a later ``off`` load
bit-identical instead of inheriting a previous ``max`` run's globals. torch is
imported lazily.
"""
from __future__ import annotations
@ -30,6 +40,54 @@ SPEED_MAX = "max"
SPEED_MODES = (SPEED_OFF, SPEED_DEFAULT, SPEED_MAX)
def snapshot_backend_flags() -> Optional[dict]:
"""Capture the process-wide torch backend flags this layer may mutate, so the
caller can restore them on unload. None if torch is unavailable. Each flag is read
defensively so a build/platform missing one (e.g. no cuda.matmul on CPU/MPS) still
captures the rest -- otherwise a single missing attribute would skip the whole
snapshot and a real mutated flag would leak."""
try:
import torch
except Exception: # noqa: BLE001 — no torch -> nothing to snapshot/restore
return None
state: dict[str, bool] = {}
matmul = getattr(getattr(torch.backends, "cuda", None), "matmul", None)
if matmul is not None and hasattr(matmul, "allow_tf32"):
state["matmul_tf32"] = bool(matmul.allow_tf32)
cudnn = getattr(torch.backends, "cudnn", None)
if cudnn is not None:
if hasattr(cudnn, "allow_tf32"):
state["cudnn_tf32"] = bool(cudnn.allow_tf32)
if hasattr(cudnn, "benchmark"):
state["cudnn_benchmark"] = bool(cudnn.benchmark)
return state
def restore_backend_flags(state: Optional[dict]) -> None:
"""Restore the flags captured by ``snapshot_backend_flags``. No-op on None. Each
flag is restored independently so one failure can't leave the others leaked."""
if not state:
return
try:
import torch
except Exception: # noqa: BLE001 — no torch -> nothing to restore
return
def _set(obj: Any, attr: str, key: str) -> None:
if obj is not None and key in state and hasattr(obj, attr):
try:
setattr(obj, attr, state[key])
except Exception: # noqa: BLE001 — best-effort per-flag restore
pass
_set(
getattr(getattr(torch.backends, "cuda", None), "matmul", None), "allow_tf32", "matmul_tf32"
)
cudnn = getattr(torch.backends, "cudnn", None)
_set(cudnn, "allow_tf32", "cudnn_tf32")
_set(cudnn, "benchmark", "cudnn_benchmark")
def normalize_speed_mode(value: Optional[str]) -> str:
"""Lower/strip a requested speed mode (dashes ok); None / "" -> off."""
if value is None:
@ -44,14 +102,29 @@ def normalize_speed_mode(value: Optional[str]) -> str:
return normalized
def resolve_speed_mode(value: Optional[str], *, is_gguf: bool) -> str:
"""The effective speed mode when the caller leaves it UNSET (``None``).
A GGUF model defaults to ``default``: regional compile is ~2.2x faster and its
numeric perturbation sits well below the quantisation noise floor (measured
PSNR ~37 dB compile-vs-eager versus ~21 dB Q4-vs-bf16), so it does not reduce
output quality relative to the dense reference. A dense (non-GGUF) model stays
``off`` / bit-identical, since there compile would be the only source of drift.
An explicit value -- including ``"off"`` -- is always honored verbatim."""
if value is None:
return SPEED_DEFAULT if is_gguf else SPEED_OFF
return normalize_speed_mode(value)
def compile_eligible(target: Any, *, is_gguf: bool, family: Any) -> bool:
"""Whether the denoiser's repeated block should be regionally compiled.
Only on CUDA (incl. ROCm via supports_default_torch_compile), for a non-GGUF
bf16 transformer, on a compile-friendly family. The GGUF transformer is never
compiled (it dequantises per-op)."""
if is_gguf:
return False
Only on CUDA (incl. ROCm via supports_default_torch_compile), for a bf16
transformer, on a compile-friendly family. ``is_gguf`` no longer disqualifies:
``compile_repeated_blocks`` runs fine on the GGUF transformer (the per-op
dequant stays eager, the rest of the block compiles) and is ~2.3x faster, so it
is kept only for signature/logging compatibility."""
del is_gguf # GGUF is compile-eligible now; param kept for call-site compat.
if not bool(getattr(target, "supports_default_torch_compile", False)):
return False
if not bool(getattr(family, "supports_torch_compile", True)):
@ -79,7 +152,13 @@ def apply_speed_optims(
"""Apply the opt-in speed optimisations for ``speed_mode`` to a built pipeline,
BEFORE placement / offload. Returns which optimisations actually engaged. Every
step is best-effort: a pipeline that doesn't support one is simply skipped."""
applied = {"channels_last": False, "tf32": False, "fused_qkv": False, "compiled": False}
applied = {
"channels_last": False,
"cudnn_benchmark": False,
"tf32": False,
"fused_qkv": False,
"compiled": False,
}
mode = normalize_speed_mode(speed_mode)
# TF32 is the one PROCESS-GLOBAL flag we flip (on max). Restore it whenever this
# load isn't max, so a later default/off diffusion load -- or chat inference in the
@ -93,9 +172,16 @@ def apply_speed_optims(
# Lossless: a channels-last VAE speeds up its convolutions with no numeric change.
applied["channels_last"] = _vae_channels_last(pipe, logger)
# Lossless-ish: regional compile of the repeated denoiser block, where eligible.
# Near-lossless: let cuDNN autotune the fixed-shape VAE convs (CUDA only). It may
# pick a different conv algorithm, so it is a "default"-tier (not bit-identical) win.
if getattr(target, "device", None) == "cuda":
applied["cudnn_benchmark"] = _enable_cudnn_benchmark(logger)
# Near-lossless and the largest win: regional compile of the repeated denoiser
# block, where eligible (now incl. the GGUF transformer). `max` opts into
# max-autotune (longer compile, autotuned kernels).
if compile_eligible(target, is_gguf = is_gguf, family = family):
applied["compiled"] = _compile_repeated_blocks(pipe, logger)
applied["compiled"] = _compile_repeated_blocks(pipe, logger, max_autotune = mode == SPEED_MAX)
if mode == SPEED_MAX:
# Near-lossless: TF32 matmul (CUDA only) trades a few mantissa bits for speed.
@ -119,19 +205,43 @@ def _vae_channels_last(pipe: Any, logger: Any) -> bool:
return False
def _compile_repeated_blocks(pipe: Any, logger: Any) -> bool:
def _compile_repeated_blocks(
pipe: Any,
logger: Any,
*,
max_autotune: bool = False,
) -> bool:
transformer = getattr(pipe, "transformer", None)
fn = getattr(transformer, "compile_repeated_blocks", None)
if not callable(fn):
return False
# default: mode="default" + dynamic=True -- fast cold start, robust to resolution
# changes (no recompile). max: mode="max-autotune-no-cudagraphs" + dynamic=False --
# Triton autotuning for a few % more on GEMM/conv-heavy models, at a much longer
# compile and a recompile per new resolution. The CUDA-graph modes (reduce-overhead
# / max-autotune) are deliberately NOT used: they crash on the regionally-compiled
# block because its static output buffer is overwritten across denoise steps.
kwargs: dict[str, Any] = {"fullgraph": True, "dynamic": not max_autotune}
if max_autotune:
kwargs["mode"] = "max-autotune-no-cudagraphs"
try:
fn(fullgraph = True, dynamic = True)
fn(**kwargs)
return True
except Exception as exc: # noqa: BLE001 — optimisation only
_warn(logger, "compile_repeated_blocks", exc)
return False
def _enable_cudnn_benchmark(logger: Any) -> bool:
try:
import torch
torch.backends.cudnn.benchmark = True
return True
except Exception as exc: # noqa: BLE001 — optimisation only
_warn(logger, "cudnn_benchmark", exc)
return False
# The TF32 flag values from before the first max load flipped them, so a later
# non-max load / unload can put the process back exactly as it found it (rather than
# forcing a hardcoded default that might clobber another component's choice).

View file

@ -118,6 +118,34 @@ class SdCppUpscaleParams:
tile_size: Optional[int] = None
# Native (sd.cpp) speed profiles, the engine-side analogue of diffusion_speed's
# modes. off: nothing (default). default: --diffusion-fa (flash attention; upstream
# reports it usually speeds CUDA and cuts attention memory, near-lossless). max: also
# --diffusion-conv-direct (direct conv; helps some backends, but measured +45% on
# CUDA here, so it stays opt-in/experimental, never auto-on for CUDA).
NATIVE_SPEED_OFF = "off"
NATIVE_SPEED_DEFAULT = "default"
NATIVE_SPEED_MAX = "max"
NATIVE_SPEED_MODES = (NATIVE_SPEED_OFF, NATIVE_SPEED_DEFAULT, NATIVE_SPEED_MAX)
def native_speed_flags(speed_mode: Optional[str]) -> list[str]:
"""sd-cli speed flags for a native speed mode (empty for off / None).
These are separate from the offload flags: ``--diffusion-fa`` is a speed/memory
win in its own right, not tied to whether weights are offloaded. De-duplicated
against offload flags at the call site (offload already adds ``--diffusion-fa``).
"""
mode = (speed_mode or NATIVE_SPEED_OFF).strip().lower()
if mode in ("", NATIVE_SPEED_OFF):
return []
if mode == NATIVE_SPEED_DEFAULT:
return ["--diffusion-fa"]
if mode == NATIVE_SPEED_MAX:
return ["--diffusion-fa", "--diffusion-conv-direct"]
raise ValueError(f"native speed_mode must be one of {NATIVE_SPEED_MODES}, got '{speed_mode}'")
def offload_flags(
policy: str,
*,

View file

@ -24,9 +24,11 @@ from __future__ import annotations
import logging
import os
import queue
import shutil
import subprocess
import sys
import threading
import time
from pathlib import Path
from typing import Callable, Optional
@ -37,6 +39,7 @@ from core.inference.sd_cpp_args import (
SdCppUpscaleParams,
build_sd_cpp_command,
build_sd_cpp_upscale_command,
native_speed_flags,
)
logger = logging.getLogger(__name__)
@ -189,6 +192,7 @@ class SdCppEngine:
*,
output_path: str,
offload: Optional[list[str]] = None,
native_speed: Optional[str] = None,
threads: Optional[int] = None,
verbose: bool = False,
extra_args: Optional[list[str]] = None,
@ -198,10 +202,15 @@ class SdCppEngine:
) -> Path:
"""Run one ``sd-cli`` generation; return the written image path.
Raises ``RuntimeError`` if the binary is missing, the process exits
nonzero, or no output file is produced. ``on_log`` (if given) receives
each line of sd-cli's progress output as it arrives.
``native_speed`` ("default"/"max") adds sd.cpp's own speed flags
(``--diffusion-fa`` etc.), de-duplicated against the offload flags that may
already include them. Raises ``RuntimeError`` if the binary is missing, the
process exits nonzero, or no output file is produced. ``on_log`` (if given)
receives each line of sd-cli's progress output as it arrives.
"""
offload = list(offload or [])
speed = [f for f in native_speed_flags(native_speed) if f not in offload]
merged_extra = speed + list(extra_args or [])
cmd = build_sd_cpp_command(
self._require_binary(),
files,
@ -210,7 +219,7 @@ class SdCppEngine:
offload = offload,
threads = threads,
verbose = verbose,
extra_args = extra_args,
extra_args = merged_extra,
)
return self._run(cmd, output_path, timeout = timeout, env = env, on_log = on_log)
@ -281,20 +290,49 @@ class SdCppEngine:
errors = "replace",
env = run_env,
)
# Drain stdout on a reader thread so the timeout is enforced even when the
# child hangs WITHOUT printing (e.g. stuck in model load / GPU init): a plain
# `for line in proc.stdout` blocks until EOF, so proc.wait(timeout) would
# never be reached. The reader pushes lines (then a None sentinel at EOF) to a
# queue the main loop polls against a wall-clock deadline.
tail: list[str] = []
line_q: "queue.Queue[Optional[str]]" = queue.Queue()
def _drain() -> None:
try:
assert proc.stdout is not None
for raw in proc.stdout:
line_q.put(raw.rstrip("\n"))
finally:
line_q.put(None)
reader = threading.Thread(target = _drain, daemon = True)
reader.start()
deadline = None if timeout is None else time.monotonic() + float(timeout)
stdout_done = False
try:
assert proc.stdout is not None
for line in proc.stdout:
line = line.rstrip("\n")
while True:
if deadline is not None and time.monotonic() >= deadline and proc.poll() is None:
proc.kill()
raise RuntimeError(f"sd-cli timed out after {timeout}s")
try:
line = line_q.get(timeout = 0.1)
except queue.Empty:
if proc.poll() is not None and stdout_done:
break
continue
if line is None:
stdout_done = True
if proc.poll() is not None:
break
continue
tail.append(line)
if len(tail) > 40:
tail.pop(0)
if on_log is not None:
on_log(line)
ret = proc.wait(timeout = timeout)
except subprocess.TimeoutExpired:
proc.kill()
raise RuntimeError(f"sd-cli timed out after {timeout}s")
ret = proc.wait(timeout = 5.0)
finally:
if proc.poll() is None:
proc.kill()

View file

@ -10,6 +10,7 @@ GPU, weights, or network access is needed (sub-second, CI-friendly).
from __future__ import annotations
import contextlib
import sys
import types
@ -202,6 +203,8 @@ def fake_runtime(monkeypatch):
torch.Generator = _FakeGenerator
torch.cuda = types.SimpleNamespace(is_available = lambda: False)
torch.backends = types.SimpleNamespace(mps = None)
# generate() wraps the pipe call in torch.inference_mode(); a no-op CM here.
torch.inference_mode = lambda: contextlib.nullcontext()
diffusers = types.ModuleType("diffusers")
diffusers.GGUFQuantizationConfig = lambda compute_dtype = None: ("quant", compute_dtype)
@ -856,12 +859,19 @@ def test_load_explicit_cpu_offload_engages_model_offload_on_cuda(
assert status["offload_policy"] == "model" and status["cpu_offload"] is True
def test_load_speed_mode_threads_and_defaults_off(fake_runtime, tmp_path):
# No speed_mode -> off, no optimisations engaged (the bit-identical default).
def test_load_speed_mode_gguf_auto_defaults_and_explicit(fake_runtime, tmp_path):
# No speed_mode on a GGUF model -> auto `default` (near-lossless, compile sits
# below the quant noise floor). compile itself only engages on CUDA, so on this
# CPU stub no optim need engage, but the resolved mode is `default`.
(tmp_path / "m.gguf").write_bytes(b"x")
backend = DiffusionBackend()
status = backend.load_pipeline(str(tmp_path), gguf_filename = "m.gguf", family_override = "z-image")
assert status["speed_mode"] == "off" and status["speed_optims"] == []
assert status["speed_mode"] == "default"
# An explicit "off" opts back into the bit-identical path (engages nothing).
status_off = backend.load_pipeline(
str(tmp_path), gguf_filename = "m.gguf", family_override = "z-image", speed_mode = "off"
)
assert status_off["speed_mode"] == "off" and status_off["speed_optims"] == []
# An explicit speed_mode threads through to status (engaged optims are GPU-verified).
status2 = backend.load_pipeline(
str(tmp_path), gguf_filename = "m.gguf", family_override = "z-image", speed_mode = "max"

View file

@ -185,6 +185,9 @@ def test_auto_group_offload_when_transformer_overflows_but_companions_fit():
base_overhead_mib = 1000,
)
assert plan.offload_policy == OFFLOAD_GROUP
# Group keeps the VAE resident, so it uses exact slicing but NOT lossy tiling
# -> balanced stays bit-identical while still capping the offload footprint.
assert plan.vae_slicing is True and plan.vae_tiling is False
def test_auto_model_offload_when_companions_exceed_budget():
@ -432,6 +435,18 @@ def test_apply_group_falls_back_to_model_without_transformer():
assert effective == OFFLOAD_MODEL and "model_offload" in pipe.calls
def test_apply_group_fallback_enables_vae_tiling():
# A balanced/group plan keeps the VAE resident (tiling off); when group offload can't
# engage and we drop to whole-module offload, the applier must turn VAE tiling ON to
# cap the decode-time spike on what is now a low-VRAM path.
plan = _plan(OFFLOAD_GROUP, tiling = True)
assert plan.vae_tiling is False # group plan leaves tiling off by design
pipe = _RecordingPipe() # no .transformer -> group offload falls back to model
effective, tiled = apply_memory_plan(pipe, plan, device = "cuda")
assert effective == OFFLOAD_MODEL
assert tiled is True and "vae_tiling" in pipe.calls
def test_apply_sequential_offload():
pipe = _RecordingPipe()
effective, _ = apply_memory_plan(

View file

@ -21,6 +21,9 @@ from core.inference.diffusion_speed import (
apply_speed_optims,
compile_eligible,
normalize_speed_mode,
resolve_speed_mode,
restore_backend_flags,
snapshot_backend_flags,
)
@ -47,7 +50,7 @@ def _stub_torch(monkeypatch):
torch.channels_last = "channels_last"
torch.backends = types.SimpleNamespace(
cuda = types.SimpleNamespace(matmul = types.SimpleNamespace(allow_tf32 = False)),
cudnn = types.SimpleNamespace(allow_tf32 = False),
cudnn = types.SimpleNamespace(allow_tf32 = False, benchmark = False),
)
monkeypatch.setitem(sys.modules, "torch", torch)
return torch
@ -64,23 +67,91 @@ def test_normalize_speed_mode():
normalize_speed_mode("ludicrous")
def test_resolve_speed_mode_gguf_auto_default():
# Unset (None) -> default for GGUF (near-lossless), off for dense.
assert resolve_speed_mode(None, is_gguf = True) == SPEED_DEFAULT
assert resolve_speed_mode(None, is_gguf = False) == SPEED_OFF
# An explicit value is honored verbatim, including an explicit opt-out to off.
assert resolve_speed_mode("off", is_gguf = True) == SPEED_OFF
assert resolve_speed_mode("max", is_gguf = True) == SPEED_MAX
assert resolve_speed_mode("max", is_gguf = False) == SPEED_MAX
# ── compile gating ────────────────────────────────────────────────────────────
def test_compile_eligible_requires_non_gguf_bf16_cuda_friendly(monkeypatch):
def test_compile_eligible_requires_bf16_cuda_friendly(monkeypatch):
_stub_torch(monkeypatch)
# The happy path: non-GGUF, bf16, CUDA, compile-friendly family.
# The happy path: bf16, CUDA, compile-friendly family.
assert compile_eligible(_target(), is_gguf = False, family = _family()) is True
# GGUF is never compiled.
assert compile_eligible(_target(), is_gguf = True, family = _family()) is False
# GGUF is now compile-eligible too (measured ~2.3x, PSNR ~37 dB vs eager).
assert compile_eligible(_target(), is_gguf = True, family = _family()) is True
# fp16 (non-bf16) is excluded.
assert compile_eligible(_target(dtype = "float16"), is_gguf = False, family = _family()) is False
# A family flagged not compile-friendly (Z-Image) is excluded.
# A family flagged not compile-friendly is excluded.
assert compile_eligible(_target(), is_gguf = False, family = _family(compile_ok = False)) is False
# No compile support (e.g. ROCm/XPU/MPS) is excluded.
# No compile support (e.g. XPU/MPS) is excluded.
assert compile_eligible(_target(compile_ok = False), is_gguf = False, family = _family()) is False
# ── backend-flag snapshot / restore (TF32 / cudnn.benchmark leak guard) ────────
def test_snapshot_restore_backend_flags(monkeypatch):
torch = _stub_torch(monkeypatch)
snap = snapshot_backend_flags()
assert snap == {"matmul_tf32": False, "cudnn_tf32": False, "cudnn_benchmark": False}
# An opt-in max run flips the globals on...
torch.backends.cuda.matmul.allow_tf32 = True
torch.backends.cudnn.allow_tf32 = True
torch.backends.cudnn.benchmark = True
# ...and restore puts them back, so a later `off` load is bit-identical again.
restore_backend_flags(snap)
assert torch.backends.cuda.matmul.allow_tf32 is False
assert torch.backends.cudnn.allow_tf32 is False
assert torch.backends.cudnn.benchmark is False
def test_restore_backend_flags_tolerates_none():
restore_backend_flags(None) # no torch needed, no-op
def test_snapshot_partial_when_some_backends_missing(monkeypatch):
# A build/platform without cuda.matmul (e.g. CPU/MPS) must still snapshot + restore the
# flags it does have, rather than skipping the whole snapshot on one missing attribute.
torch = types.ModuleType("torch")
torch.backends = types.SimpleNamespace(
cuda = types.SimpleNamespace(), # no .matmul
cudnn = types.SimpleNamespace(benchmark = True), # no .allow_tf32
)
monkeypatch.setitem(sys.modules, "torch", torch)
snap = snapshot_backend_flags()
assert snap == {"cudnn_benchmark": True}
torch.backends.cudnn.benchmark = False
restore_backend_flags(snap)
assert torch.backends.cudnn.benchmark is True
def test_restore_is_independent_per_flag(monkeypatch):
# A read-only / failing attribute must not abort restoring the remaining flags.
torch = _stub_torch(monkeypatch)
class _NoMatmulSet:
@property
def allow_tf32(self):
return False
@allow_tf32.setter
def allow_tf32(self, value):
raise RuntimeError("read-only on this build")
torch.backends.cuda.matmul = _NoMatmulSet()
snap = {"matmul_tf32": False, "cudnn_tf32": False, "cudnn_benchmark": False}
torch.backends.cudnn.benchmark = True
restore_backend_flags(snap) # matmul setter raises, cudnn still restored
assert torch.backends.cudnn.benchmark is False
# ── applier ───────────────────────────────────────────────────────────────────
@ -103,24 +174,33 @@ class _Pipe:
def _vae_to(self, *, memory_format):
self.vae.mem_format = memory_format
def _compile(self, *, fullgraph, dynamic):
def _compile(self, **kwargs):
self.compiled = True
self.compile_kwargs = kwargs
def _fuse(self):
self.fused = True
def test_speed_off_applies_nothing(monkeypatch):
_stub_torch(monkeypatch)
torch = _stub_torch(monkeypatch)
pipe = _Pipe(with_compile = True, with_fuse = True)
applied = apply_speed_optims(
pipe, _target(), is_gguf = False, family = _family(), speed_mode = SPEED_OFF
)
assert applied == {"channels_last": False, "tf32": False, "fused_qkv": False, "compiled": False}
assert applied == {
"channels_last": False,
"cudnn_benchmark": False,
"tf32": False,
"fused_qkv": False,
"compiled": False,
}
assert pipe.vae.mem_format is None and pipe.compiled is False
# off must not touch any process-wide flag (bit-identical reference path).
assert torch.backends.cudnn.benchmark is False
def test_speed_default_channels_last_and_compile_when_eligible(monkeypatch):
def test_speed_default_channels_last_compile_and_cudnn_benchmark(monkeypatch):
torch = _stub_torch(monkeypatch)
pipe = _Pipe(with_compile = True)
applied = apply_speed_optims(
@ -128,18 +208,36 @@ def test_speed_default_channels_last_and_compile_when_eligible(monkeypatch):
)
assert applied["channels_last"] is True and pipe.vae.mem_format == torch.channels_last
assert applied["compiled"] is True and pipe.compiled is True
# default does not flip TF32 or fuse QKV.
# default compiles with dynamic=True and no autotune mode (fast cold start,
# resolution-robust, sidesteps the CUDA-graph crash).
assert pipe.compile_kwargs == {"fullgraph": True, "dynamic": True}
# default also autotunes the VAE convs but does NOT flip TF32 or fuse QKV.
assert applied["cudnn_benchmark"] is True and torch.backends.cudnn.benchmark is True
assert applied["tf32"] is False and applied["fused_qkv"] is False
def test_speed_default_skips_compile_for_gguf(monkeypatch):
def test_speed_default_compiles_gguf(monkeypatch):
_stub_torch(monkeypatch)
pipe = _Pipe(with_compile = True)
applied = apply_speed_optims(
pipe, _target(), is_gguf = True, family = _family(), speed_mode = SPEED_DEFAULT
)
assert applied["channels_last"] is True # lossless layout still applies
assert applied["compiled"] is False and pipe.compiled is False # GGUF never compiles
assert applied["channels_last"] is True
# GGUF now compiles (the big near-lossless win).
assert applied["compiled"] is True and pipe.compiled is True
def test_speed_default_cudnn_benchmark_only_on_cuda(monkeypatch):
_stub_torch(monkeypatch)
pipe = _Pipe(with_compile = True)
applied = apply_speed_optims(
pipe,
_target(device = "mps", compile_ok = False),
is_gguf = True,
family = _family(),
speed_mode = SPEED_DEFAULT,
)
assert applied["cudnn_benchmark"] is False # not CUDA -> no autotune flip
def test_speed_max_enables_tf32_and_fused_qkv(monkeypatch):
@ -150,6 +248,9 @@ def test_speed_max_enables_tf32_and_fused_qkv(monkeypatch):
)
assert applied["tf32"] is True and torch.backends.cuda.matmul.allow_tf32 is True
assert applied["fused_qkv"] is True and pipe.fused is True
# max opts into autotuned kernels (static shapes); CUDA-graph modes are avoided.
assert pipe.compile_kwargs["mode"] == "max-autotune-no-cudagraphs"
assert pipe.compile_kwargs["dynamic"] is False
def test_speed_max_tf32_only_on_cuda(monkeypatch):

View file

@ -23,6 +23,7 @@ from core.inference.sd_cpp_args import (
SdCppUpscaleParams,
build_sd_cpp_command,
build_sd_cpp_upscale_command,
native_speed_flags,
offload_flags,
text_encoder_flags_for_family,
)
@ -47,6 +48,16 @@ def test_te_flags_by_family():
# ── offload policy -> sd-cli flags ──────────────────────────────────────────
def test_native_speed_flags():
assert native_speed_flags(None) == []
assert native_speed_flags("off") == []
assert native_speed_flags("") == []
assert native_speed_flags("default") == ["--diffusion-fa"]
assert native_speed_flags("max") == ["--diffusion-fa", "--diffusion-conv-direct"]
with pytest.raises(ValueError):
native_speed_flags("ludicrous")
def test_offload_none_is_empty():
assert offload_flags(OFFLOAD_NONE) == []

View file

@ -12,6 +12,7 @@ from __future__ import annotations
import os
import sys
import time
import types
from pathlib import Path
@ -242,6 +243,55 @@ def test_generate_raises_when_binary_missing():
)
class _HangingPopen:
"""A child that runs but never prints and never exits -- the case a plain
`for line in stdout` would block on forever, ignoring the timeout."""
def __init__(self, cmd, **_kw):
self._alive = True
class _Blocking:
def __init__(self, owner):
self.owner = owner
def __iter__(self):
return self
def __next__(self):
while self.owner._alive:
time.sleep(0.01)
raise StopIteration
@property
def stdout(self):
return self._Blocking(self)
def poll(self):
return None if self._alive else -9
def wait(self, timeout = None):
self._alive = False
return -9
def kill(self):
self._alive = False
def test_generate_times_out_on_silent_hang(tmp_path, monkeypatch):
e = _engine(tmp_path)
monkeypatch.setattr(eng.subprocess, "Popen", lambda cmd, **kw: _HangingPopen(cmd, **kw))
t0 = time.time()
with pytest.raises(RuntimeError, match = "timed out"):
e.generate(
SdCppModelFiles(diffusion_model = "/m/z.gguf"),
SdCppGenParams(prompt = "x"),
output_path = str(tmp_path / "x.png"),
timeout = 0.3,
)
# The timeout is enforced promptly (not blocked until stdout EOF).
assert time.time() - t0 < 5.0
def test_img2img_generate_passes_init_image(tmp_path, monkeypatch):
e = _engine(tmp_path)
out = tmp_path / "img.png"
@ -257,6 +307,36 @@ def test_img2img_generate_passes_init_image(tmp_path, monkeypatch):
assert str(src) == _FakePopen.captured_cmd[_FakePopen.captured_cmd.index("--init-img") + 1]
def test_generate_native_speed_dedupes_against_offload(tmp_path, monkeypatch):
e = _engine(tmp_path)
out = tmp_path / "img.png"
_patch_popen(monkeypatch, lines = ["ok"], returncode = 0, out_file = out)
# offload already adds --diffusion-fa; native_speed="default" would add it again.
e.generate(
SdCppModelFiles(diffusion_model = "/m/z.gguf"),
SdCppGenParams(prompt = "x"),
output_path = str(out),
offload = ["--offload-to-cpu", "--diffusion-fa"],
native_speed = "default",
)
# --diffusion-fa appears exactly once (de-duped), not twice.
assert _FakePopen.captured_cmd.count("--diffusion-fa") == 1
def test_generate_native_speed_adds_flag_when_not_offloaded(tmp_path, monkeypatch):
e = _engine(tmp_path)
out = tmp_path / "img.png"
_patch_popen(monkeypatch, lines = ["ok"], returncode = 0, out_file = out)
e.generate(
SdCppModelFiles(diffusion_model = "/m/z.gguf"),
SdCppGenParams(prompt = "x"),
output_path = str(out),
offload = [], # fast/resident tier: no offload, but speed flag still applies
native_speed = "default",
)
assert _FakePopen.captured_cmd.count("--diffusion-fa") == 1
def test_upscale_runs_and_returns_path(tmp_path, monkeypatch):
e = _engine(tmp_path)
out = tmp_path / "big.png"