mirror of
https://github.com/unslothai/unsloth.git
synced 2026-08-25 08:42:25 +00:00
* fix(studio): validate legacy sd discovery * studio: separate sd.cpp identity from MiniMax-H3 capability The legacy PATH probe answers "is this stable-diffusion.cpp". The H3 gate was answering "does this build carry H3" and reporting the result as if it had answered the first, so an unrelated binary reached through SD_CLI_PATH was described as a stable-diffusion.cpp build predating MiniMax-H3 -- true of every program that is not stable-diffusion.cpp, and the sentence that sent #8507 looking for a newer build of something never installed. - share one predicate, help_text_identifies_sd_cpp, between the two callers - the H3 gate takes ONE --help and derives both answers from it, asking identity first. --ref-video is a plain option name that unrelated reference-video tools also expose, so returning on the marker alone readmitted the same class of program through the override - a user-supplied binary that is not stable-diffusion.cpp gets a message saying so, and the log lines on the managed path name the actual fault - memoize the identity verdict per file revision (path + mtime + ctime + size). Discovery runs on every load and ensure_sd_cpp_binary resolves twice, so an unrelated "sd" was executed once per discovery. Only a verdict the probe produced is cached: a timeout leaves the file unchanged, so caching that "no" would blacklist a genuine build until Studio restarts * studio: vet the MiniMax-H3 binary before downloading its bundle ensure_h3_sd_cpp_binary exists to refuse a build that cannot run H3 before the four-file bundle is fetched, and its own error said so. _run_load_h3_native called it after the download loop, so every refusal still cost tens of GB, and a None return -- auto-install off, an unsupported platform, no network, or a managed copy something else is running out of -- was not rejected until later still. - move the preflight and the accelerator fallback above the asset resolution - check cancel_event before it: the preflight may install the prebuilt and takes no cancel_event, so a load cancelled before its worker started paid for an install nobody was waiting for. The download loop used to be the first check - refuse a preflight that produced no binary there, rather than after the downloads. The claimed re-vet keeps its own check; that one guards a replacement arriving mid-download - test_sd_cpp_h3_matrix.py covers platform x GPU vendor x binary state, because the combinations are what broke: a CPU-only prebuilt on a CUDA host reaches its refusal through the fallback, and that path downloaded the bundle on the way to failing * studio: apply the repo kwarg-spacing format hook * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * studio: assert the managed-path fault string, and correct which sources reach it Mutation testing found the one production change in this branch that could be reverted silently: forcing fault back to the H3 wording left every test green. That log line is the same wrong diagnosis #8507 was reported as, written to the log instead of the user. The comment above it also claimed only an env override reaches that branch. An in-tree developer build and a PATH sd-cli do too, since the identity gate covers only the sd stem. * studio: do not memoize an identity probe that learned nothing A binary that cannot load a shared library exits 127 from the dynamic loader with nothing identifying on either stream. That is a CompletedProcess, not an exception, so the previous rule cached it as a definitive "not stable-diffusion.cpp" against a file that never changed, and installing the missing library would not get it re-probed until restart. Memoize a decisive verdict only: identifying output settles it whatever the exit code, and otherwise only a clean exit is evidence of anything. The case this memo exists for is unaffected, since Debian/Ubuntu's sd answers --help with rc 0. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * studio: close two windows the H3 preflight move opened Both found in review of the move itself. Re-check cancellation on the way out of the preflight. The ensure takes no cancel_event and can spend minutes downloading and extracting the prebuilt, so a cancel arriving during it is already late; a CPU or MPS target then skipped the claimed accelerator probe (the only other cancel-aware step) and paid four sequential model_info round trips before the download loop noticed. Re-vet EVERY binary after the download, not only a managed one. The managed-only test was written when the ensure sat right there, so a user-supplied build had been vetted microseconds earlier and only a concurrent install could have moved underneath it. Vetting before the download makes that window the whole download -- long enough to rebuild or repoint an SD_CLI_PATH copy, after which the replacement became the recorded identity that every later generation compares against. * studio: re-vet identity too, and bound how long an identity verdict answers Both from review of the previous round. The post-download re-vet asked only the capability question. --ref-video is a plain option name that unrelated reference-video tools expose too, so a swap to one of those cleared a marker-only check and was then recorded as the vetted build. It now applies the same identity-and-capability pair the preflight does, off one --help. And the memo key is not a content revision on Windows: st_ctime there is the CREATION time, which an in-place overwrite preserves, so a same-sized write that also restores mtime leaves the whole tuple unchanged. Hashing the binary per lookup would trade the exec this memo avoids for a full read on a path walked every load, so verdicts expire instead. That bounds what the key cannot see on any platform, and a minute is still far longer than the several resolutions inside one load. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * studio: re-check the accelerator for every binary, on a decisive reading native_device is decided on the accelerator reading and then committed for the life of the runtime, and moving that reading before the download made the gap between deciding and committing the whole fetch. The re-check still only ran for a managed binary, so a user's own build rebuilt inside that window committed a GPU device around a CPU executable -- offload policy and an arbiter claim written against hardware nothing is running on. The comparison needs a decisive reading, which is why this adds the verdict form rather than reusing sd_cpp_lists_accelerator_device: that one folds "could not tell" into True, and against a recorded False it would refuse the very CPU fallback that recorded it. * studio: only re-probe the device list where there is a baseline to compare it to Hoisting the verdict out of the condition made every H3 load spawn --list-devices, including the CPU and MPS targets that deliberately never record a baseline and so cannot use the answer. On a build that hangs on that flag it cost the full probe timeout before readiness, for a value the comparison then discarded. --------- Co-authored-by: danielhanchen <danielhanchen@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
432 lines
17 KiB
Python
432 lines
17 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
|
|
|
|
"""The MiniMax-H3 native preflight across the host matrix.
|
|
|
|
``_run_load_h3_native`` decides three things before it commits a runtime: which binary to run,
|
|
which device to commit it on, and -- since #8507 -- whether to spend the four-file download at all.
|
|
Those interact: the accelerator probe only runs on a GPU target, the CPU fallback rewrites both the
|
|
binary and the device, and every refusal has to happen before the bundle is fetched.
|
|
|
|
Parametrised over ``platform x GPU vendor x binary state`` because the combinations are what broke:
|
|
a CPU-only prebuilt on a CUDA host has its own fallback path, and a refusal reached through that
|
|
fallback used to download the bundle first on the way to failing.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import threading
|
|
import types
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from core.inference.video import VideoBackend, _detect_load_family
|
|
from core.inference.video_families import VIDEO_CANCELLED_MSG
|
|
|
|
|
|
H3_REPO = "leejet/MiniMax-H3-GGUF"
|
|
H3_FILE = "minimax_h3_fl2va-Q4_K_M.gguf"
|
|
|
|
_BANNER = "stable-diffusion.cpp version unknown, commit unknown\n"
|
|
_H3_HELP = _BANNER + " --ref-video MiniMax-H3 Ref2VA reference video frame directory\n"
|
|
_PRE_H3_HELP = _BANNER + " -M, --mode run mode, one of [img_gen, vid_gen, upscale]\n"
|
|
# Debian/Ubuntu's find-and-replace `sd`, and Homebrew ships the same tool on macOS.
|
|
_UNRELATED_HELP = "sd 1.0.0\nFind & replace CLI\n\nUSAGE:\n sd <find> <replace-with>\n"
|
|
|
|
# (label, DiffusionDeviceTarget.backend, .device)
|
|
HARDWARE = [
|
|
("nvidia", "cuda", "cuda"),
|
|
("amd", "rocm", "cuda"),
|
|
("apple", "mps", "mps"),
|
|
("cpu", "cpu", "cpu"),
|
|
]
|
|
# sys.platform values. WSL is a Linux platform string with a Windows kernel underneath, so it is
|
|
# the linux row -- listed separately because it is where PATH picks up a Windows-side install.
|
|
PLATFORMS = ["linux", "wsl", "darwin", "win32"]
|
|
|
|
|
|
class _PlanInfo:
|
|
def __init__(self, siblings) -> None:
|
|
self.siblings = siblings
|
|
|
|
|
|
class _Engine:
|
|
def __init__(self, binary) -> None:
|
|
self.binary = binary
|
|
|
|
def version(self):
|
|
return "stub-version"
|
|
|
|
|
|
@pytest.fixture
|
|
def h3_host(monkeypatch, tmp_path):
|
|
"""Drive `_run_load_h3_native` on a chosen host with a chosen binary, recording downloads."""
|
|
from core.inference import video as video_mod
|
|
from core.inference import sd_cpp_backend, sd_cpp_engine
|
|
|
|
def _setup(
|
|
*,
|
|
platform: str,
|
|
backend: str,
|
|
device: str,
|
|
help_text: str | None,
|
|
managed: bool = False,
|
|
lists_accelerator: bool = True,
|
|
):
|
|
monkeypatch.setattr(
|
|
sd_cpp_engine.sys, "platform", "linux" if platform == "wsl" else platform
|
|
)
|
|
monkeypatch.setattr(
|
|
video_mod,
|
|
"resolve_diffusion_device_target",
|
|
lambda: types.SimpleNamespace(backend = backend, device = device, dtype = None),
|
|
)
|
|
monkeypatch.setattr(sd_cpp_backend, "_install_allowed", lambda: True)
|
|
monkeypatch.setattr(sd_cpp_backend, "is_managed_binary", lambda _b: managed)
|
|
monkeypatch.setattr(sd_cpp_engine, "SdCppEngine", _Engine)
|
|
|
|
binary = None if help_text is None else "/opt/sd/sd-cli"
|
|
monkeypatch.setattr(sd_cpp_backend, "ensure_sd_cpp_binary", lambda **_kwargs: binary)
|
|
|
|
device_probes: list[str] = []
|
|
|
|
def _probe(_binary, *args):
|
|
if args == ("--list-devices",):
|
|
device_probes.append(_binary)
|
|
if lists_accelerator:
|
|
return "CUDA0\tNVIDIA H100 PCIe\nCPU\tIntel(R) Xeon(R)\n"
|
|
return "CPU\tIntel(R) Xeon(R)\n"
|
|
return help_text
|
|
|
|
monkeypatch.setattr(sd_cpp_backend, "_sd_cpp_probe_output", _probe)
|
|
|
|
asset_calls: list[str] = []
|
|
|
|
class _Api:
|
|
def __init__(self, **_kwargs):
|
|
pass
|
|
|
|
def model_info(self, repo, *_args, **_kwargs):
|
|
asset_calls.append(repo)
|
|
return _PlanInfo([])
|
|
|
|
monkeypatch.setattr("huggingface_hub.HfApi", _Api)
|
|
|
|
downloads: list[str] = []
|
|
|
|
def _download(_repo, wanted, *_args, **_kwargs):
|
|
downloads.append(wanted)
|
|
path = tmp_path / Path(wanted).name
|
|
path.write_bytes(b"x")
|
|
return str(path)
|
|
|
|
monkeypatch.setattr("utils.hf_xet_fallback.hf_hub_download_with_xet_fallback", _download)
|
|
|
|
def run(cancel_event: threading.Event | None = None):
|
|
fam = _detect_load_family(H3_REPO, None, "minimax-h3")
|
|
assert fam is not None
|
|
backend_obj = VideoBackend()
|
|
backend_obj._run_load_h3_native(
|
|
fam = fam,
|
|
token = None,
|
|
cancel_event = cancel_event or threading.Event(),
|
|
repo_id = H3_REPO,
|
|
gguf_filename = H3_FILE,
|
|
)
|
|
return backend_obj
|
|
|
|
return types.SimpleNamespace(
|
|
run = run,
|
|
downloads = downloads,
|
|
asset_calls = asset_calls,
|
|
device_probes = device_probes,
|
|
)
|
|
|
|
return _setup
|
|
|
|
|
|
@pytest.mark.parametrize("platform", PLATFORMS)
|
|
@pytest.mark.parametrize("hw_label,backend,device", HARDWARE)
|
|
@pytest.mark.parametrize(
|
|
"state,help_text,expected",
|
|
[
|
|
("unrelated_binary", _UNRELATED_HELP, "is not stable-diffusion.cpp"),
|
|
("pre_h3_binary", _PRE_H3_HELP, "does not advertise MiniMax-H3"),
|
|
("no_binary", None, "could not be installed or started"),
|
|
],
|
|
)
|
|
def test_h3_preflight_refuses_before_downloading(
|
|
h3_host, platform, hw_label, backend, device, state, help_text, expected
|
|
):
|
|
"""Every refusal, on every host, costs zero downloads.
|
|
|
|
The bundle is tens of GB. A refusal that arrives after it has been fetched is the failure mode
|
|
the H3 gate was written to prevent, and it was reachable on all of these hosts: the gate ran
|
|
after the download loop, and a None binary was not rejected until later still.
|
|
"""
|
|
host = h3_host(platform = platform, backend = backend, device = device, help_text = help_text)
|
|
with pytest.raises(RuntimeError, match = expected):
|
|
host.run()
|
|
assert host.downloads == []
|
|
|
|
|
|
@pytest.mark.parametrize("platform", PLATFORMS)
|
|
@pytest.mark.parametrize("hw_label,backend,device", HARDWARE)
|
|
def test_h3_preflight_admits_a_capable_build_and_downloads_once(
|
|
h3_host, platform, hw_label, backend, device
|
|
):
|
|
"""The other direction: a genuine H3 build is not refused anywhere, and the load proceeds to
|
|
fetch exactly the four files. Guards against an identity check that is too strict."""
|
|
host = h3_host(platform = platform, backend = backend, device = device, help_text = _H3_HELP)
|
|
backend_obj = host.run()
|
|
assert len(host.downloads) == 4
|
|
assert backend_obj._state is not None
|
|
# A GPU target keeps its device when the build offers an accelerator; CPU/MPS are unchanged.
|
|
assert backend_obj._state.device == device
|
|
|
|
|
|
@pytest.mark.parametrize("platform", PLATFORMS)
|
|
@pytest.mark.parametrize(
|
|
"hw_label,backend,device", [h for h in HARDWARE if h[1] in ("cuda", "rocm")]
|
|
)
|
|
def test_h3_gpu_host_falls_back_to_the_cpu_build(h3_host, platform, hw_label, backend, device):
|
|
"""Upstream publishes no Linux CUDA archive, so a GPU host routinely ends up on the CPU
|
|
prebuilt. It must still load, committed on the CPU rather than on a GPU it never ran on."""
|
|
host = h3_host(
|
|
platform = platform,
|
|
backend = backend,
|
|
device = device,
|
|
help_text = _H3_HELP,
|
|
lists_accelerator = False,
|
|
)
|
|
backend_obj = host.run()
|
|
assert len(host.downloads) == 4
|
|
assert backend_obj._state is not None
|
|
assert backend_obj._state.device == "cpu"
|
|
|
|
|
|
@pytest.mark.parametrize("platform", PLATFORMS)
|
|
@pytest.mark.parametrize("hw_label,backend,device", HARDWARE)
|
|
def test_h3_cancellation_during_the_preflight_stops_before_the_asset_calls(
|
|
h3_host, platform, hw_label, backend, device, monkeypatch
|
|
):
|
|
"""The ensure takes no cancel_event and can spend minutes installing the prebuilt, so a cancel
|
|
arriving during it is already late. It must not then cost four more model_info round trips."""
|
|
from core.inference import sd_cpp_backend
|
|
|
|
cancelled = threading.Event()
|
|
host = h3_host(platform = platform, backend = backend, device = device, help_text = _H3_HELP)
|
|
original = sd_cpp_backend.ensure_h3_sd_cpp_binary
|
|
|
|
def _cancel_midway(**kwargs):
|
|
cancelled.set() # the user hits cancel while the install is running
|
|
return original(**kwargs)
|
|
|
|
monkeypatch.setattr(sd_cpp_backend, "ensure_h3_sd_cpp_binary", _cancel_midway)
|
|
|
|
with pytest.raises(RuntimeError, match = VIDEO_CANCELLED_MSG):
|
|
host.run(cancel_event = cancelled)
|
|
assert host.downloads == []
|
|
# The point is not that it eventually raises -- the download loop always would. It is that a
|
|
# cancelled load stops before paying for the four sequential size-estimate round trips.
|
|
assert host.asset_calls == []
|
|
|
|
|
|
@pytest.mark.parametrize("platform", PLATFORMS)
|
|
@pytest.mark.parametrize("hw_label,backend,device", HARDWARE)
|
|
def test_h3_revets_a_user_supplied_binary_swapped_during_the_download(
|
|
h3_host, platform, hw_label, backend, device, monkeypatch
|
|
):
|
|
"""Vetting before the download means the vet-to-commit window is now the whole download.
|
|
|
|
A user-supplied binary is not ours to reinstall, so nothing else guards it: if the file at that
|
|
path changes while the bundle is fetched, the re-vet is the only thing standing between the
|
|
replacement and being recorded as the identity every later generation compares against."""
|
|
from core.inference import sd_cpp_backend
|
|
|
|
host = h3_host(platform = platform, backend = backend, device = device, help_text = _H3_HELP)
|
|
|
|
# The preflight sees an H3 build; by the time the download is done the path holds a pre-H3 one.
|
|
swapped = {"done": False}
|
|
real_probe = sd_cpp_backend._sd_cpp_probe_output
|
|
|
|
def _probe(binary, *args):
|
|
if args == ("--help",) and swapped["done"]:
|
|
return _PRE_H3_HELP
|
|
return real_probe(binary, *args)
|
|
|
|
def _download(*args, **kwargs):
|
|
swapped["done"] = True
|
|
return _real_download(*args, **kwargs)
|
|
|
|
_real_download = None
|
|
import utils.hf_xet_fallback as xet
|
|
|
|
_real_download = xet.hf_hub_download_with_xet_fallback
|
|
monkeypatch.setattr(sd_cpp_backend, "_sd_cpp_probe_output", _probe)
|
|
monkeypatch.setattr(xet, "hf_hub_download_with_xet_fallback", _download)
|
|
|
|
with pytest.raises(RuntimeError, match = "changed while this model was loading"):
|
|
host.run()
|
|
assert len(host.downloads) == 4 # the swap is caught after the fetch, not before it
|
|
|
|
|
|
@pytest.mark.parametrize("platform", PLATFORMS)
|
|
@pytest.mark.parametrize("hw_label,backend,device", HARDWARE)
|
|
def test_h3_revet_checks_identity_not_just_the_h3_marker(
|
|
h3_host, platform, hw_label, backend, device, monkeypatch
|
|
):
|
|
"""The post-download re-vet asks both of the preflight's questions, not only capability.
|
|
|
|
--ref-video is a plain option name that unrelated reference-video tools expose too, so a swap
|
|
to one of those would clear a marker-only re-check and then be recorded by _sd_cli_identity as
|
|
the vetted build every later generation compares against."""
|
|
from core.inference import sd_cpp_backend
|
|
|
|
host = h3_host(platform = platform, backend = backend, device = device, help_text = _H3_HELP)
|
|
|
|
swapped = {"done": False}
|
|
real_probe = sd_cpp_backend._sd_cpp_probe_output
|
|
|
|
def _probe(binary, *args):
|
|
# Not sd.cpp, but it does carry the H3 marker -- capability alone would wave it through.
|
|
if args == ("--help",) and swapped["done"]:
|
|
return "reference-video-cli 2.1\n --ref-video PATH reference clip\n"
|
|
return real_probe(binary, *args)
|
|
|
|
import utils.hf_xet_fallback as xet
|
|
|
|
real_download = xet.hf_hub_download_with_xet_fallback
|
|
|
|
def _download(*args, **kwargs):
|
|
swapped["done"] = True
|
|
return real_download(*args, **kwargs)
|
|
|
|
monkeypatch.setattr(sd_cpp_backend, "_sd_cpp_probe_output", _probe)
|
|
monkeypatch.setattr(xet, "hf_hub_download_with_xet_fallback", _download)
|
|
|
|
with pytest.raises(RuntimeError, match = "changed while this model was loading"):
|
|
host.run()
|
|
assert len(host.downloads) == 4
|
|
|
|
|
|
@pytest.mark.parametrize("platform", PLATFORMS)
|
|
@pytest.mark.parametrize(
|
|
"hw_label,backend,device", [h for h in HARDWARE if h[1] in ("cuda", "rocm")]
|
|
)
|
|
def test_h3_revet_catches_a_user_binary_whose_accelerator_changed(
|
|
h3_host, platform, hw_label, backend, device, monkeypatch
|
|
):
|
|
"""native_device is decided on the accelerator reading and then committed for the life of the
|
|
runtime, so the reading has to still hold at commit time -- for a user's own build too, not
|
|
only a managed one. A GPU device committed around a CPU binary means offload policy and an
|
|
arbiter claim written against hardware nothing is running on."""
|
|
from core.inference import sd_cpp_backend
|
|
|
|
host = h3_host(platform = platform, backend = backend, device = device, help_text = _H3_HELP)
|
|
|
|
swapped = {"done": False}
|
|
real_probe = sd_cpp_backend._sd_cpp_probe_output
|
|
|
|
def _probe(binary, *args):
|
|
# Still an H3-capable sd.cpp, so identity and capability both pass; only the device list
|
|
# changed, which is exactly the case the H3 re-vet cannot see.
|
|
if args == ("--list-devices",) and swapped["done"]:
|
|
return "CPU\tIntel(R) Xeon(R)\n"
|
|
return real_probe(binary, *args)
|
|
|
|
import utils.hf_xet_fallback as xet
|
|
|
|
real_download = xet.hf_hub_download_with_xet_fallback
|
|
|
|
def _download(*args, **kwargs):
|
|
swapped["done"] = True
|
|
return real_download(*args, **kwargs)
|
|
|
|
monkeypatch.setattr(sd_cpp_backend, "_sd_cpp_probe_output", _probe)
|
|
monkeypatch.setattr(xet, "hf_hub_download_with_xet_fallback", _download)
|
|
|
|
with pytest.raises(RuntimeError, match = "built for a different accelerator"):
|
|
host.run()
|
|
|
|
|
|
@pytest.mark.parametrize("platform", PLATFORMS)
|
|
@pytest.mark.parametrize(
|
|
"hw_label,backend,device", [h for h in HARDWARE if h[1] in ("cuda", "rocm")]
|
|
)
|
|
def test_h3_revet_tolerates_an_unreadable_accelerator_reprobe(
|
|
h3_host, platform, hw_label, backend, device, monkeypatch
|
|
):
|
|
""" "Could not tell" is not "it changed". sd_cpp_lists_accelerator_device folds an unreadable
|
|
probe into True, so comparing THAT against a recorded False would refuse the CPU fallback that
|
|
recorded it -- the load must proceed on the reading it already has."""
|
|
from core.inference import sd_cpp_backend
|
|
|
|
host = h3_host(platform = platform, backend = backend, device = device, help_text = _H3_HELP)
|
|
|
|
swapped = {"done": False}
|
|
real_probe = sd_cpp_backend._sd_cpp_probe_output
|
|
|
|
def _probe(binary, *args):
|
|
if args == ("--list-devices",) and swapped["done"]:
|
|
return None # older build that rejects the flag, or an unreadable probe
|
|
return real_probe(binary, *args)
|
|
|
|
import utils.hf_xet_fallback as xet
|
|
|
|
real_download = xet.hf_hub_download_with_xet_fallback
|
|
|
|
def _download(*args, **kwargs):
|
|
swapped["done"] = True
|
|
return real_download(*args, **kwargs)
|
|
|
|
monkeypatch.setattr(sd_cpp_backend, "_sd_cpp_probe_output", _probe)
|
|
monkeypatch.setattr(xet, "hf_hub_download_with_xet_fallback", _download)
|
|
|
|
backend_obj = host.run()
|
|
assert backend_obj._state is not None
|
|
assert backend_obj._state.device == device
|
|
|
|
|
|
@pytest.mark.parametrize("platform", PLATFORMS)
|
|
@pytest.mark.parametrize("hw_label,backend,device", HARDWARE)
|
|
def test_h3_probes_devices_only_where_the_answer_is_used(
|
|
h3_host, platform, hw_label, backend, device
|
|
):
|
|
"""--list-devices costs a subprocess, and the full probe timeout when a build hangs on it.
|
|
|
|
A CPU or MPS target never records a baseline, so neither the decision nor the re-check can use
|
|
the answer -- it must not be asked. A GPU target asks exactly twice: once to decide, once under
|
|
the claim to confirm the decision still holds."""
|
|
host = h3_host(platform = platform, backend = backend, device = device, help_text = _H3_HELP)
|
|
host.run()
|
|
expected = 2 if backend not in ("cpu", "mps") else 0
|
|
assert len(host.device_probes) == expected
|
|
|
|
|
|
@pytest.mark.parametrize("platform", PLATFORMS)
|
|
@pytest.mark.parametrize("hw_label,backend,device", HARDWARE)
|
|
def test_h3_cancellation_precedes_the_binary_install(h3_host, platform, hw_label, backend, device):
|
|
"""The preflight can download and extract the sd-cli prebuilt and takes no cancel_event, so a
|
|
load cancelled before its worker started must not reach it."""
|
|
from core.inference import sd_cpp_backend
|
|
|
|
host = h3_host(platform = platform, backend = backend, device = device, help_text = _H3_HELP)
|
|
ensures: list[str] = []
|
|
original = sd_cpp_backend.ensure_h3_sd_cpp_binary
|
|
|
|
def _spy(**kwargs):
|
|
ensures.append("called")
|
|
return original(**kwargs)
|
|
|
|
sd_cpp_backend.ensure_h3_sd_cpp_binary = _spy
|
|
try:
|
|
cancelled = threading.Event()
|
|
cancelled.set()
|
|
with pytest.raises(RuntimeError):
|
|
host.run(cancel_event = cancelled)
|
|
finally:
|
|
sd_cpp_backend.ensure_h3_sd_cpp_binary = original
|
|
assert ensures == []
|
|
assert host.downloads == []
|