Memory planning and dense-quant path: size a local diffusers base's resident companions from its on-disk VAE and text-encoder weights instead of folding them to zero, feed the distilled variant hint into the runtime headroom estimate so turbo and schnell models are not over-reserved, place group-offload companions resident before attaching the transformer hooks so a failed placement falls back to whole-module offload instead of crashing, and bail out of the dense transformer download before it starts when the requested quant scheme is unsupported so the load falls back to GGUF cleanly. sd.cpp stack: scrub the native path lease secret from sd-cli child env, redact native load-progress errors, forward the resolved accelerator when auto-installing a forced-native binary, release stale diffusion GPU ownership on CPU-native loads, and remove the sd.cpp install tree on uninstall. Prequant and scripts: reject prequant artifacts missing base_model_id when a base is requested, expanduser before checkpoint existence checks, record and validate the int8 exclusion filter and fp8 fast-accum in checkpoint metadata, make verify_prequant_backend allowlist its local checkpoint and fail on missing or bad LPIPS and on load-peak regressions, average only finite PSNR values in diffusion_quality, and reset the process-wide attention backend between perf probe variants. API and UI: normalize attention_backend casing before Literal validation, close hidden popovers when leaving the Images page, and clear the stale quant label when loading a direct local GGUF file.
357 lines
14 KiB
Python
357 lines
14 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
|
|
|
|
"""Tests for the native sd.cpp diffusion backend (the no-GPU engine)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import threading
|
|
import types
|
|
|
|
import pytest
|
|
from PIL import Image
|
|
|
|
from core.inference import sd_cpp_backend as bk
|
|
from core.inference.diffusion_families import detect_family
|
|
from core.inference.sd_cpp_args import SdCppGenParams, SdCppModelFiles
|
|
from core.inference.sd_cpp_backend import (
|
|
SdCppDiffusionBackend,
|
|
_map_guidance,
|
|
ensure_sd_cpp_binary,
|
|
)
|
|
from core.inference.sd_cpp_engine import SdCppCancelled
|
|
|
|
|
|
class _FakeEngine:
|
|
"""Stands in for SdCppEngine: writes a 1x1 PNG and records the args."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
fail = None,
|
|
cancel_on_call = False,
|
|
):
|
|
self.calls = []
|
|
self.fail = fail
|
|
self.cancel_on_call = cancel_on_call
|
|
|
|
def is_available(self):
|
|
return True
|
|
|
|
def version(self, **_):
|
|
return "fake sd-cli"
|
|
|
|
def generate(
|
|
self,
|
|
files,
|
|
params,
|
|
*,
|
|
output_path,
|
|
cancel_event = None,
|
|
**kw,
|
|
):
|
|
self.calls.append((files, params, output_path, kw))
|
|
if self.cancel_on_call and cancel_event is not None:
|
|
cancel_event.set()
|
|
if self.fail is not None:
|
|
raise self.fail
|
|
if cancel_event is not None and cancel_event.is_set():
|
|
raise SdCppCancelled("cancelled")
|
|
Image.new("RGB", (1, 1), (10, 20, 30)).save(output_path)
|
|
from pathlib import Path
|
|
|
|
return Path(output_path)
|
|
|
|
|
|
def _loaded_backend(fam_name = "z-image", engine = None):
|
|
b = SdCppDiffusionBackend(engine = engine or _FakeEngine())
|
|
fam = detect_family(fam_name)
|
|
b._state = bk._SdState(
|
|
repo_id = "unsloth/Z-Image-Turbo-GGUF",
|
|
base_repo = fam.base_repo,
|
|
family = fam,
|
|
device = "cpu",
|
|
files = SdCppModelFiles(
|
|
diffusion_model = "/m/z.gguf", vae = "/m/vae.safetensors", llm = "/m/llm.safetensors"
|
|
),
|
|
vae_format = fam.sd_cpp_vae_format,
|
|
sampling_method = fam.sd_cpp_sampling_method,
|
|
flow_shift = fam.sd_cpp_flow_shift,
|
|
)
|
|
return b
|
|
|
|
|
|
# ── asset resolution ──────────────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"fam_name,expect_kinds",
|
|
[
|
|
("flux.1", {"diffusion_model", "vae", "clip_l", "t5xxl"}),
|
|
("z-image", {"diffusion_model", "vae", "llm"}),
|
|
("qwen-image", {"diffusion_model", "vae", "qwen2vl"}),
|
|
("flux.2-klein", {"diffusion_model", "vae", "llm"}),
|
|
],
|
|
)
|
|
def test_asset_specs_cover_required_files(fam_name, expect_kinds):
|
|
b = SdCppDiffusionBackend(engine = _FakeEngine())
|
|
fam = detect_family(fam_name)
|
|
specs = b._asset_specs("unsloth/x-GGUF", "x-Q4_K_M.gguf", fam)
|
|
kinds = {kind for _, _, kind in specs}
|
|
assert kinds == expect_kinds
|
|
# Every spec has a non-empty repo + filename.
|
|
assert all(repo and fn for repo, fn, _ in specs)
|
|
# The transformer reuses the requested GGUF, not a registry file.
|
|
tr = [s for s in specs if s[2] == "diffusion_model"][0]
|
|
assert tr[0] == "unsloth/x-GGUF" and tr[1] == "x-Q4_K_M.gguf"
|
|
|
|
|
|
# ── guidance mapping ──────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_map_guidance_flux_uses_distilled_guidance():
|
|
cfg, g = _map_guidance(detect_family("flux.1"), 3.5)
|
|
assert cfg is None and g == 3.5
|
|
|
|
|
|
def test_map_guidance_cfg_family_off_when_distilled():
|
|
# qwen-image uses real CFG; a distilled 0 -> CFG off (1.0), a >1 value passes through.
|
|
assert _map_guidance(detect_family("qwen-image"), 0.0) == (1.0, None)
|
|
assert _map_guidance(detect_family("qwen-image"), 4.0) == (4.0, None)
|
|
|
|
|
|
# ── status ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_status_unloaded_reports_sd_cpp_engine():
|
|
b = SdCppDiffusionBackend(engine = _FakeEngine())
|
|
st = b.status()
|
|
assert st["loaded"] is False and st["engine"] == "sd_cpp"
|
|
|
|
|
|
def test_status_loaded_shape():
|
|
b = _loaded_backend()
|
|
st = b.status()
|
|
assert st["loaded"] is True
|
|
assert st["engine"] == "sd_cpp"
|
|
assert st["family"] == "z-image"
|
|
assert st["device"] == "cpu"
|
|
# diffusers-only fields are present (route response parity) but null.
|
|
for k in ("transformer_quant", "attention_backend", "transformer_cache", "text_encoder_quant"):
|
|
assert st[k] is None
|
|
|
|
|
|
# ── generate ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_generate_returns_images_and_seed():
|
|
eng = _FakeEngine()
|
|
b = _loaded_backend(engine = eng)
|
|
out = b.generate(prompt = "a fox", width = 64, height = 64, steps = 8, seed = 123, batch_size = 2)
|
|
assert out["seed"] == 123
|
|
assert out["repo_id"] == "unsloth/Z-Image-Turbo-GGUF"
|
|
assert len(out["images"]) == 2
|
|
assert all(isinstance(im, Image.Image) for im in out["images"])
|
|
# One sd-cli run per batch image, each a distinct seed from the base.
|
|
assert len(eng.calls) == 2
|
|
seeds = [params.seed for _, params, _, _ in eng.calls]
|
|
assert seeds == [123, 124]
|
|
# The per-image seeds are returned so the route can persist each one.
|
|
assert out["seeds"] == [123, 124]
|
|
|
|
|
|
def test_generate_qwen_passes_sampling_args():
|
|
eng = _FakeEngine()
|
|
b = _loaded_backend(fam_name = "qwen-image", engine = eng)
|
|
b.generate(prompt = "x", steps = 20, guidance = 4.0, seed = 1)
|
|
_, params, _, kw = eng.calls[0]
|
|
assert params.sampling_method == "euler" # Qwen's supported sd.cpp sampler
|
|
assert "--flow-shift" in (kw.get("extra_args") or [])
|
|
|
|
|
|
def test_generate_raises_when_not_loaded():
|
|
b = SdCppDiffusionBackend(engine = _FakeEngine())
|
|
with pytest.raises(RuntimeError, match = "No diffusion model is loaded"):
|
|
b.generate(prompt = "x")
|
|
|
|
|
|
def test_generate_passes_vae_format_for_flux2():
|
|
eng = _FakeEngine()
|
|
b = _loaded_backend(fam_name = "flux.2-klein", engine = eng)
|
|
b.generate(prompt = "x", steps = 4, seed = 1)
|
|
_, _, _, kw = eng.calls[0]
|
|
assert kw.get("extra_args") == ["--vae-format", "flux2"]
|
|
|
|
|
|
def test_generate_cancellation_raises_cancelled_not_failure():
|
|
# The engine cancels mid-run; the backend surfaces a cancellation, not a crash.
|
|
eng = _FakeEngine(cancel_on_call = True)
|
|
b = _loaded_backend(engine = eng)
|
|
with pytest.raises(RuntimeError, match = "cancelled"):
|
|
b.generate(prompt = "x", steps = 8, seed = 5)
|
|
|
|
|
|
def test_generate_progress_tracks_parsed_steps():
|
|
b = _loaded_backend()
|
|
b._gen = bk._SdGen(total_steps = 8)
|
|
b._on_log(" sampling 4/8 done")
|
|
p = b.generate_progress()
|
|
assert p["active"] is True and p["step"] == 4 and p["total_steps"] == 8
|
|
# A fraction with a different denominator must not move the bar.
|
|
b._on_log("loaded 1/3 tensors")
|
|
assert b.generate_progress()["step"] == 4
|
|
|
|
|
|
# ── load validation + binary install ──────────────────────────────────────────
|
|
|
|
|
|
def test_begin_load_rejects_unsupported_family(monkeypatch):
|
|
b = SdCppDiffusionBackend(engine = _FakeEngine())
|
|
# A family with no native asset mapping must be rejected (router falls back).
|
|
monkeypatch.setattr(bk, "family_sd_cpp_supported", lambda fam: False)
|
|
with pytest.raises(ValueError, match = "no native sd.cpp asset mapping"):
|
|
b.begin_load("unsloth/Z-Image-Turbo-GGUF", gguf_filename = "z.gguf")
|
|
|
|
|
|
def test_begin_load_requires_gguf_filename():
|
|
b = SdCppDiffusionBackend(engine = _FakeEngine())
|
|
with pytest.raises(ValueError, match = "gguf_filename is required"):
|
|
b.begin_load("unsloth/Z-Image-Turbo-GGUF")
|
|
|
|
|
|
def test_begin_load_resolves_family_from_filename_only(monkeypatch):
|
|
# A local .gguf pick whose family keyword lives only in the basename (parent dir
|
|
# carries none) must resolve via the same filename fallback the route validated
|
|
# with -- not dead-end with "Could not infer" on a native (no-GPU) host.
|
|
b = SdCppDiffusionBackend(engine = _FakeEngine())
|
|
monkeypatch.setattr(b, "_run_load", lambda **kwargs: None) # skip the download thread
|
|
b.begin_load("/models/gguf-store", gguf_filename = "Z-Image-Turbo-Q4_K_M.gguf")
|
|
# Validation passed (no ValueError) and the family was inferred from the filename.
|
|
assert b._loading is not None and b._loading.repo_id == "/models/gguf-store"
|
|
|
|
|
|
def test_ensure_binary_returns_found(monkeypatch):
|
|
monkeypatch.setattr(bk, "find_sd_cpp_binary", lambda: "/usr/bin/sd-cli")
|
|
assert ensure_sd_cpp_binary() == "/usr/bin/sd-cli"
|
|
|
|
|
|
def test_ensure_binary_install_disabled_returns_none(monkeypatch):
|
|
monkeypatch.setattr(bk, "find_sd_cpp_binary", lambda: None)
|
|
assert ensure_sd_cpp_binary(allow_install = False) is None
|
|
|
|
|
|
def test_unload_clears_state_and_signals_cancel():
|
|
cancel = threading.Event()
|
|
b = _loaded_backend()
|
|
b._active_generate_cancel = cancel
|
|
st = b.unload()
|
|
assert st["loaded"] is False
|
|
assert cancel.is_set()
|
|
assert b._cancel_event.is_set()
|
|
|
|
|
|
def test_status_reports_offload_when_flags_active():
|
|
# status must reflect the offload flags actually passed to sd-cli, not always "none",
|
|
# so a balanced/low_vram (or cpu_offload) load is verifiable.
|
|
b = _loaded_backend()
|
|
# No flags (CPU default) -> none.
|
|
assert b.status()["offload_policy"] == "none" and b.status()["cpu_offload"] is False
|
|
# Flags present (off-CPU offload) -> reported active.
|
|
s = b._state
|
|
b._state = bk._SdState(
|
|
repo_id = s.repo_id,
|
|
base_repo = s.base_repo,
|
|
family = s.family,
|
|
device = "cuda",
|
|
files = s.files,
|
|
offload_flags = ("--vae-on-cpu", "--clip-on-cpu"),
|
|
)
|
|
st = b.status()
|
|
assert st["cpu_offload"] is True and st["offload_policy"] == "active"
|
|
|
|
|
|
def test_run_load_cancels_and_waits_for_inflight_generation(monkeypatch):
|
|
# A generation that started during the asset download is still running against the OLD
|
|
# model. _run_load must cancel it AND wait on _generate_lock before committing the new
|
|
# state, or a stale sd-cli run finishes afterward and persists an image from the previous
|
|
# model once the new load reports ready.
|
|
b = SdCppDiffusionBackend(engine = _FakeEngine())
|
|
fam = detect_family("z-image")
|
|
monkeypatch.setattr(b, "_asset_specs", lambda *a, **k: [])
|
|
monkeypatch.setattr(b, "_set_expected_bytes", lambda *a, **k: None)
|
|
monkeypatch.setattr(
|
|
b,
|
|
"_fetch_assets",
|
|
lambda *a, **k: {"diffusion_model": "/m/z.gguf", "vae": "/m/vae.sft", "llm": "/m/llm.sft"},
|
|
)
|
|
# Avoid importing torch from the worker thread (its first import deadlocks off the main
|
|
# thread -- a test artifact, not a production path); the device only needs to be CPU here.
|
|
monkeypatch.setattr(
|
|
bk, "resolve_diffusion_device_target", lambda: types.SimpleNamespace(device = "cpu")
|
|
)
|
|
|
|
b._load_token = 5
|
|
cancel = threading.Event()
|
|
b._active_generate_cancel = cancel # a generation is "in flight"
|
|
|
|
committed = threading.Event()
|
|
|
|
def _load():
|
|
b._run_load(
|
|
repo_id = "unsloth/Z-Image-Turbo-GGUF",
|
|
gguf_filename = "z.gguf",
|
|
base = fam.base_repo,
|
|
fam = fam,
|
|
hf_token = None,
|
|
_load_token = 5,
|
|
)
|
|
committed.set()
|
|
|
|
b._generate_lock.acquire() # simulate the live denoise holding _generate_lock
|
|
try:
|
|
threading.Thread(target = _load, daemon = True).start()
|
|
# The commit must block behind the live generation and not publish the new state,
|
|
# but must already have signalled the in-flight cancel.
|
|
assert not committed.wait(0.5)
|
|
assert b._state is None
|
|
assert cancel.is_set()
|
|
finally:
|
|
b._generate_lock.release()
|
|
assert committed.wait(5) # only now does the commit run
|
|
assert b._state is not None and b._state.repo_id == "unsloth/Z-Image-Turbo-GGUF"
|
|
|
|
|
|
def test_run_load_redacts_paths_in_progress_error(monkeypatch):
|
|
# A load failure surfaced via load_progress() must run through redact_native_paths, the
|
|
# same scrub the diffusers load path applies, so a registered native path can't leak.
|
|
from utils import native_path_leases as npl
|
|
|
|
secret_root = "/managed/native/root"
|
|
npl._remember_native_path_for_redaction(secret_root, "model dir")
|
|
try:
|
|
b = SdCppDiffusionBackend(engine = _FakeEngine())
|
|
fam = detect_family("z-image")
|
|
monkeypatch.setattr(b, "_asset_specs", lambda *a, **k: [])
|
|
monkeypatch.setattr(b, "_set_expected_bytes", lambda *a, **k: None)
|
|
|
|
def _boom(*a, **k):
|
|
raise RuntimeError(f"failed to read {secret_root}/z.gguf")
|
|
|
|
monkeypatch.setattr(b, "_fetch_assets", _boom)
|
|
|
|
b._load_token = 1
|
|
b._loading = bk._SdLoading(repo_id = "unsloth/Z-Image-Turbo-GGUF", base_repo = fam.base_repo)
|
|
b._run_load(
|
|
repo_id = "unsloth/Z-Image-Turbo-GGUF",
|
|
gguf_filename = "z.gguf",
|
|
base = fam.base_repo,
|
|
fam = fam,
|
|
hf_token = None,
|
|
_load_token = 1,
|
|
)
|
|
err = b.load_progress()["error"]
|
|
assert err and secret_root not in err and "<native_path>" in err
|
|
finally:
|
|
with npl._REDACTION_LOCK:
|
|
if secret_root in npl._NATIVE_PATH_REDACTIONS:
|
|
npl._NATIVE_PATH_REDACTIONS.remove(secret_root)
|