unsloth/scripts/compile_probe.py
Daniel Han 395816cf7e 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.
2026-06-26 03:18:44 +00:00

138 lines
6.1 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
"""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())