Studio diffusion (Phase 16) review round 2: native CPU arbiter, status offload, load race
Codex review on the native-engine routing: - The /images/load route took the GPU arbiter (acquire_for(DIFFUSION) -> evict chat) unconditionally after engine selection. A native sd.cpp load on a pure-CPU host never touches the GPU, so that needlessly tore down the resident chat model. The handoff is now gated: diffusers always takes it, a force-native sd.cpp load on a CUDA/XPU/MPS box still takes it, but a native sd.cpp load on a CPU host skips it. - sd_cpp status() hardcoded offload_policy 'none' / cpu_offload False even when _run_load computed real offload flags (balanced/low_vram/cpu_offload off-CPU), so the setting was unverifiable. status now derives them from state.offload_flags (still 'none' on CPU, where the flags are empty). - _run_load committed the new state without cancelling/waiting on a generation that started during the (slow) asset download, so a stale sd-cli run against the OLD model could finish afterward and persist an image from the previous model once the new load reported ready. The commit now signals the in-flight cancel and waits on _generate_lock before swapping _state (taken only at commit, so the download never serialises against generation), mirroring the diffusers load path. Tests: CPU native load skips the arbiter while a GPU native load takes it; status reports offload active when flags are set; _run_load cancels and waits for an in-flight generation before committing.
This commit is contained in:
parent
656d11c731
commit
8b385c44fb
4 changed files with 156 additions and 8 deletions
|
|
@ -6,6 +6,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import types
|
||||
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
|
@ -236,3 +237,74 @@ def test_unload_clears_state_and_signals_cancel():
|
|||
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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue