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.
114 lines
4.6 KiB
Python
114 lines
4.6 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
|
|
"""End-to-end smoke for the native stable-diffusion.cpp engine.
|
|
|
|
Drives the real ``SdCppEngine`` over a built ``sd-cli`` and a set of split GGUF
|
|
assets (the same the diffusers path consumes), running one txt2img generation
|
|
and reporting wall time. This is the GPU/native analogue of
|
|
``scripts/diffusion_bench.py``: it proves the engine wiring (finder -> command
|
|
builder -> subprocess -> output PNG) works against real weights.
|
|
|
|
Example (Z-Image-Turbo on one GPU):
|
|
|
|
SD_CLI_PATH=.../sd-cli CUDA_VISIBLE_DEVICES=6 python scripts/sd_cpp_smoke.py \\
|
|
--family z-image \\
|
|
--diffusion-model .../z-image-turbo-Q4_K_M.gguf \\
|
|
--vae .../ae.safetensors \\
|
|
--llm .../Qwen3-4B-Instruct-2507-Q4_K_M.gguf \\
|
|
--memory-mode balanced --steps 8 --cfg-scale 1.0 --width 512 --height 512
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
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))
|
|
|
|
from core.inference.diffusion_memory import ( # noqa: E402
|
|
MEMORY_MODE_BALANCED,
|
|
MEMORY_MODE_FAST,
|
|
MEMORY_MODE_LOW_VRAM,
|
|
OFFLOAD_GROUP,
|
|
OFFLOAD_MODEL,
|
|
OFFLOAD_NONE,
|
|
)
|
|
from core.inference.sd_cpp_args import ( # noqa: E402
|
|
SdCppGenParams,
|
|
SdCppModelFiles,
|
|
offload_flags,
|
|
)
|
|
from core.inference.sd_cpp_engine import SdCppEngine, find_sd_cpp_binary # noqa: E402
|
|
|
|
# memory-mode (user knob) -> sd.cpp offload policy, matching the diffusers planner.
|
|
_MODE_TO_POLICY = {
|
|
MEMORY_MODE_FAST: OFFLOAD_NONE,
|
|
MEMORY_MODE_BALANCED: OFFLOAD_GROUP,
|
|
MEMORY_MODE_LOW_VRAM: OFFLOAD_MODEL,
|
|
}
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
p = argparse.ArgumentParser(description = "Native sd-cli engine smoke test.")
|
|
p.add_argument("--binary", default = None, help = "sd-cli path (else env / finder)")
|
|
p.add_argument("--family", default = "z-image")
|
|
p.add_argument("--diffusion-model", required = True)
|
|
p.add_argument("--vae", default = None)
|
|
p.add_argument("--clip_l", default = None)
|
|
p.add_argument("--t5xxl", default = None)
|
|
p.add_argument("--llm", default = None)
|
|
p.add_argument("--qwen2vl", default = None)
|
|
p.add_argument("--prompt", default = "A cinematic photograph of a red fox in a snowy forest at dawn, highly detailed")
|
|
p.add_argument("--negative-prompt", default = None)
|
|
p.add_argument("--width", type = int, default = 512)
|
|
p.add_argument("--height", type = int, default = 512)
|
|
p.add_argument("--steps", type = int, default = 8)
|
|
p.add_argument("--cfg-scale", type = float, default = 1.0)
|
|
p.add_argument("--seed", type = int, default = 42)
|
|
p.add_argument("--memory-mode", default = "balanced", choices = list(_MODE_TO_POLICY))
|
|
p.add_argument("--out-image", default = "outputs/sdcpp_verify/sdcpp_smoke.png")
|
|
p.add_argument("--timeout", type = float, default = 1800.0)
|
|
args = p.parse_args(argv)
|
|
|
|
binary = args.binary or find_sd_cpp_binary()
|
|
engine = SdCppEngine(binary = binary)
|
|
print(f"binary: {engine.binary}", flush = True)
|
|
print(f"available: {engine.is_available()}", flush = True)
|
|
print(f"version: {engine.version()}", flush = True)
|
|
if not engine.is_available():
|
|
print("ERROR: sd-cli not found (set --binary / SD_CLI_PATH / UNSLOTH_SD_CPP_PATH).", flush = True)
|
|
return 2
|
|
|
|
files = SdCppModelFiles(
|
|
diffusion_model = args.diffusion_model, vae = args.vae,
|
|
clip_l = args.clip_l, t5xxl = args.t5xxl, llm = args.llm, qwen2vl = args.qwen2vl,
|
|
)
|
|
params = SdCppGenParams(
|
|
prompt = args.prompt, negative_prompt = args.negative_prompt,
|
|
width = args.width, height = args.height, steps = args.steps,
|
|
cfg_scale = args.cfg_scale, seed = args.seed,
|
|
)
|
|
policy = _MODE_TO_POLICY[args.memory_mode]
|
|
off = offload_flags(policy)
|
|
print(f"memory: {args.memory_mode} -> policy={policy} -> flags={off}", flush = True)
|
|
|
|
out = Path(args.out_image)
|
|
t0 = time.time()
|
|
result = engine.generate(
|
|
files, params, output_path = str(out), offload = off, verbose = True,
|
|
timeout = args.timeout, on_log = lambda ln: print(f" [sd] {ln}", flush = True),
|
|
)
|
|
dt = time.time() - t0
|
|
size_kb = result.stat().st_size / 1024 if result.is_file() else 0
|
|
print(f"\nOK: generated {result} ({size_kb:.0f} KB) in {dt:.1f}s", flush = True)
|
|
print("SD-CPP-SMOKE-OK", flush = True)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|