unsloth/tests/studio/test_diffusion_gpu_layers_contract.py
Daniel Han 29b9cbb984
Studio: pin GGUF to CPU on virtualised Metal, clamp parallel slots under MTP (#7717)
* Studio: pin GGUF to CPU on virtualised Metal, clamp parallel slots under MTP

Two GGUF inference defects found while driving the desktop app end to end.

1. On a virtualised Apple GPU every offloaded layer returns corrupt tokens. On
   macos-14/15 runners the same model, quant and binary emit an ordered walk
   through the character set with offload and correct English at gpu_layers=0,
   while MLX on the same machine stays coherent. _metal_device_is_paravirtual
   detects the case from MLX's device name, falling back to system_profiler,
   and load_model pins gpu_layers=0 only there. Physical Apple Silicon never
   reports "Paravirtual" and keeps full offload.

2. llama.cpp's draft-mtp path serves one sequence, but Studio still asked for
   the default 4 parallel slots, so concurrent chats against
   unsloth/Qwen3.5-2B-MTP-GGUF shared tokens across replies. Clamp --parallel
   to 1 when MTP is selected, both from an explicit --spec-type in extra_args
   and from the resolved spec flags.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Make the CPU pin actually stick, and stop the clamps misfiring

Review found five real defects in the first version. Four of them meant the fix
looked applied and did nothing.

The pin was defeatable. Only manual mode strips offload flags at the route, so
an Auto request still carries `-ngl 99` into load_model, and user extras are
appended after the managed `--gpu-layers 0` where llama.cpp's last-wins parser
takes them. The log said "Forcing gpu_layers=0" while the launch offloaded
anyway. Offload flags are now stripped when the fallback fires.

The pin also missed everything that does not read --gpu-layers. clip.cpp picks
its backend from mmproj_use_gpu, which defaults true, so a vision encoder kept
running the corrupt path; and common_base_params_to_speculative overwrites
n_gpu_layers for a separate drafter, whose -1 default means auto. Both are now
pinned, gated on a --help probe like the other optional flags here, since an
older build would reject the unknown argument and refuse to start.

Repeat Auto loads tore down a healthy server. The duplicate-load check ran
before the guard, so it compared the raw request against the normalized state
the previous launch recorded, missed the fast path every time, and killed a
working CPU server. The guard now settles placement above that check.

The MTP backstop could clamp launches that are not MTP. When the user owns
--spec-type, _build_speculative_flags returns nothing, and judging that empty
list falls through to LLAMA_ARG_SPEC_TYPE. Measured:
_extra_args_requests_mtp([], {"LLAMA_ARG_SPEC_TYPE": "draft-mtp"}) is True.
Both clamps now pass env = {} and the backstop skips user-owned spec types.

The training guard sized 4 slots while load_model clamped to 1, so a load that
fits could still be refused with a 409. It now mirrors the clamp, as it already
does for --kv-unified.

Rejected one suggestion: applying the fallback before the training guard would
take the manual exemption and skip the guard, which on unified memory can OOM a
concurrent training run.

Also corrected the MTP comment. It claimed draft-mtp serves one sequence and
that concurrent chats share tokens. Upstream keeps MTP state per sequence and
acceptance is gated on target logits, so committed text stays correct. The
clamp is still right, because the model card ships -np 1 and split_equal ubatch
reordering against raw-index nextn reads collapses draft acceptance above one
slot.

Tests: guard 8 -> 15, parallel slots +2, both asserting the negative case so the
fixes cannot over-apply. Full related suite goes 98 -> 100 failures against clean
main, the two additions being the new tests themselves, which fail there for the
same pre-existing pollution that already fails their five untouched
test_training_guard siblings; the file passes 43/43 in isolation.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Key the companion pins on the hardware, and make them outlive user extras

Round two of review, plus a defect I introduced in round one.

The guard was keyed on the request, not the hardware. A user picking Manual with
0 layers on the same virtualised Mac never set _paravirtual_cpu_forced, so the
mmproj and drafter pins were silently dropped, and neither of those reads
--gpu-layers so nothing else covered them. Manual + 0 is a first-class UI
selection and exactly what someone with corrupt output reaches for. The detector
now gates the block; only the placement rewrite is skipped when there is nothing
left to rewrite.

Both companion pins were also undoable. --mmproj-offload and -ngld are real
positive flags a user can pass, the pass-through extras are appended after the
managed flags, and llama.cpp is last-wins. llama.cpp's seen_args only warns and
keys on the literal alias, so "--spec-draft-ngl 0 ... -ngld 99" does not even
warn, it just takes 99. The strip cannot help: _SPEC_FLAGS deliberately excludes
drafter knobs. Both pins now emit after the extras. The drafter pin also reads
extra_args alongside spec_flags, since a user-owned --spec-type makes
_build_speculative_flags return nothing and their --model-draft was invisible.

The capability gate I added in round one was itself broken. It probed
'--spec-draft-ngl' or '-ngld' and then emitted the literal --spec-draft-ngl,
which only exists from llama.cpp b8955, so an older build would answer
"supported" and then refuse to start on a name it does not know -- the exact
failure the gate exists to prevent. The '-ngld' half was dead code besides: the
help-block parser records long forms only. The probe now walks --spec-draft-ngl,
--gpu-layers-draft and --n-gpu-layers-draft and records which one the build has,
mirroring spec_draft_n_max_flag.

Declared the new tensor_parallel drop site in _ALLOWED_TP_DROP_GUARDS. The
allowlist is there so a new drop is conscious rather than silent; this one drops
nothing real, since a paravirtual Mac has a single emulated device.

Rejected one item: the training guard cannot know a UI-selected mode resolves to
MTP, and clamping on the requested mode would under-reserve KV on the four paths
where _build_speculative_flags drops MTP, trading a conservative 409 for a
training-run OOM.

Tests: guard and parallel-slot files 61 passed in isolation, mtp detection 275
passed, tp regression 42 passed. Full related suite is 100 failures, byte
identical to the PR-head baseline.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Neutralise a pass-through --split-mode on the virtualised-Mac CPU fallback

--split-mode is a GPU placement flag, and the paravirtual fallback has already
pinned the load to CPU, but it is not inert at --gpu-layers 0. llama.cpp builds
a buffer-type list for every device in model->devices before any layer is
assigned (llama-model.cpp: the make_gpu_buft_list loop runs at :1270, the
i_gpu_start/act_gpu_layers split is not computed until :1315), so n_gpu_layers
never reaches that step. -sm row therefore throws "device <X> does not support
split buffers" on every backend except SYCL, and -sm tensor throws
"LLAMA_SPLIT_MODE_TENSOR not implemented for architecture" on every arch
llm_arch_supports_sm_tensor excludes. Either one turns the CPU rescue into a
server that refuses to start. The zero-offload mask cannot save it: it only
writes CUDA/HIP visibility vars, so the Metal device stays in the list on the
one platform this fallback fires on.

Overridden rather than stripped. llama.cpp is last-wins, so appending the
default "layer" neutralises the mode without rewriting extra_args. Stripping
would: the route's duplicate-load comparator compares the stored extras verbatim
and the UI does not round-trip the extras box, so a rewrite would leave the two
permanently unequal and turn every later Apply into a real model swap. Emitted
after the pass-through extras for the same reason as the mmproj and drafter
pins, and before the env block, since _zero_offload_keeps_gpu_visible reads the
finished cmd.

--tensor-split needs no treatment: splits is built but never indexed once
act_gpu_layers is 0, and parsing only bounds it against llama_max_devices().

16 tests, each with negatives so the override cannot over-apply: -ts/
--tensor-split return nothing, an existing "layer" returns nothing, and an AST
check pins the call site's guard to _paravirtual_cpu_forced so a real Mac keeps
its row/none split.

* Stop the httpx test stub from shadowing the real module in a combined run

Cross-platform staging CI ran the four test files this PR touches in one pytest
session and produced 18 failures on Linux, macOS and Windows alike, all one
error: "module 'httpx' has no attribute 'Response'" out of routes/inference.py.

sys.modules.setdefault("httpx", stub) only checks whether httpx has already been
imported, not whether it is installed, so on a runner that pip-installs the real
httpx the stub still won if this file imported first. Every file importing later
then found a working httpx and kept it, and this stub is a strict subset of the
real module, so anything reaching httpx.Response blew up. test_tp_vision_
regression.py and test_parallel_slots_per_load.py already guard against this by
preferring the real module; this file now does the same, and its fallback stub
carries Response so the deps-absent path is covered too.

Pre-existing, not introduced here: the same 18 failures reproduce at the merge
base from the two untouched files alone. It surfaced now because this PR is the
first change to touch both halves of the pair, so CI selects them together.

Combined run goes from 18 failed / 360 passed to 394 passed. Each file
separately is unchanged: 275, 43, 42 and 34 passed. On an interpreter with no
httpx at all the count is identical to the merge base, so the stub path did not
regress.

* Give the MTP fallback back the slots the clamp took

The MTP backstop patches cmd's --parallel to 1 in place, and the --spec-default
retry slices fallback_cmd out of that already-patched cmd. --parallel is emitted
long before _spec_start, so it sits in the untouched prefix and the retry
inherits the clamp. That retry is not MTP, so a load that asked for N slots,
resolved to MTP, and then failed MTP startup came up on a working non-MTP server
that serves one chat at a time.

It was also permanent. _requested_n_parallel is set from the pre-clamp snapshot,
so the dedupe compares the original ask against the incoming ask, they match, and
an identical reload returns already_loaded instead of rebuilding the server.
_requested_spec_mode is deliberately preserved across the fallback, so the
spec-mode branch dedupes too.

Reachable on the ordinary path: the earlier clamp fires only on an explicit user
--spec-type in extras, and the route passes n_parallel through untouched (the
training guard clamps only a local for VRAM sizing). The backstop exists exactly
because Auto/MTP is not resolved until _build_speculative_flags runs.

Restores the clamp-time count, not the caller's original ask, so the fit's own
reduction is respected and the KV budget still covers it. Rebound at patch time
because the mmproj text-only retry and the no---fit retry both derive from
fallback_cmd.

6 tests, 4 of them negative: a single-slot load stays single, a user-owned
--spec-type gains nothing, a non-MTP resolution is inert, and an 8-slot ask the
fitter already cut to 4 restores 4 rather than 8. An AST check pins the restore
to the one branch that actually drops MTP, so a successful MTP launch and the
flash-attention retry both stay clamped.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Drop the vision projector when it cannot be pinned off a virtualised Metal device

The projector pin was probe-gated but the projector itself was not: with
supports_no_mmproj_offload false, --mmproj still shipped and clip kept its
default Metal backend, so the main model ran on CPU while every image was
encoded from the corrupt device this fallback exists to avoid. clip has no other
lever: tools/mtmd never reads n_gpu_layers, mmproj_use_gpu defaults true, and
--device does not reach clip, so --no-mmproj-offload is the only way off.

The reachable trigger is a failed or timed-out --help probe, which zeroes every
capability, not an old llama-server. A build that lacks --no-mmproj-offload
(llama.cpp 7c727fbe, 2025-04-24) cannot start at all here, since the base argv
always passes --flash-attn on and that value form only arrives with e81b8e4b,
2025-08-30, four months later.

Clears launch_mmproj_path rather than only effective_is_vision, because the same
value feeds the mmproj VRAM budget and the audio-encoder probe; a half drop would
leave the server advertising an encoder it never launched.

Tradeoff: on a virtualised Mac whose probe fails, vision goes away for the
session instead of returning corrupt embeddings. The warning names the cause and
the fix, and the branch is confined to paravirtual hardware.

6 tests, 3 negative: a pinnable projector survives on the same hardware, a real
Mac keeps vision on a build without the flag, and a text-only GGUF is unaffected.
The three positives fail with the source change reverted.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Drop a separate drafter that cannot be pinned off a virtualised Metal device

Same gap as the projector, one flag over. The pin was probe-gated but the
drafter was not, so with no spec_draft_ngl_flag the CPU pin was skipped while
--model-draft still shipped. A separate drafter carries its own placement:
common_base_params_to_speculative overwrites n_gpu_layers with the draft default
of -1, and any negative resolves to every layer, so --gpu-layers 0 does not reach
it and the drafter ran on the corrupt device.

Severity is throughput, not correctness. Draft tokens are only ever compared:
common_sampler_sample_and_accept_n pushes the target model's own draw and breaks
at the first mismatch, and the server rolls back the provisional tokens before
streaming. A corrupt drafter therefore has every proposal rejected, costing a
wasted forward pass per step, and cannot leak into the output.

As with the projector, an old build is not the trigger. llama-server has
advertised a draft-ngl flag since d9d54e49 (2024-11-25), and the base argv
requires --flash-attn on from e81b8e4b (2025-08-30), so the only reachable paths
are a failed or timed-out --help probe or a custom fork.

Takes the spec group and the inherited LLAMA_ARG_SPEC_* with it, since a
surviving --spec-type would reach a server with no drafter to serve it, and
--spec-type appends rather than replaces. A drafter the user already pinned
themselves is left alone: the probe only decides whether Unsloth can emit the
flag, so their pin works regardless and dropping would cost them speed for
nothing.

12 tests, 8 negative. Each gate clause is pinned by mutation: deleting any one of
them fails exactly the negative that guards it.

* Probe for httpx instead of importing it

The Source lint import-hoist check blocks a newly added import that is never
used, and the try/import guard was exactly that: the import existed only for its
ImportError. find_spec answers the same question without importing, which also
keeps installing the stub as this module's only side effect on sys.modules.

Behaviour is unchanged: the real httpx still wins when installed, and the stub is
still installed when it is not.

* Keep a suppressed drafter and an inherited projector from defeating their guards

Two follow-ons to the paravirtual guards, plus a landmine in my own httpx fix.

The drafter drop cleared launch_mtp_draft_path, and that None was stored as the
loaded drafter. The sibling is still on disk, so the detector kept handing it
back and both duplicate-load checks compared it against None: the backend's
_already_in_target_state and the route's detected-versus-stored probe, which
fires first. Every repeat Apply therefore tore down a healthy drafter-free server
and could hit the active-generation reload gate. The suppressed path is now
recorded and accepted by both comparisons, and cleared on unload. Only an update
lifts the suppression, and unload_model runs before the binary swap, so a
newly-capable build can never be deduped away.

The projector guard only ever cleared Unsloth's own resolved path, so an
inherited LLAMA_ARG_MMPROJ loaded a projector the guard believed it had dropped,
unpinned and independent of --gpu-layers 0. LLAMA_ARG_MMPROJ_URL is worse: its
download overwrites mmproj.path, so it outranks even the --mmproj Unsloth emits
with --no-mmproj-offload. Both are popped on this device, matching the
LLAMA_ARG_SPEC_* scrub the drafter drop already does. Users cannot reach this
through argv, since --mmproj and --mmproj-url are denylisted.

The find_spec probe I added for the import-hoist lint could abort collection.
find_spec raises ValueError on a module already in sys.modules without a spec,
and 27 test files in this tree install a bare ModuleType httpx stub. On any
interpreter without real httpx, one of those collecting first turned the whole
run into a collection error rather than a failed test. CI never saw it because
starlette imports real httpx first. sys.modules is now tested before find_spec,
which is also the more direct question: if anything already provided httpx, leave
it alone.

9 tests, 6 negative: a drafter that genuinely appeared still reloads, a different
or deleted drafter still reloads, unload clears the suppression, a launched
drafter records none, and a real Mac keeps its inherited projector.

* Ask hw.model too: SPDisplaysDataType is empty on the headless Macs this catches

Measured the detector on macos-14 and macos-15 runners, which are the exact
hardware this guard exists for (hw.model VirtualMac2,1). MLX names the device
"Apple Paravirtual device" in about 40 ms. system_profiler SPDisplaysDataType
returns zero bytes on both, stdout and stderr alike, after about 300 ms.

So the fallback did not work. Its comment says MLX is not on every Mac, but on a
VM with no display, which is every cloud and CI Mac, the display probe has
nothing to report, and the machine read as bare metal and kept the offload that
corrupts its output. The probe only ever worked when MLX was already installed,
which is the case that never needed it.

hw.model answers headless: VirtualMac2,1 against Mac<n>,<n> on real hardware.
It goes between the two, so the order is now cheapest-first and each probe covers
what the next cannot: MLX names the device outright, hw.model catches headless
VMs, and SPDisplaysDataType still catches desktop VMs whose model identifier
looks physical.

4 tests, 3 negative: a physical Mac reporting Mac15,3 is not dragged down, a
desktop VM is still caught through the display probe so the net did not narrow,
and MLX answering spawns no subprocess at all.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Stop a failed capability probe from dropping vision and speculation

Parallel review of the whole branch. Three fixes, all in the same direction: the
guards were treating "the probe did not answer" as "the build cannot do this".

Both companion flags predate the oldest build that can start here.
--no-mmproj-offload is llama.cpp b5178 (2025-04-24) and --gpu-layers-draft is
from 2023, while the base argv already requires --flash-attn on from b6325
(2025-08-30). So a false capability never means the flag is missing, it means the
--help probe failed, and that is easy to trigger: llama.cpp applies every
LLAMA_ARG_* before parsing -h, so one malformed inherited variable makes --help
exit non-zero, and the all-false result is cached for the rest of the process.
The drops then disabled image input and speculation and told the user to update a
build that already supported both. The drops now require a conclusive probe, and
the pins cover the unanswered case, which is the same fail-open shape the MTP
probe already uses.

The drafter env scrub only knew the post-b8955 variable names. Between the
launchable floor and the rename on 2026-04-28 the child reads LLAMA_ARG_MODEL_DRAFT
and LLAMA_ARG_HFD_REPO instead, so on any build in that window an inherited
drafter survived the drop and llama.cpp put it straight back on the virtualised
device. Both spellings are now read and scrubbed.

--override-tensor was the one placement flag --gpu-layers 0 cannot answer.
llama.cpp applies it while selecting each weight's buffer type, before any layer
is assigned to a device, so -ot ".*=Metal" puts weights on the corrupt GPU
whatever the layer count says, and -otd does the same to the drafter. It
accumulates rather than replacing, so no appended flag can neutralise it. Only
GPU-bound targets are dropped: -ot exps=CPU is the common spelling, it moves
weights the same way this fallback does, and stripping it would slow the load it
is meant to rescue. Same rule for the inherited LLAMA_ARG_OVERRIDE_TENSOR.

The exec-based harnesses now seed their scope from the module instead of listing
helpers by hand, so they run against the real functions and a new helper cannot
silently fall out of scope.

17 new tests, 9 negative.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Normalize the virtualised-Metal CPU pin once, and compare against it

On a paravirtual Mac load_model rewrites the requested placement to CPU
(manual/0, no tensor split, no MoE offload, offload flags stripped from the
extras) and records the rewrite. The rewrite lived inside load_model while both
duplicate-load comparators judged the RAW incoming request, so a repeat
identical Apply mismatched the backend's own state and tore down a healthy CPU
server, 409-ing on an active generation or cancelling it.

paravirtual_normalized_request is now the one definition of that rewrite, used
by the launch and by both comparators (LlamaCppBackend._already_in_target_state
and the route's _request_matches_loaded_settings). It is pure and idempotent, so
load_model's own comparator call and a respawn replay are no-ops.

It also runs unconditionally, not only when the request asks for offload. Manual
plus 0 layers already places the main model on CPU, but the extras are the
caller's: an -ot ".*=Metal" is applied while each weight's buffer type is chosen,
before any layer is assigned, and the route's manual-mode strip only covers the
--gpu-layers family. That request reached the child with the override intact and
put the weights straight back on the corrupt device.

The drafter drop rewrites the extras too (it strips the whole spec group). Rather
than compare that rewrite against the list the caller keeps sending, record the
requested extras beside the launched ones, the way _requested_n_ctx and
_mtp_draft_suppressed_path already do, and restore the requested spec mode when
the drop removed the user's own --spec-type.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Gate the paravirtual drafter drop on a drafter the launch actually loads

On a virtualised Metal Mac with no draft-layer flag to pin with, the drop
fired on any resolved sibling mtp-*.gguf, even when the user's own
--spec-type selects a drafter-free mode. _build_speculative_flags returns
before it can emit --model-draft for that sibling, so nothing was going to
be placed on the device, yet strip_spec removed the user's --spec-type and
its ngram knobs. Key the sibling half on Unsloth still owning the spec
block; an explicit --model-draft (or the inherited env) still drops, since
llama.cpp loads a draft model whenever its path is set.

* Pin the device outright, widen VM detection and add an escape hatch for PR #7717

--gpu-layers 0 does not by itself keep work off the device: with op_offload
on, ggml_backend_sched still routes ops whose weights live in host buffers to
a higher-priority backend and Metal takes them at batch >= 32, so the launch
now also passes --device none. It is emitted after the user extras and
--device assigns rather than appends, so the pin wins.

hw.model reports VMM-x86_64 inside an Intel guest, which contains neither
'virtual' nor 'paravirtual', so that spelling is matched too.

UNSLOTH_ALLOW_PARAVIRTUAL_METAL=1 keeps the GPU on a VM known to be good: the
corruption does not reproduce on every build and quant, and gemma-3-270m
Q4_K_M on b9000 and b10090 was byte identical at gpu_layers 0 and 99.

The MTP slot clamp reads the environment again. llama.cpp appends spec types
rather than replacing them, so an inherited LLAMA_ARG_SPEC_TYPE=draft-mtp
really does launch MTP and no later flag clears it.

--override_tensor now folds through _flag_name, matching llama.cpp's own
underscore folding, so the underscore spelling cannot slip past the strip.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Keep the suite host independent for PR #7717

The dedupe comparators consult real hardware, so every existing test of them
became host dependent: on a Mac, and on GitHub's macos runners which are
paravirtual, the incoming request normalizes to the CPU pin while fixture
state that was never normalized does not. 27 tests failed on macos-14 and
none on Linux. An autouse fixture pins the detector off, patching the route's
binding too since its import sits in a module-level try and is a separate
global.

test_the_route_dedupe_reads_the_suppressed_drafter_too patched only the
llama_cpp attribute while calling the route comparator, so it exercised the
bare-metal path on Linux and the real one on a Mac. It now patches both and
starts from the placement a paravirtual load leaves behind.

The diffusion dedupe contract follows the rename to the normalized locals,
which fall back to the request itself off a virtualised device.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Align the route slot clamp and stop scoring a CPU-pinned projector as GPU resident for PR #7717

The route sized VRAM with env = {} while the backend clamp reads the
environment, so with an inherited LLAMA_ARG_SPEC_TYPE=draft-mtp it budgeted
for the slots that were asked for against a server that launches one, and
could 409 a load that fits. Both sides read the env now.

--no-mmproj-offload clears mmproj_use_gpu and clip.cpp gates the whole GPU
backend on it, so the projector holds no VRAM. _cmd_has_gpu_companion still
answered True for the --mmproj token, which made a --device none vision
server look GPU resident and let the training coordinator unload a healthy
chat server. The last of the two offload flags wins, as llama.cpp assigns
there rather than accumulating.

The diffusion contract loads its own copy of the module, so neither conftest
reaches it; it pins the detector off the same way.

* Pin the drafter device, not just its layer count, for PR #7717

common_base_params_to_speculative replaces the draft context's device list
with the draft one, so the main --device none never reaches a separate
drafter, and an empty draft list means no device filter at all: every device
stays visible to it. The layer count alone left the drafter free to run on
the corrupt device for exactly the reason --gpu-layers 0 did, so the pin now
adds --device-draft none. That spelling is accepted across the whole
supported range, primary at b6325 and an alias at head, and the CPU-drafter
accounting already reads none as CPU so the budget still does not charge it.

* Detect MTP across every accumulated spec type, and drop drafter state on diffusion loads for PR #7717

_extra_args_requests_mtp read only the last --spec-type and ignored the env
behind it, but llama.cpp applies the env first and inserts each --spec-type
rather than replacing, so an inherited draft-mtp alongside an extras
--spec-type ngram-mod still launches MTP. The clamp then left the slots alone
and the backstop was skipped because the extras owned the spec type, which is
exactly the unsupported multi-slot MTP path. It now counts a type from any
source. _effective_spec_type keeps last-wins, since it answers the different
question of which single label to display and compare, and its docstring no
longer claims that matches llama.cpp.

The diffusion branch returns before the assignment that records the drafter,
and only unload clears it, so a drafter from the previous load stayed on the
backend while the dedupe compared the pair. Both fields are cleared there now,
beside the preserved-fallback flag that is reset for the same reason.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Accumulate speculative types in the separate-draft check too for PR #7717

The MTP fix left its sibling on last-wins, so an inherited draft-simple
followed by --spec-default or another --spec-type read as no separate draft
model and the budget under-reserved one that still loads. Both now share
_accumulated_spec_types, which is also the single place that documents why
reading the last value is wrong.

The two budget tests asserted the old premise outright (a later CLI type
overrides an earlier one, and --spec-default clears a stale env). They assert
the accumulate semantics now, including --spec-type none, which returns NONE
but is still appended and so cannot clear an earlier draft-mtp.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Clear the inherited spec env when Unsloth owns the spec block for PR #7717

Nothing Unsloth emits can undo an inherited LLAMA_ARG_SPEC_TYPE, because
llama.cpp applies the env first and appends rather than replaces. So a managed
non-MTP launch still started MTP, the crash-recovery replay that sets the mode
to off and appends --spec-default could not actually drop it, and the fit had
not budgeted the separate drafter an inherited draft-simple plus
LLAMA_ARG_SPEC_DRAFT_MODEL would add, which is a load that passes the training
guard sized main-only and then evicts training.

The launch now scrubs the spec env whenever the extras do not name a
--spec-type, which is exactly when _build_speculative_flags emits the whole
block. This is the same reconciliation the launch already does for
LLAMA_ARG_SPLIT_MODE, LLAMA_ARG_TENSOR_SPLIT, LLAMA_ARG_DEVICE and
LLAMA_ARG_OVERRIDE_TENSOR. Extras that own --spec-type keep theirs, since
there the flags and the env genuinely accumulate, and that is the case the
slot clamp reads.

The fit is paired with it: it reserves against the env only in that same
extras-own case, the one where the env still reaches the child.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Judge the spec env the child will actually get for PR #7717

Scrubbing the spec env for a managed block left every prediction of the launch
reading os.environ, so they described a server that was not the one starting.
The slot clamp cut a multi-slot load to one for an inherited draft-mtp the
child never receives, and the count never came back because the dedupe records
the original ask. The fit reserved weights and KV for an inherited drafter that
never loads, shrinking context or rejecting a placement that fits.

_child_spec_env is now the single answer to what survives to the child, and
every site uses it: the early clamp, the resolved-flags backstop, the fit
budget and its env drafter, the launch-time MTP decision and the route guard.

Two more sites the same reasoning reaches. The launch treated the env as live
whenever nothing emitted a spec flag, which is now exactly when it is scrubbed.
And the MTP crash recovery neutralised a user --spec-type draft-mtp by
appending --spec-default, which cannot work because llama.cpp appends types;
the replay strips the spec flags instead, and then owns the block so the env
goes too.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Stop the training guard shrinking itself for MTP in PR #7717

The MTP branch this PR added to the chat-load guard sized the request as one
slot, matching what load_model launches. But _estimate_gguf_required_gb counts
only the drafter file and the main KV: not the draft KV, the duplicated target
context MLA keeps, or the draft compute reserve, all of which load_model does
budget. Clamping there dropped the slot KV without adding those back, so a
model whose one-slot KV fits beside training could be admitted and then evict
it. main has no such clamp, so this was a guard my own change had weakened.

Sizing the MTP overhead properly at the route would mean replicating the
launch resolution the estimator needs (draft cache types, swa_full,
kv_unified, ubatch, flash-attn), so the guard keeps the asked count instead
and the spare slots stand in for what is not modelled. A false 409 is a retry;
a guard that under-sizes evicts a running job. The diffusion and kv-unified
clamps stay, since neither drops a term the estimate models.

* Give back the slots the drafter drop frees, and guard the CPU pin for PR #7717

The extras-MTP clamp cuts a multi-slot request to one slot. On a virtualised
Mac whose llama-server advertises no draft-layer flag, the drop then strips
that same spec group, so the server launches without speculation and still
serves one chat at a time. Nothing restored it either: the dedupe records the
original ask, so every repeat Apply matched and kept the clamp. The slots are
handed back at the drop, before the spec flags are rebuilt, so the existing
backstop still re-clamps if Unsloth's own resolution turns out to be MTP.

The training guard sized the request as it arrived, but load_model rewrites a
GGUF placement to CPU on this hardware, so an Auto chat load could be refused
for VRAM it never takes. It now normalizes through the same helper the launch
and both duplicate-load comparators use; the rewrite lands on manual with zero
layers, which the guard already treats as an explicit placement.

* Refuse a diffusion load whose CPU pin cannot be applied for PR #7717

The paravirtual rewrite hands the diffusion runner manual with zero layers, but
an older unsloth_zoo shim has no --ngl and the split is dropped rather than
failing argparse. Nothing else keeps that runner off Metal: cpu_only comes from
_effective_gpu_count, which is torch.cuda only and reads 0 on a Mac, and the
empty --gpu token it produces still leaves Metal available. Only the zero-layer
split keeps the weights off every backend, so dropping it put the load back on
the path this guard exists to avoid, silently.

It now refuses, naming both ways out: update for a shim with --ngl, or set
UNSLOTH_ALLOW_PARAVIRTUAL_METAL=1 on a VM that has been verified. A non-zero
manual split is untouched, since that is the user's own placement.
_start_diffusion_server enforces it for every path; the local-file case is
settled above the teardown so a refusal leaves the running server alone.

* Settle the HF diffusion refusal early, and stop over-reading the env for PR #7717

Three follow-ons from the last two rounds.

The diffusion refusal only covered a local file. An HF load has no gguf_path
above the teardown, so it killed the healthy server and downloaded the model
before raising. The shim probe is local and cheap, so it is taken once and now
also gates the preflight, letting the HF case raise beside the Vulkan one.

The unpinnable-drafter gate still read os.environ, so an inherited
LLAMA_ARG_SPEC_DRAFT_MODEL dropped a drafter the launch already scrubs and
took the caller's --spec-draft-n-max and ngram knobs with it. It reads the
same _child_spec_env view as the launch and the budget now.

The MTP crash replay launches a stripped list while the caller keeps sending
the original, so both comparators missed and the next Apply restarted the
configuration that had just crashed. The replay restores the caller's extras
as the requested state, device-stripped the way the launch records them.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Tighten the comments added by PR #7717

* Drop the now-unused MTP import from the route for PR #7717

The training guard stopped clamping for MTP, which left
_extra_args_requests_mtp imported in both blocks and referenced nowhere. The
import-hoist safety net flags that as a blocker.

* Drop accumulated MTP sources before the startup retry for PR #7717

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Hand back the extras-owned MTP slot clamp on the startup retry for PR #7717

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Keep an inherited extras list from reloading the server it rewrote for PR #7717

* Tighten the comments added by the main merge for PR #7717

* Record no effective GPU pin for a forced-CPU launch for PR #7717

* Clear the effective GPU pin at the final recorder too for PR #7717

* Budget the slots the MTP retry restores, and clear the diffusion CPU pin for PR #7717

* Restore the extras MTP slots only where no GPU had to admit them for PR #7717

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Pin env-only drafters and judge inherited extras by flag for PR #7717

* Skip GPU budgeting on the forced-CPU guard and persist the fallback extras for PR #7717

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <michaelhan2050@gmail.com>
2026-08-03 05:05:00 -07:00

404 lines
16 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 diffusion runner must honour the GPU-layer split (#7574).
Studio used to drop a manual GPU-layers setting on the diffusion path and pin every layer
to GPU, so a GGUF larger than VRAM OOMed in cudaMalloc with no way out.
The pure helpers run directly; the wiring is checked at source level, since importing the
backend pulls in the whole studio stack.
"""
from __future__ import annotations
import ast
import importlib.util
import sys
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[2]
SOURCE_PATH = REPO_ROOT / "studio" / "backend" / "core" / "inference" / "llama_cpp.py"
ROUTE_PATH = REPO_ROOT / "studio" / "backend" / "routes" / "inference.py"
SRC = SOURCE_PATH.read_text(encoding = "utf-8")
TREE = ast.parse(SRC)
@pytest.fixture(scope = "module")
def llama_cpp():
"""Import the backend module directly; skip if the studio deps aren't installed."""
backend = str(REPO_ROOT / "studio" / "backend")
if backend not in sys.path:
sys.path.insert(0, backend)
spec = importlib.util.spec_from_file_location("_llama_cpp_under_test", SOURCE_PATH)
module = importlib.util.module_from_spec(spec)
try:
spec.loader.exec_module(module)
except Exception as exc: # missing optional studio dep on a bare checkout
pytest.skip(f"llama_cpp not importable here: {exc}")
finally:
# Do not leave studio/backend on sys.path: it shadows generic top-level names
# (utils, state, models, hub, auth, storage) for every later test.
if sys.path and sys.path[0] == backend:
sys.path.pop(0)
# The dedupe comparators consult the Metal device, so on a Mac (and on the macos
# runners, which are paravirtual) they would normalize the request to the CPU pin and
# stop matching these fixtures. A private copy, so pinning cannot leak into the app.
module._metal_device_is_paravirtual = lambda: False
return module
def _function(name: str) -> ast.FunctionDef:
for node in ast.walk(TREE):
if isinstance(node, ast.FunctionDef) and node.name == name:
return node
raise AssertionError(f"{name} missing")
def _body(name: str) -> str:
return ast.get_source_segment(SRC, _function(name)) or ""
# ── the split the child actually launches with ──
@pytest.mark.parametrize(
("mode", "layers", "expected"),
[
("manual", 8, 8),
("manual", 0, 0), # CPU-only is a real request, not "unset"
("manual", -1, None), # Auto slider defers to the runner
("auto", 8, None), # Unsloth mode ignores a stale layer count
("auto", -1, None),
],
)
def test_effective_ngl(llama_cpp, mode, layers, expected):
assert llama_cpp._diffusion_manual_ngl(mode, layers) == expected
def test_zero_layers_is_not_swallowed_as_falsy(llama_cpp):
"""The exact case in the report: GPU layers set to 0 must reach the child."""
assert llama_cpp._diffusion_manual_ngl("manual", 0) == 0
# ── shim capability probe ──
def test_shim_without_ngl_is_detected(llama_cpp, tmp_path):
shim = tmp_path / "shim.py"
shim.write_text('ap.add_argument("--maxtok", type=int)\n', encoding = "utf-8")
assert llama_cpp._shim_supports_ngl(["python", str(shim)]) is False
def test_shim_with_ngl_is_detected(llama_cpp, tmp_path):
shim = tmp_path / "shim.py"
shim.write_text('ap.add_argument("--ngl", type=int)\n', encoding = "utf-8")
assert llama_cpp._shim_supports_ngl(["python", str(shim)]) is True
def test_missing_shim_file_does_not_raise(llama_cpp, tmp_path):
assert llama_cpp._shim_supports_ngl(["python", str(tmp_path / "gone.py")]) is False
# ── wiring ──
def test_diffusion_server_accepts_the_layer_split():
fn = _function("_start_diffusion_server")
names = {a.arg for a in fn.args.kwonlyargs} | {a.arg for a in fn.args.args}
assert {"gpu_memory_mode", "gpu_layers"} <= names
def test_diffusion_server_forwards_ngl_and_gates_it_on_shim_support():
body = _body("_start_diffusion_server")
assert '"--ngl"' in body
assert "_shim_supports_ngl" in body
def test_zero_layers_masks_the_child_devices(llama_cpp):
"""gpu_layers=0 must CUDA-mask the child, else _gpu_offload_active=False lies to the
training VRAM coordinator and a GPU-resident runner survives into a training run.
Behavioural, not a source-text match: what matters is the token the child gets."""
arg = llama_cpp.LlamaCppBackend._diffusion_gpu_arg
assert arg([3, 1], force_cpu = True) == ""
assert arg(None, force_cpu = True) == ""
def test_explicit_pick_still_wins_when_layers_are_not_zero(llama_cpp):
"""force_cpu is the only thing above the picker. A host whose GPU torch cannot see
(Metal, Vulkan, Windows-HIP, Intel XPU) still has to honour an explicit pick."""
arg = llama_cpp.LlamaCppBackend._diffusion_gpu_arg
assert arg([3, 1], cpu_only = True) == "1"
assert arg([3, 1]) == "1"
def test_no_gpu_and_no_pick_masks_the_child(llama_cpp):
assert llama_cpp.LlamaCppBackend._diffusion_gpu_arg(None, cpu_only = True) == ""
def test_diffusion_load_passes_the_users_split_through():
call = next(
node
for node in ast.walk(_function("load_model"))
if isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute)
and node.func.attr == "_start_diffusion_server"
)
keywords = {keyword.arg: keyword.value for keyword in call.keywords}
for name in ("gpu_memory_mode", "gpu_layers"):
assert isinstance(keywords.get(name), ast.Name)
assert keywords[name].id == name
def test_diffusion_no_longer_hardcodes_auto_over_the_users_choice():
body = _body("_start_diffusion_server")
assert 'self._gpu_memory_mode = "auto"' not in body
assert "self._gpu_layers = -1" not in body
# ── dedup guards must see a split change ──
def _loaded_diffusion(llama_cpp, *, recorded_layers, requested_ngl):
"""A backend that looks like a healthy diffusion runner, for the dedup guards."""
b = llama_cpp.LlamaCppBackend()
b._process, b._healthy, b._is_diffusion = object(), True, True
b._model_identifier = "unsloth/DiffusionGemma-GGUF"
b._hf_variant = b._gguf_path = b._cache_type_kv = None
b._requested_n_ctx = 4096
b._tensor_parallel = b._layer_preserves_tensor_intent = False
b._gpu_layers = recorded_layers
b._gpu_memory_mode = "auto" if recorded_layers < 0 else "manual"
b._diffusion_requested_ngl = requested_ngl
b._gpu_ids = b._requested_gpu_ids = [0]
b._requested_spec_mode = "auto"
b._spec_fallback_reason = b._speculative_type = b._spec_draft_n_max = None
b._chat_template_override = b._mtp_draft_path = b._extra_args = None
# Dropped-split rows model "the shim stayed old"; the upgrade flip is separate.
b.diffusion_split_supported = lambda: False
return b
def _in_target_state(llama_cpp, b, *, mode, layers):
return b.adopt_load_intent_if_matched(
llama_cpp.GgufLoadIntent(
model_identifier = "unsloth/DiffusionGemma-GGUF",
n_ctx = 4096,
gpu_memory_mode = mode,
gpu_layers = layers,
gpu_ids = [0],
)
)
@pytest.mark.parametrize(
("recorded", "requested_ngl", "mode", "layers", "expected"),
[
(-1, None, "auto", -1, True), # auto -> auto
(-1, None, "manual", -1, True), # inert manual preference must not loop
(-1, None, "manual", 8, False), # a real split must reload
(8, 8, "manual", 8, True), # same split dedupes
(8, 8, "manual", 4, False), # a split change reloads
(8, 8, "auto", -1, False), # manual -> auto reloads
(0, 0, "manual", 0, True), # CPU-only split dedupes with itself
# No --ngl: -1 runs but 20 was the ask; comparing on the ask stops a reload loop.
(-1, 20, "manual", 20, True),
(-1, 20, "manual", 8, False),
],
)
def test_backend_dedup_compares_the_requested_split(
llama_cpp, recorded, requested_ngl, mode, layers, expected
):
b = _loaded_diffusion(llama_cpp, recorded_layers = recorded, requested_ngl = requested_ngl)
assert _in_target_state(llama_cpp, b, mode = mode, layers = layers) is expected
def test_the_dedupe_compares_the_requested_split_through_the_paravirtual_rewrite():
"""The single comparator now lives on the backend, so the diffusion split has to be
judged on the normalized intent: a virtualised Metal device launches the CPU-pinned
rewrite, and comparing the raw ask against it would reload a healthy server forever."""
bodies = {
node.name: (ast.get_source_segment(SRC, node) or "")
for node in ast.walk(TREE)
if isinstance(node, ast.FunctionDef)
and node.name in ("adopt_load_intent_if_matched", "_runtime_matches_intent")
}
adopt = bodies["adopt_load_intent_if_matched"]
assert "_metal_device_is_paravirtual()" in adopt
assert "paravirtual_normalized_request(" in adopt
# Normalized before the runtime comparison reads it, or the rewrite changes nothing.
assert adopt.index("paravirtual_normalized_request(") < adopt.index("_runtime_matches_intent(")
runtime = bodies["_runtime_matches_intent"]
assert "_diffusion_manual_ngl(intent.gpu_memory_mode, intent.gpu_layers)" in runtime
assert "self.diffusion_requested_ngl" in runtime
def test_requested_split_survives_a_shim_without_the_flag(llama_cpp):
"""gpu_layers reports what is running; diffusion_requested_ngl reports the ask."""
b = _loaded_diffusion(llama_cpp, recorded_layers = -1, requested_ngl = 20)
assert b.gpu_layers == -1
assert b.diffusion_requested_ngl == 20
# ── the capability probe must read code, not prose ──
@pytest.mark.parametrize(
("source", "expected"),
[
('ap.add_argument("--ngl", type=int)', True),
("ap.add_argument('--ngl', type=int)", True), # quoting must not matter
('# someday: support "--ngl"', False), # a comment is not support
('"""usage: --ngl N"""', False), # nor is a docstring
('ap.add_argument("--maxtok", type=int)', False),
],
)
def test_probe_reads_declarations_not_substrings(llama_cpp, tmp_path, source, expected):
shim = tmp_path / "shim.py"
shim.write_text(source + "\n", encoding = "utf-8")
assert llama_cpp._shim_supports_ngl(["python", str(shim)]) is expected
def test_probe_accepts_an_uppercase_extension(llama_cpp, tmp_path):
"""A Windows UNSLOTH_DG_SHIM override may be SHIM.PY; it must still be the file read."""
shim = tmp_path / "SHIM.PY"
shim.write_text('ap.add_argument("--ngl", type=int)\n', encoding = "utf-8")
assert llama_cpp._shim_supports_ngl(["python", str(shim)]) is True
def test_probe_falls_back_to_a_substring_scan_on_unparseable_source(llama_cpp, tmp_path):
shim = tmp_path / "shim.py"
shim.write_text('ap.add_argument("--ngl"\n', encoding = "utf-8") # syntax error
assert llama_cpp._shim_supports_ngl(["python", str(shim)]) is True
# ── the probe must inspect the file that will be spawned, whatever its name ──
@pytest.mark.parametrize("name", ["shim", "shim.pyw", "SHIM.PY"])
def test_probe_keys_on_argv_shape_not_suffix(llama_cpp, tmp_path, name):
"""Any UNSLOTH_DG_SHIM file launches as-is, so the probe must answer for that exact
file; an extensionless or .pyw override used to fall through to the package."""
shim = tmp_path / name
shim.write_text('ap.add_argument("--ngl", type=int)\n', encoding = "utf-8")
assert llama_cpp._shim_supports_ngl(["python", str(shim)]) is True
def test_probe_does_not_mistake_the_module_form_for_a_file(llama_cpp, monkeypatch):
"""[python, -m, unsloth_zoo.diffusion_studio.shim] carries a module name, not a
path; the probe must resolve the installed package, not stat the module string."""
import importlib.util as ilu
monkeypatch.setattr(ilu, "find_spec", lambda name: None)
cmd = ["python", "-m", "unsloth_zoo.diffusion_studio.shim"]
assert llama_cpp._shim_supports_ngl(cmd) is False # unresolvable -> conservative
# ── the guard must mirror what the launcher will actually do ──
def test_split_supported_mirrors_the_launch_gate(llama_cpp, tmp_path, monkeypatch):
b = llama_cpp.LlamaCppBackend()
shim = tmp_path / "shim.py"
shim.write_text('ap.add_argument("--ngl", type=int)\n', encoding = "utf-8")
monkeypatch.setattr(
b, "_find_diffusion_assets", lambda: (["python", str(shim)], "/bin/dg", None)
)
assert b.diffusion_split_supported() is True
shim.write_text('ap.add_argument("--maxtok", type=int)\n', encoding = "utf-8")
assert b.diffusion_split_supported() is False
monkeypatch.setattr(b, "_find_diffusion_assets", lambda: None)
assert b.diffusion_split_supported() is False # no runner -> no split
def test_training_guard_mirrors_shim_support():
"""The zero-layer bypass and the split-scaled estimate are only valid when the
launcher will actually emit --ngl; a dropped split runs GPU-resident."""
route_src = ROUTE_PATH.read_text(encoding = "utf-8")
route_tree = ast.parse(route_src)
fn = next(
n
for n in ast.walk(route_tree)
if isinstance(n, ast.FunctionDef) and n.name == "_guard_chat_load_against_training"
)
body = ast.get_source_segment(route_src, fn) or ""
assert "diffusion_split_supported" in body
assert body.index("diffusion_split_supported") < body.index("diffusion_ngl == 0")
assert "_scale_diffusion_required_gb" in body
# ── a positive split competes with its GPU share, not the whole file ──
@pytest.mark.parametrize(
("required", "ngl", "n_layers", "expected"),
[
(15.0, 10, 30, 5.0), # a third of the layers -> a third of the footprint
(15.0, 30, 30, 15.0), # all layers -> unchanged
(15.0, 99, 30, 15.0), # over-ask clamps to all layers
(15.0, 10, None, 15.0), # unknown layer count stays conservative
(15.0, 10, 0, 15.0), # degenerate header value stays conservative
],
)
def test_positive_split_scales_the_guard_estimate(llama_cpp, required, ngl, n_layers, expected):
assert llama_cpp._scale_diffusion_required_gb(required, ngl, n_layers) == pytest.approx(
expected
)
# ── a custom-named override answers for itself, not a sibling shim.py ──
def test_probe_ignores_a_sibling_shim_next_to_a_custom_override(llama_cpp, tmp_path):
"""An override runs as-is; a capable sibling shim.py must not vouch for it, or the
launch appends --ngl to a parser that exits on it."""
override = tmp_path / "my_shim"
override.write_text('ap.add_argument("--maxtok", type=int)\n', encoding = "utf-8")
sibling = tmp_path / "shim.py"
sibling.write_text('ap.add_argument("--ngl", type=int)\n', encoding = "utf-8")
assert llama_cpp._shim_supports_ngl(["python", str(override)]) is False
# ── a zoo upgrade mid-session must un-stick a dropped split ──
def test_zoo_upgrade_reloads_a_dropped_split(llama_cpp):
"""manual/20 against an old shim launched with the default and deduped on the
ask. Once the shim gains --ngl, the identical ask must reload to apply it."""
b = _loaded_diffusion(llama_cpp, recorded_layers = -1, requested_ngl = 20)
assert _in_target_state(llama_cpp, b, mode = "manual", layers = 20) is True # shim still old
b.diffusion_split_supported = lambda: True # zoo upgraded in this session
assert _in_target_state(llama_cpp, b, mode = "manual", layers = 20) is False # now applies
b2 = _loaded_diffusion(llama_cpp, recorded_layers = 20, requested_ngl = 20)
b2.diffusion_split_supported = lambda: True
assert _in_target_state(llama_cpp, b2, mode = "manual", layers = 20) is True # applied: rest
# ── the dropped split must reach the client ──
def test_response_models_expose_the_requested_split():
"""A refresh has no in-memory split left, so the wire has to carry the ask."""
models_src = (REPO_ROOT / "studio" / "backend" / "models" / "inference.py").read_text(
encoding = "utf-8"
)
tree = ast.parse(models_src)
runtime = next(
n
for n in ast.walk(tree)
if isinstance(n, ast.ClassDef) and n.name == "_InferenceRuntimeFields"
)
fields = {
node.target.id
for node in runtime.body
if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name)
}
assert "diffusion_requested_ngl" in fields
for name in ("LoadResponse", "InferenceStatusResponse"):
cls = next(n for n in ast.walk(tree) if isinstance(n, ast.ClassDef) and n.name == name)
assert any(isinstance(base, ast.Name) and base.id == runtime.name for base in cls.bases)