unsloth/studio/install_sd_cpp_prebuilt.py
Daniel Han eee191d30e Tighten diffusion comments (second pass)
Collapse the remaining multi-line comment blocks in the video page, training routes and service, sd.cpp server and installer, memory and speed planners, and the shared request models. Comments only, no code or behaviour changes.
2026-07-27 12:01:44 +00:00

491 lines
22 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
"""Install a prebuilt ``sd-cli`` (stable-diffusion.cpp) for the native diffusion
engine.
The chat backend ships a prebuilt llama-server; this is the diffusion analogue,
kept deliberately small. stable-diffusion.cpp publishes per-platform release
zips (macOS-arm64/Metal, Linux x86_64 CPU, plus Vulkan / ROCm / Windows
variants), so on the Phase-4 targets (Apple Silicon and CPU) there is nothing to
compile: resolve the right asset, download, extract into
``~/.unsloth/stable-diffusion.cpp``, and the engine's finder picks it up.
``resolve_release_asset`` -- the host -> asset choice -- is a pure function so the
matching matrix is unit-tested without any network. CUDA / ROCm / XPU hosts stay
on diffusers and never need this; it exists for the engines diffusers serves
poorly.
Usage:
python studio/install_sd_cpp_prebuilt.py # auto-detect host
python studio/install_sd_cpp_prebuilt.py --accelerator vulkan
python studio/install_sd_cpp_prebuilt.py --print-asset # resolve only
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import platform
import shutil
import stat
import sys
import urllib.error
import urllib.request
import zipfile
from pathlib import Path
from typing import Optional, Sequence
# Default source: the Unsloth-built mirror, whose CPU/Apple prebuilts are published by unslothai/stable-diffusion.cpp (like unslothai/llama.cpp ships its prebuilts). Override with UNSLOTH_SD_CPP_REPO to point elsewhere (e.g. back to leejet).
# GPU hosts never reach here (they run diffusers), so only CPU/Apple assets are needed.
DEFAULT_REPO = "unslothai/stable-diffusion.cpp"
# Upstream we fall back to if the mirror cannot serve this host (mirror release missing, or a host we do not yet build): resolve against leejet so native install still works.
UPSTREAM_FALLBACK_REPO = "leejet/stable-diffusion.cpp"
# Pinned release tag for REPRODUCIBILITY: "releases/latest" silently swaps the binary under users on every push. Override with UNSLOTH_SD_CPP_TAG; set it empty to track latest. If the pinned tag is gone, install falls back to that repo latest.
DEFAULT_TAG = "master-741-484baa4"
# Back-compat alias (some callers/tests import REPO).
REPO = DEFAULT_REPO
def _repo() -> str:
return (os.environ.get("UNSLOTH_SD_CPP_REPO") or DEFAULT_REPO).strip() or DEFAULT_REPO
def _pinned_tag() -> Optional[str]:
"""The release tag to install: env override, else the pinned default; '' = latest."""
val = os.environ.get("UNSLOTH_SD_CPP_TAG", DEFAULT_TAG).strip()
return val or None
# accelerator -> the token that must appear in a Linux/Windows asset name.
_LINUX_ACCEL_TOKEN = {"rocm": "rocm", "vulkan": "vulkan"}
_WINDOWS_ACCEL_TOKEN = {
"cuda": "cuda12",
"vulkan": "vulkan",
"rocm": "rocm",
"cpu": "avx2",
"auto": "avx2",
}
# Tokens that mark an accelerator-specific Linux build; "auto"/"cpu" want none of them.
_LINUX_ACCEL_MARKERS = ("rocm", "vulkan", "cuda", "sycl", "musa")
_ARCH_TOKENS = {
"x86_64": ("x86_64", "x64", "amd64"),
"amd64": ("x86_64", "x64", "amd64"),
"arm64": ("arm64", "aarch64"),
"aarch64": ("arm64", "aarch64"),
}
def _arch_tokens(machine: str) -> tuple[str, ...]:
return _ARCH_TOKENS.get(machine.lower(), (machine.lower(),))
def resolve_release_asset(
asset_names: Sequence[str],
*,
system: str,
machine: str,
accelerator: str = "auto",
) -> Optional[str]:
"""Pick the best release asset for a host, or None if none matches.
``system`` / ``machine`` are ``platform.system()`` / ``platform.machine()``
values; ``accelerator`` is ``auto`` (CPU/Metal default), ``vulkan``,
``rocm``, or ``cuda`` (Windows only). Pure -- the caller passes the release's
asset name list.
"""
system = system.lower()
accel = accelerator.lower()
arch = _arch_tokens(machine)
zips = [
a for a in asset_names if a.lower().endswith(".zip") and not a.lower().startswith("cudart")
]
if system == "darwin":
pool = [
a
for a in zips
if ("darwin" in a.lower() or "macos" in a.lower()) and any(t in a.lower() for t in arch)
]
return pool[0] if pool else None
if system == "windows":
# Filter by host architecture the same way Darwin and Linux do. Without it a Windows arm64 host matched the x64 assets, installed one, and only failed later when the extracted x64 sd-cli could not run.
# No compatible build is a real miss: return None so the caller falls back rather than installing something that cannot execute.
pool = [a for a in zips if "bin-win" in a.lower() and any(t in a.lower() for t in arch)]
token = _WINDOWS_ACCEL_TOKEN.get(accel, accel)
sel = [a for a in pool if token in a.lower()]
if sel:
return sel[0]
# An EXPLICIT GPU accelerator with no matching asset is a real miss, not a CPU request: return None so the caller can fall back to a repo that builds it, rather than installing a CPU build for a --accelerator cuda request.
if accel in ("cuda", "vulkan", "rocm"):
return None
# auto / cpu -> a plain avx2 CPU build, else any windows build.
cpu = [a for a in pool if "avx2" in a.lower()]
return cpu[0] if cpu else (pool[0] if pool else None)
# linux (and anything else unix-like)
pool = [a for a in zips if "linux" in a.lower() and any(t in a.lower() for t in arch)]
if accel in ("cuda", "vulkan", "rocm"):
# Explicit GPU accelerator: require its marker, else no match (let the caller fall back) -- never hand back a plain CPU build for a GPU request.
marker = _LINUX_ACCEL_TOKEN.get(accel, accel)
sel = [a for a in pool if marker in a.lower()]
else: # auto / cpu -> the plain build with no accelerator marker
sel = [a for a in pool if not any(m in a.lower() for m in _LINUX_ACCEL_MARKERS)]
return sel[0] if sel else None
def _fetch_release(
tag: Optional[str] = None,
*,
repo: Optional[str] = None,
token: Optional[str] = None,
timeout: float = 30.0,
allow_latest: bool = True,
) -> Optional[dict]:
"""GET a release JSON from GitHub. With ``tag`` set, fetch that exact release; otherwise
fetch latest. ``token`` is optional and lifts the API rate limit.
When the pinned ``tag`` is missing (404): if ``allow_latest`` fall back to that repo's
latest, else return ``None`` so the caller can try the SAME pin on another repo before
settling for any repo's unpinned latest."""
repo = repo or _repo()
token = token or os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN")
def _get(url: str) -> dict:
req = urllib.request.Request(url, headers = {"Accept": "application/vnd.github+json"})
if token:
req.add_header("Authorization", f"Bearer {token}")
with urllib.request.urlopen(req, timeout = timeout) as resp: # noqa: S310 (fixed https host)
return json.loads(resp.read().decode("utf-8"))
base = f"https://api.github.com/repos/{repo}/releases"
if tag:
try:
return _get(f"{base}/tags/{tag}")
except urllib.error.HTTPError as exc: # pinned tag removed -> maybe latest
if exc.code != 404:
raise
if not allow_latest:
return None
print(
f"sd-cli: pinned tag {tag} not found on {repo}; falling back to latest", flush = True
)
return _get(f"{base}/latest")
# Back-compat alias: the old name fetched latest.
def _fetch_latest_release(*, token: Optional[str] = None, timeout: float = 30.0) -> dict:
return _fetch_release(None, token = token, timeout = timeout)
def _verify_sha256(path: Path, expected_digest: Optional[str]) -> None:
"""Verify ``path`` against a GitHub asset ``digest`` ('sha256:<hex>'). Integrity check
against a corrupted/tampered download before we extract + execute the binary. When the
release publishes no digest (older releases), warn and proceed rather than hard-fail."""
if not expected_digest:
print(f"sd-cli: WARNING no digest for {path.name}; cannot verify integrity", flush = True)
return
algo, _, want = expected_digest.partition(":")
if algo.lower() != "sha256" or not want:
print(
f"sd-cli: WARNING unrecognised digest {expected_digest!r}; skipping check", flush = True
)
return
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1 << 20), b""):
h.update(chunk)
got = h.hexdigest()
if got != want.lower():
raise RuntimeError(f"sha256 mismatch for {path.name}: expected {want.lower()}, got {got}")
def default_install_dir() -> Path:
"""``~/.unsloth/stable-diffusion.cpp`` (or under ``UNSLOTH_STUDIO_HOME`` /
``STUDIO_HOME`` if set), the sibling of the llama.cpp install the finder
probes."""
home = os.environ.get("UNSLOTH_STUDIO_HOME") or os.environ.get("STUDIO_HOME")
base = Path(home).parent if home else Path.home() / ".unsloth"
return base / "stable-diffusion.cpp"
def _make_executable(path: Path) -> None:
mode = path.stat().st_mode
path.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
def _locate_sd_cli(root: Path) -> Optional[Path]:
name = "sd-cli.exe" if sys.platform == "win32" else "sd-cli"
for p in root.rglob(name):
if p.is_file():
return p
return None
def _locate_sd_server(root: Path) -> Optional[Path]:
"""The persistent ``sd-server`` binary in the extracted tree, if the archive ships
one (modern stable-diffusion.cpp releases do). Best-effort: the native backend
falls back to one-shot ``sd-cli`` when it is absent."""
name = "sd-server.exe" if sys.platform == "win32" else "sd-server"
for p in root.rglob(name):
if p.is_file():
return p
return None
def _download(
url: str,
dest: Path,
*,
timeout: float = 300.0,
) -> None:
"""Stream ``url`` to ``dest`` with an explicit timeout. ``urlretrieve`` takes no
timeout and can hang forever on a stalled socket. A User-Agent is set because the
GitHub asset CDN can reject header-less requests; the API fetch carries any token."""
import shutil
req = urllib.request.Request(url, headers = {"User-Agent": "unsloth-sd-cpp-installer"})
with urllib.request.urlopen(req, timeout = timeout) as resp, open(dest, "wb") as f: # noqa: S310
shutil.copyfileobj(resp, f)
def _safe_extractall(zf: zipfile.ZipFile, target: Path) -> None:
"""``extractall`` with a per-member containment check, so an archive carrying an
absolute path or a ``..`` entry can't write outside ``target`` (Zip-Slip)."""
base = target.resolve()
for member in zf.infolist():
dest = (base / member.filename).resolve()
if dest != base and base not in dest.parents:
raise RuntimeError(f"unsafe path in archive: {member.filename!r}")
zf.extractall(target)
def _maybe_fetch_windows_cudart(release: dict, chosen: str, target: Path) -> None:
"""On Windows + a CUDA build, also fetch the separate CUDA-runtime DLL archive.
Upstream ships the runtime as ``cudart-sd-...-win-cu12-...zip`` (which
``resolve_release_asset`` filters out); without those DLLs ``sd-cli.exe`` cannot start
on a machine that does not already have the CUDA runtime installed."""
if platform.system().lower() != "windows" or "cuda" not in chosen.lower():
return
cudart = next(
(
a
for a in release.get("assets", [])
if a["name"].lower().startswith("cudart") and "win" in a["name"].lower()
),
None,
)
if cudart is None:
return
dest = target / cudart["name"]
print(f"downloading CUDA runtime {cudart['name']} ...", flush = True)
try:
_download(cudart["browser_download_url"], dest)
# Verify integrity BEFORE extracting, like the main sd-cli archive: these DLLs load into sd-cli.exe at runtime, so a corrupt/tampered runtime archive must be rejected rather than extracted next to the binary.
_verify_sha256(dest, cudart.get("digest"))
with zipfile.ZipFile(dest) as zf:
_safe_extractall(zf, target)
finally:
dest.unlink(missing_ok = True)
def _resolve_repo_asset(
repo: str,
tag: Optional[str],
accelerator: str,
token: Optional[str],
*,
allow_latest: bool = True,
) -> tuple[Optional[dict], Optional[str]]:
"""Fetch ``repo``'s release and pick the asset for this host. Returns
``(release, asset_name)`` or ``(None, None)`` when the repo has no usable release
(fetch failed, or the pinned tag is missing and ``allow_latest`` is False) or no
asset for this host, so the caller can fall back."""
try:
release = _fetch_release(tag, repo = repo, token = token, allow_latest = allow_latest)
except Exception as exc: # noqa: BLE001 - network / rate limit -> fall back
print(f"sd-cli: {repo} release fetch failed ({exc})", flush = True)
return None, None
if release is None: # pinned tag missing and the latest fallback was withheld
return None, None
names = [a["name"] for a in (release.get("assets") or [])]
chosen = resolve_release_asset(
names,
system = platform.system(),
machine = platform.machine(),
accelerator = accelerator,
)
return release, chosen
def _resolve_with_fallback(
accelerator: str, token: Optional[str]
) -> tuple[str, Optional[dict], Optional[str]]:
"""Resolve ``(used_repo, release, asset_name)`` for this host across the primary repo
and -- only when the built-in default is in use and the user did not pin a repo -- the
upstream fallback.
Ordering guarantees reproducibility: a pinned tag is tried EXACTLY on every candidate
repo before any repo's unpinned latest, so a mirror that is missing the pinned release
prefers the pinned upstream build over an unpinned mirror-latest. Returns
``(primary, None, None)`` when nothing serves this host. Shared by ``install`` and
``--print-asset`` so both honour the same fallback."""
tag = _pinned_tag()
primary = _repo()
# Fall back to upstream ONLY when the user did not pin a repo (env unset) and the built-in default is in use: an explicit UNSLOTH_SD_CPP_REPO (even one equal to the default) gets exactly that repo, with no surprise upstream substitution.
repo_pinned = bool((os.environ.get("UNSLOTH_SD_CPP_REPO") or "").strip())
allow_upstream = (
not repo_pinned and primary == DEFAULT_REPO and DEFAULT_REPO != UPSTREAM_FALLBACK_REPO
)
# (repo, tag_to_fetch, allow_latest). With a pin set, try the exact pin on every repo first (allow_latest = False), then each repo latest (tag = None).
attempts: list[tuple[str, Optional[str], bool]] = []
if tag:
attempts.append((primary, tag, False))
if allow_upstream:
attempts.append((UPSTREAM_FALLBACK_REPO, tag, False))
attempts.append((primary, None, True))
if allow_upstream:
attempts.append((UPSTREAM_FALLBACK_REPO, None, True))
else:
attempts.append((primary, None, True))
if allow_upstream:
attempts.append((UPSTREAM_FALLBACK_REPO, None, True))
for repo, want_tag, allow_latest in attempts:
release, chosen = _resolve_repo_asset(
repo, want_tag, accelerator, token, allow_latest = allow_latest
)
if release is not None and chosen:
if repo != primary:
# Diagnostic goes to stderr, not stdout: --print-asset documents its stdout as the asset name only, so a caller parsing it as a single line must not see this log.
print(
f"falling back to {repo} for {platform.system()}/{platform.machine()}",
file = sys.stderr,
flush = True,
)
return repo, release, chosen
return primary, None, None
def install(
*,
install_dir: Optional[Path] = None,
accelerator: str = "auto",
token: Optional[str] = None,
) -> Path:
"""Download + extract the prebuilt for this host. Returns the sd-cli path.
Resolves against the Unsloth mirror (``DEFAULT_REPO``) first; if the mirror can't
serve this host (release missing, or a host we don't build) AND the default repo is
in use, falls back to leejet upstream so native install still works. Raises
``RuntimeError`` only when neither source has an asset for the host, or the archive
has no ``sd-cli``.
"""
target = install_dir or default_install_dir()
# Only claim ownership of ``target`` (marking it for the uninstaller recursive delete) when this install created it or it was empty, or it already carries our marker -- never adopt a pre-existing non-empty dir, else a later uninstall could wipe a user own checkout.
marker = target / ".unsloth-studio-owned"
_may_own = True
if target.exists():
if not target.is_dir():
raise RuntimeError(f"sd.cpp install target is not a directory: {target}")
try:
_pre_existing_entries = any(target.iterdir())
except OSError:
_pre_existing_entries = True
# Empty dir, or one we already own, may be (re)claimed; a non-empty unowned dir may not.
_may_own = (not _pre_existing_entries) or marker.is_file()
# Refuse to extract into a pre-existing, non-empty directory we do not own: merging the release in would overwrite or mix our binaries into the user own files. Fail so they point us at a fresh/empty location.
if not _may_own:
raise RuntimeError(
f"sd.cpp install target already exists and is not a Studio-managed directory: {target}. "
f"Refusing to extract prebuilt binaries into it to avoid overwriting or mixing them "
f"into your files. Remove or move that directory, or install into a different, empty "
f"location (pass a different --install-dir / set the Studio sd.cpp install dir)."
)
used_repo, release, chosen = _resolve_with_fallback(accelerator, token)
if release is None or not chosen:
raise RuntimeError(
f"No prebuilt sd-cli for {platform.system()}/{platform.machine()} "
f"(accelerator={accelerator}) from {used_repo}. Build from source: "
f"https://github.com/{used_repo}"
)
print(f"sd-cli: source {used_repo} release {release.get('tag_name', '?')}", flush = True)
asset = next(a for a in release["assets"] if a["name"] == chosen)
url = asset["browser_download_url"]
target.mkdir(parents = True, exist_ok = True)
# Claim ownership BEFORE any partial write (download/extract/cudart fetch below). An interrupted extraction (disk full, killed process, a raising _maybe_fetch_windows_cudart) leaves the target non-empty; without the marker the next lazy install would see it as non-empty-and-unowned and trip the refusal guard above, wedging native install until the user manually deletes the directory.
# Writing the marker first makes the retry treat this partial install as reclaimable (_may_own) and re-extract over it. Written only when _may_own (empty/new or already ours), so it never adopts a user pre-existing files.
# _may_own (empty/new or already ours), so it never adopts a user's pre-existing files.
if _may_own:
try:
marker.touch()
except OSError:
pass
archive = target / chosen
print(f"downloading {chosen} -> {archive}", flush = True)
try:
_download(url, archive)
# Verify integrity BEFORE extracting + executing.
_verify_sha256(archive, asset.get("digest"))
print("extracting ...", flush = True)
with zipfile.ZipFile(archive) as zf:
_safe_extractall(zf, target)
# Windows CUDA builds need the separately-published cudart runtime DLLs.
_maybe_fetch_windows_cudart(release, chosen, target)
finally:
# Always drop the archive: on a sha256 mismatch / corrupt zip / network error it must not linger (and a stale partial would defeat a later retry).
archive.unlink(missing_ok = True)
sd_cli = _locate_sd_cli(target)
if not sd_cli:
raise RuntimeError(f"archive {chosen} contained no sd-cli binary")
if sys.platform != "win32":
_make_executable(sd_cli)
print(f"installed sd-cli -> {sd_cli}", flush = True)
# The same archive ships the persistent sd-server; make it runnable too so the native backend can prefer it (load once, serve many) over one-shot sd-cli.
sd_server = _locate_sd_server(target)
if sd_server is not None and sys.platform != "win32":
_make_executable(sd_server)
if sd_server is not None:
print(f"installed sd-server -> {sd_server}", flush = True)
# The ownership marker (the same one setup.sh/_is_studio_root use, so the uninstaller deletes only Studio-installed sd.cpp) was already written above, before extraction, so a crashed partial install is still recognised as ours on a retry.
return sd_cli
def main(argv: Optional[list[str]] = None) -> int:
p = argparse.ArgumentParser(description = "Install a prebuilt sd-cli (stable-diffusion.cpp).")
p.add_argument(
"--accelerator", default = "auto", choices = ["auto", "cpu", "vulkan", "rocm", "cuda"]
)
p.add_argument("--install-dir", default = None)
p.add_argument(
"--print-asset", action = "store_true", help = "resolve + print the asset, don't download"
)
args = p.parse_args(argv)
if args.print_asset:
# Route through the same primary/fallback resolution as install(), so a host the mirror does not build (e.g. a Linux Vulkan request) reports the upstream asset it would actually download instead of a false "no matching prebuilt".
_used, _release, chosen = _resolve_with_fallback(args.accelerator, None)
print(chosen or "(no matching prebuilt; build from source)")
return 0 if chosen else 2
try:
install(
install_dir = Path(args.install_dir).expanduser() if args.install_dir else None,
accelerator = args.accelerator,
)
except RuntimeError as exc:
print(f"error: {exc}", file = sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())