Skip to content

cmake : remove STATIC from impl libraries, enable LLAMA_BUILD_APP by default - #23462

Merged
ggerganov merged 3 commits into
masterfrom
gg/impl-shared-libs
May 21, 2026
Merged

ggerganov merged 3 commits into
masterfrom
gg/impl-shared-libs

Conversation

@ggerganov

Copy link
Copy Markdown
Member

Overview

  • Remove explicit STATIC from all -impl libraries (server, cli, completion, bench, batched-bench, fit-params, quantize, perplexity) so BUILD_SHARED_LIBS controls shared vs static linkage
  • Add WINDOWS_EXPORT_ALL_SYMBOLS ON for proper DLL export on Windows when built as shared libraries
  • Enable LLAMA_BUILD_APP by default

Additional information

Addresses comment and comment from PR #23296.

Requirements

@github-actions github-actions Bot added build Compilation issues examples server labels May 21, 2026
ggerganov added 3 commits May 21, 2026 19:02
…ntrol

Remove explicit STATIC from all -impl libraries (server, cli, completion, bench,
batched-bench, fit-params, quantize, perplexity) so BUILD_SHARED_LIBS controls
shared vs static linkage.

Add WINDOWS_EXPORT_ALL_SYMBOLS ON for proper DLL export on Windows.

Assisted-by: llama.cpp:local pi
Assisted-by: llama.cpp:local pi
@ggerganov
ggerganov force-pushed the gg/impl-shared-libs branch from 07bb337 to 7bd176e Compare May 21, 2026 16:02
@ggerganov
ggerganov marked this pull request as ready for review May 21, 2026 16:03
@ggerganov
ggerganov requested review from a team and ngxson as code owners May 21, 2026 16:03
@ggerganov
ggerganov requested a review from angt May 21, 2026 16:03
@ggerganov
ggerganov merged commit bb28c1f into master May 21, 2026
8 of 40 checks passed
@ggerganov
ggerganov deleted the gg/impl-shared-libs branch May 21, 2026 18:14
@CISC

CISC commented May 21, 2026

Copy link
Copy Markdown
Member

@cesarb

cesarb commented May 22, 2026

Copy link
Copy Markdown

This change seems to have broken the Vulkan docker image.

Build command: podman build --file=.devops/vulkan.Dockerfile --target=server --tag=llama.cpp:server-vulkan-`git describe --tags` .

Running llama.cpp:server-vulkan-b9275-5-gee7c30578a works.

Running llama.cpp:server-vulkan-b9275-7-g4f0e43da6f fails with:

/app/llama-server: error while loading shared libraries: libllama-common.so.0: cannot open shared object file: No such file or directory

ProTekk referenced this pull request in ProTekk/buun-llama-cpp May 22, 2026
…default (#23462)

* cmake : remove STATIC from impl libraries, allow BUILD_SHARED_LIBS control

Remove explicit STATIC from all -impl libraries (server, cli, completion, bench,
batched-bench, fit-params, quantize, perplexity) so BUILD_SHARED_LIBS controls
shared vs static linkage.

Add WINDOWS_EXPORT_ALL_SYMBOLS ON for proper DLL export on Windows.

Assisted-by: llama.cpp:local pi

* cmake : enable LLAMA_BUILD_APP by default

Assisted-by: llama.cpp:local pi

* ci : disable app in build-cmake-pkg.yml
Alex7MV pushed a commit to Alex7MV/claude_llama.cpp that referenced this pull request May 22, 2026
…default (ggml-org#23462)

* cmake : remove STATIC from impl libraries, allow BUILD_SHARED_LIBS control

Remove explicit STATIC from all -impl libraries (server, cli, completion, bench,
batched-bench, fit-params, quantize, perplexity) so BUILD_SHARED_LIBS controls
shared vs static linkage.

Add WINDOWS_EXPORT_ALL_SYMBOLS ON for proper DLL export on Windows.

Assisted-by: llama.cpp:local pi

* cmake : enable LLAMA_BUILD_APP by default

Assisted-by: llama.cpp:local pi

* ci : disable app in build-cmake-pkg.yml
gabe-l-hart added a commit to gabe-l-hart/llama.cpp that referenced this pull request May 22, 2026
* origin/master:
server: only parse empty msg if continuing an assistant msg (ggml-org#23506)
perplexity : fix integer overflow (ggml-org#23496)
SYCL: improve MoE prefill throughput (ggml-org#23142)
sycl : Level Zero detection in ggml_sycl_init (ggml-org#23097)
SYCL : gated_delta_net K>1 (ggml-org#23174)
SYCL: add BF16 to DMMV kernel path (~4x tg speedup on Intel Arc) (ggml-org#21580)
docs: Update documentation with Granite 4.0/4.1 (ggml-org#23404)
ggml-zendnn : add Q8_0 quantization support (ggml-org#23414)
cmake : build router app only during standalone builds (ggml-org#23521)
vocab : fix HybridDNA tokenizer (ggml-org#23466)
cmake : add install() for impl libraries + fix apple builds (ggml-org#23511)
CUDA: fix PDL CC check for JIT compilation (ggml-org#23471)
cmake : remove STATIC from impl libraries, enable LLAMA_BUILD_APP by default (ggml-org#23462)
Update WebGPU support and add link to blog/demo (ggml-org#23483)
vulkan: fuse snake activation (mul, sin, sqr, mul, add) (ggml-org#22855)
baramofme pushed a commit to baramofme/llama-cpp-turboquant that referenced this pull request May 23, 2026
…default (ggml-org#23462)

* cmake : remove STATIC from impl libraries, allow BUILD_SHARED_LIBS control

Remove explicit STATIC from all -impl libraries (server, cli, completion, bench,
batched-bench, fit-params, quantize, perplexity) so BUILD_SHARED_LIBS controls
shared vs static linkage.

Add WINDOWS_EXPORT_ALL_SYMBOLS ON for proper DLL export on Windows.

Assisted-by: llama.cpp:local pi

* cmake : enable LLAMA_BUILD_APP by default

Assisted-by: llama.cpp:local pi

* ci : disable app in build-cmake-pkg.yml
srossitto79 pushed a commit to srossitto79/llama.cpp that referenced this pull request May 23, 2026
…default (ggml-org#23462)

* cmake : remove STATIC from impl libraries, allow BUILD_SHARED_LIBS control

Remove explicit STATIC from all -impl libraries (server, cli, completion, bench,
batched-bench, fit-params, quantize, perplexity) so BUILD_SHARED_LIBS controls
shared vs static linkage.

Add WINDOWS_EXPORT_ALL_SYMBOLS ON for proper DLL export on Windows.

Assisted-by: llama.cpp:local pi

* cmake : enable LLAMA_BUILD_APP by default

Assisted-by: llama.cpp:local pi

* ci : disable app in build-cmake-pkg.yml
danielhanchen added a commit to unslothai/unsloth that referenced this pull request May 24, 2026
…5741 comments (#5746)

* ci: broaden Linux llama.cpp runtime pattern to lib*.so*

#5741 patched the explicit Linux pattern list to add
``libllama-*-impl.so*`` after ggml-org/llama.cpp#23462 (between
b9279 and b9283) split each binary's entry code into a paired
``lib<binary>-impl.so`` shared library. Same class of upstream
repackaging will hit us again whenever a new shared lib is added.

Mirror what macOS already does and replace the per-lib list with a
single ``lib*.so*`` glob. ``copy_globs`` (line 3614) unions
patterns, so the per-variant ``libggml-cuda.so*`` / ``libggml-hip.so*``
entries were never filtering anything; the spec lives in
``runtime_payload_health_groups`` (line 5209) which keeps the
explicit minimum-required list per variant.

Dry-run against b9296-bin-ubuntu-x64.tar.gz: 40 files copied (all
ggml, llama, mtmd, impl variants + the two binaries we ship), 22
skipped (other CLIs, rpc-server, LICENSE). Functionally equal to
the post-#5741 set.

* cleanup: trim #5741 comments on the pydantic split

Comments added in #5741 explained the original bug in full each
time. They are mostly redundant with the commit message and the PR.
Trim them to one short paragraph per site.

No behavior change.

* ci: narrow Windows runtime pattern to llama-server.exe + llama-quantize.exe

Studio only invokes llama-server and llama-quantize. Mac and Linux
already filter to those two binaries; Windows was the odd one out
with ``*.exe`` copying every CLI upstream ships (llama-cli,
llama-bench, llama-mtmd-cli, ...).

Dry-run on b9296 (win cpu-x64, cpu-arm64, cuda-13.1, hip-radeon):
20 unused EXEs skipped per variant, all DLLs (incl. the new
llama-*-impl.dll family) still copied via ``*.dll``.

``existing_install_matches_choice`` already checks llama-server.exe
exists explicitly (line 5297), so the health gate is unchanged.
rhsCZ pushed a commit to rhsCZ/unsloth that referenced this pull request May 24, 2026
…nslothai#5741 comments (unslothai#5746)

* ci: broaden Linux llama.cpp runtime pattern to lib*.so*

unslothai#5741 patched the explicit Linux pattern list to add
``libllama-*-impl.so*`` after ggml-org/llama.cpp#23462 (between
b9279 and b9283) split each binary's entry code into a paired
``lib<binary>-impl.so`` shared library. Same class of upstream
repackaging will hit us again whenever a new shared lib is added.

Mirror what macOS already does and replace the per-lib list with a
single ``lib*.so*`` glob. ``copy_globs`` (line 3614) unions
patterns, so the per-variant ``libggml-cuda.so*`` / ``libggml-hip.so*``
entries were never filtering anything; the spec lives in
``runtime_payload_health_groups`` (line 5209) which keeps the
explicit minimum-required list per variant.

Dry-run against b9296-bin-ubuntu-x64.tar.gz: 40 files copied (all
ggml, llama, mtmd, impl variants + the two binaries we ship), 22
skipped (other CLIs, rpc-server, LICENSE). Functionally equal to
the post-unslothai#5741 set.

* cleanup: trim unslothai#5741 comments on the pydantic split

Comments added in unslothai#5741 explained the original bug in full each
time. They are mostly redundant with the commit message and the PR.
Trim them to one short paragraph per site.

No behavior change.

* ci: narrow Windows runtime pattern to llama-server.exe + llama-quantize.exe

Studio only invokes llama-server and llama-quantize. Mac and Linux
already filter to those two binaries; Windows was the odd one out
with ``*.exe`` copying every CLI upstream ships (llama-cli,
llama-bench, llama-mtmd-cli, ...).

Dry-run on b9296 (win cpu-x64, cpu-arm64, cuda-13.1, hip-radeon):
20 unused EXEs skipped per variant, all DLLs (incl. the new
llama-*-impl.dll family) still copied via ``*.dll``.

``existing_install_matches_choice`` already checks llama-server.exe
exists explicitly (line 5297), so the health gate is unchanged.
Alex7MV pushed a commit to Alex7MV/claude_llama.cpp that referenced this pull request May 27, 2026
…default (ggml-org#23462)

* cmake : remove STATIC from impl libraries, allow BUILD_SHARED_LIBS control

Remove explicit STATIC from all -impl libraries (server, cli, completion, bench,
batched-bench, fit-params, quantize, perplexity) so BUILD_SHARED_LIBS controls
shared vs static linkage.

Add WINDOWS_EXPORT_ALL_SYMBOLS ON for proper DLL export on Windows.

Assisted-by: llama.cpp:local pi

* cmake : enable LLAMA_BUILD_APP by default

Assisted-by: llama.cpp:local pi

* ci : disable app in build-cmake-pkg.yml
danielhanchen added a commit to unslothai/unsloth that referenced this pull request May 27, 2026
* Studio: expose --parallel / -np on `unsloth studio run`

The CLI was hardcoding `llama_parallel_slots=4` in `run_kwargs` at
`unsloth_cli/commands/studio.py`, leaving users unable to tune the
concurrent decode slot count even though the engine, KV-cache math,
and `studio.backend.run.run_server(llama_parallel_slots=...)`
plumbing all already accepted any N. This change adds a `--parallel`
/ `--n-parallel` / `-np` typer option (default 4 -- matches the
previous hardcoded value), forwards it into `run_kwargs`, and pins
the new surface with 4 unit tests.

Per-request state in `routes/inference.py` is already isolated
(`cancel_event` and `prev_text` are per-request locals in every
streaming handler; the `_lock` / `_serial_load_lock` only wrap
load/unload, not chat completions), so no concurrency refactor is
needed alongside this -- the engine layer already handles N
concurrent requests on one loaded model when llama-server is told
to.

Range guards: 1 <= N <= 64. With higher N each slot gets ctx/N KV
cache; users tuning this should be aware that per-call context
shrinks proportionally.

`unsloth studio` (the bare default command, no subcommand) still
defaults to llama_parallel_slots=1 via `run_server`'s own default;
this PR does not change that path -- it only exposes the knob on the
one-liner `studio run` command that already silently used 4.

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

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

* Forward --parallel through venv re-exec and drop colliding short aliases

`unsloth studio run` re-execs into the Studio venv when invoked from
outside it (the common path). The arg-builder forwards every typer
option but the new --parallel, so the child re-execs at the default 4
and any user value is silently dropped. Worse: pre-PR users who
already pass `-np N` as a pass-through extra (where llama.cpp's
last-wins parsing made it stick) silently lose N after this PR lands.
Forward --parallel explicitly in the re-exec arg list.

While auditing the re-exec path, also drop the colliding 1-char
short aliases -m (--model) and -f (--frontend) plus the redundant
-hfr. Click's short-option clustering had been silently mis-parsing
~11 llama-server short flags via the pass-through path: -fa as
`-f a`, -mg 0 as `-m g` + stray 0, -fitt 1024 as `-f itt` + stray
1024, -hff path as `-f f` + stray `-h path`, -cmoe / -cram / -sm /
-ncmoe etc. The docstring promise ("any flag this command does not
recognize is forwarded verbatim") was silently violated.

-hf (2-char) is kept because Click treats multi-char shorts atomically
(no clustering of -hff / -hfv / -hffv / -hft) and -hf is documented
in basics/api/README.md. --model / --hf-repo / --frontend long forms
all unchanged. studio_default keeps -f because it has no pass-through.

Tests:
- test_studio_run_parallel_flag.py: 8 new re-exec coverage cases
  (all 3 aliases, 3 platforms via sys.platform mock, pre-PR `-np`
  regression, mixed with pass-through extras).
- test_studio_run_short_alias_clashes.py (new): surface checks that
  the removed shorts cannot reappear, plus 11 parametrized cases
  proving each previously-broken llama-server short flag now passes
  through verbatim, plus a happy-path test that documented -hf still
  works for `org/repo:variant` syntax.

All 27 tests pass. Negative test (revert either fix) shows the new
tests catch the regression.

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

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

* Fix stale studio run docstring describing rejected llama-server flags

The pre-PR docstring listed --port, -c / --ctx-size, --api-key, -ngl,
--jinja, --flash-attn, --no-context-shift as "rejected with HTTP 400",
but only --port and --api-key (plus other networking / auth / model
identity / single-model UI flags) are actually in
studio/backend/core/inference/llama_server_args.py's denylist. -c /
-ngl / --jinja / --flash-attn / --no-context-shift are pass-through
and last-wins-override Studio's auto-set value.

Rewrite the docstring to match the real denylist groups and point at
the canonical source. Also add --parallel to one of the examples now
that it is a first-class flag.

* ci: broaden Linux + narrow Windows llama.cpp runtime patterns + trim #5741 comments (#5746)

* ci: broaden Linux llama.cpp runtime pattern to lib*.so*

#5741 patched the explicit Linux pattern list to add
``libllama-*-impl.so*`` after ggml-org/llama.cpp#23462 (between
b9279 and b9283) split each binary's entry code into a paired
``lib<binary>-impl.so`` shared library. Same class of upstream
repackaging will hit us again whenever a new shared lib is added.

Mirror what macOS already does and replace the per-lib list with a
single ``lib*.so*`` glob. ``copy_globs`` (line 3614) unions
patterns, so the per-variant ``libggml-cuda.so*`` / ``libggml-hip.so*``
entries were never filtering anything; the spec lives in
``runtime_payload_health_groups`` (line 5209) which keeps the
explicit minimum-required list per variant.

Dry-run against b9296-bin-ubuntu-x64.tar.gz: 40 files copied (all
ggml, llama, mtmd, impl variants + the two binaries we ship), 22
skipped (other CLIs, rpc-server, LICENSE). Functionally equal to
the post-#5741 set.

* cleanup: trim #5741 comments on the pydantic split

Comments added in #5741 explained the original bug in full each
time. They are mostly redundant with the commit message and the PR.
Trim them to one short paragraph per site.

No behavior change.

* ci: narrow Windows runtime pattern to llama-server.exe + llama-quantize.exe

Studio only invokes llama-server and llama-quantize. Mac and Linux
already filter to those two binaries; Windows was the odd one out
with ``*.exe`` copying every CLI upstream ships (llama-cli,
llama-bench, llama-mtmd-cli, ...).

Dry-run on b9296 (win cpu-x64, cpu-arm64, cuda-13.1, hip-radeon):
20 unused EXEs skipped per variant, all DLLs (incl. the new
llama-*-impl.dll family) still copied via ``*.dll``.

``existing_install_matches_choice`` already checks llama-server.exe
exists explicitly (line 5297), so the health gate is unchanged.

* Lower default weight_decay in RL config from 0.01 to 0.001 (#5747)

In full FT, AdamW weight decay shrinks the parameter directly so the
implicit prior is W -> 0. In LoRA the trained parameters are A and B
while the effective weight is W = W_init + (alpha/r) * B @ A; decaying
A and B separately drives BA -> 0, hence W -> W_init rather than 0.
The previous default of 0.01 inherited from full-FT recipes adds a
measurable pull on the merged adapter back toward the base model over
a few thousand steps. 0.001 keeps a small Frobenius-norm prior on
||A||^2 + ||B||^2 for numerical stability without meaningfully biasing
the merged weight toward init, and aligns with the value used across
the unsloth notebook templates.

* Studio: strip orphan tool_call XML leaking into visible content (#5735)

* Studio: strip orphan tool_call XML from streamed visible content

The speculative-buffer state machine in
`studio/backend/core/inference/llama_cpp.py` can slice a tool_call XML
block between the silent DRAINING path and the user-visible
content_accum, depending on when in the model's emission the BUFFERING
-> STREAMING -> DRAINING transitions fire. Three leak shapes were
observed in a 2026-05-22 sweep of 900 Qwen3.5 / Qwen3.6 GGUF runs:

  Pre-fix XML leak rate: 20/900 (2.22%), concentrated 6.7% on the
  larger Q8 / MTP configs:

    Qwen3.6-35B-A3B Q8_0         4/60  (6.7%)
    Qwen3.6-35B-A3B-MTP Q4       4/60  (6.7%)
    Qwen3.5-35B-A3B Q8_0         3/60  (5.0%)
    Qwen3.6-27B Q8_0             3/60  (5.0%)

The existing `_TOOL_XML_RE` only matched well-formed
`<tool_call>...</tool_call>` and `<function=...></function>` pairs, so
unterminated openings (close was DRAINED) and orphan closes (opening
was DRAINED) survived the strip and reached the user.

Fix relaxes the regex to also strip:
  1. Orphan opening up to end-of-string: `(?:</tool_call>|\Z)`
  2. Orphan closing tag: bare `</tool_call>` / `</function>`

Verified on the full sweep: 20/900 -> 0/900 (100% of detected leaks
eliminated). 16 unit tests in `test_tool_xml_strip.py` pin all three
leak shapes plus the well-formed cases, plus parametrised checks on
the 5 actual real-world leak samples from the sweep data.

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

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

* Studio: strip tail-only </parameter> orphan + tighten regex

The 2026-05-22 gdpval sweep surfaced a 4th XML-leak shape not caught
by the earlier regex: a bare `</parameter>\n\n` at end-of-buffer (7
of 192 trials, all Qwen3.5-27B + a few Qwen3.6-27B). The model emits
the full `<tool_call><function=...><parameter=...>...content...
</parameter></function></tool_call>` envelope, the speculative buffer
DRAINS the opening tags as intended, but EOS (max_tokens cutoff)
truncates the outer `</function></tool_call>` close, leaving just
`</parameter>` as the visible tail.

We strip this ONLY when end-anchored (`\s*\Z`) so legitimate
mid-text uses (user code samples, documentation discussing the
Qwen tool-call XML shape) survive. Verified on the 192-trial
gdpval corpus: before=7, after=0.

While at it, fold the five top-level alternations into three by
sharing tag-name and prefix subgroups:

  <tool_call>...    + <function=\w+>...    +    -->  <(?:tool_call|function=\w+)>...
  </tool_call>      | </function>                  -->  </(?:tool_call|function)>

Semantically identical (verified by replay over the 192-trial
corpus + adversarial inputs, 0 diffs) and 1.34x faster on real
workloads. Backtracking-safety pinned by two new perf guards
(256KB '<' spam, 1000x orphan opens).

Tests: 16 -> 28 (6 new functional + 4 well-formed-vs-orphan +
2 perf guards).

* Tighten comments in XML-strip regex and tests

Code says what it does; comments were repeating it. Strip the verbose
explanations down to the WHY-only bits (engine quirk, tail-anchor
rationale, real-world source of each test sample). No code changes.

inference.py:  21 -> 12 lines around _TOOL_XML_RE
test_tool_xml_strip.py: 343 -> 259 lines (-84)
Tests: 28/28 still pass.

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Address review: deny pass-through --parallel, preserve legacy short aliases, fix test harness

Round 1 review fixes for #5737:

1. Deny --parallel / --n-parallel / -np in the pass-through validator.
   Without this, `unsloth studio run --model X --parallel 8 -- --parallel
   999` would last-win-override the running llama-server slot count while
   Studio's app.state.llama_parallel_slots and KV-cache fitting stay at
   the typer value (8), so the resource plan and the running process
   disagree. Also bypasses the typer 1..64 range guard. Reject so the
   only path is the first-class typer flag.

2. Backwards-compat shim for -m / -hfr / -f. Dropping the short aliases
   from typer broke any script using `unsloth studio run -m X` or
   `-hfr Y` or `-f dist`. Add _consume_legacy_short_aliases which pops
   EXACT whole-token matches (or `-x=value` inline form) from ctx.args
   into the corresponding typer parameter. Clustered tokens (`-fa`,
   `-mg`, `-fitt`, ...) are left in the pass-through tail unchanged.
   --model becomes Optional with an explicit missing-required check
   after the preprocessor so legacy `-m X` still satisfies the
   "must specify a model" requirement.

3. Drop mix_stderr from CliRunner. Typer 0.25.1 / Click 8.4.1 removed
   the kwarg; the test harness raised TypeError before exercising the
   PR behaviour. Tests run cleanly on current and older Typer/Click.

4. Correct the -np regression test docstring. Pre-PR `-np 8` was
   clustered by Click as `-p 8` (port=8) + stray `-n`, silently
   breaking the port binding -- not "passed through as 8 slots". The
   post-PR assertion (child gets --parallel 8) is unchanged.

5. Update studio run docstring listing rejected flags so it now
   correctly includes --parallel / -np / --n-parallel.

New tests:
- test_llama_server_args.py: parametrized denylist coverage for
  --parallel / --n-parallel / -np including equals-form, including
  out-of-range bypass attempts (999, 0). is_managed_flag flips True.
- test_studio_run_short_alias_clashes.py: legacy -m / -hfr / -f
  promote to typer params; --model X + -m Y conflict errors; clustered
  -mg / -fa / -fitt still pass through (the original bug fix holds).

132 tests pass (98 backend + 34 cli).

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

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

* Extend legacy-alias shim tests for repo:variant, inline value form, and missing model

Three additional edge cases for the -m / -hfr / -f preprocessor:
- `-m unsloth/foo:UD-Q4_K_XL` round-trips through both the preprocessor
  and _split_repo_variant so the child sees --model + --gguf-variant.
- `-m=foo` inline value form is promoted just like `-m foo`.
- Missing --model after the preprocessor raises typer.Exit(2) cleanly
  (replacing typer's pre-PR required-flag enforcement now that --model
  is Optional to allow the legacy promotion path).

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

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

* Scrub .github/workflows for staging push (matches staging base)

* Fix studio CLI argv handling and pass-through docstring drift

- studio/backend/core/inference/llama_server_args.py: drop the stale
  ``-np``/``--parallel`` entry from the docstring's pass-through tunable
  list. These flags moved into _DENYLIST_GROUPS so the docstring now
  contradicts the validator and would mislead future maintainers
  debugging the ValueError from validate_extra_args(["--parallel","8"]).
  The deleted wording was introduced by dbea77e ("Studio: forward
  llama-server args from `unsloth studio run`, activate `unsloth run`,
  and allow passing model:quant to load models") when --parallel was
  still a documented pass-through; the same commit's "quant" reference
  is about the model:quant syntax, unrelated to the parallel slot
  wording being deleted here.

- unsloth_cli/commands/studio.py: add _expand_attached_np_short next to
  _consume_legacy_short_aliases. Both work around Click's short-option
  clustering for this command -- the legacy preprocessor for `-m` / `-f`
  / `-hfr` and this one for the attached `-np<N>` form. Click clusters
  `-np8` as `-n -p 8` because `-p` is the typer short for `--port`,
  silently setting port=8 and dropping the parallel value; rewriting the
  attached form into separated `-np <N>` in sys.argv before Click
  parses preserves the user's value. Space/equals forms (`-np 8`,
  `-np=8`) already work and are left alone.

- unsloth_cli/__init__.py: import _expand_attached_np_short from the
  studio command and run it only when argv[0] looks like the unsloth
  console-script or workspace cli.py, so importing this module from a
  notebook or pytest run does not mutate the caller's argv.

* Tighten the -np canonicaliser comments

Drop the helper's co-location sentence (location is self-evident from
grep) and shorten the entry-gate rationale to one short sentence
covering the why.

* Sync .github/workflows with upstream author branch

* Sync .github/workflows with upstream author branch

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

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

* Bump install.sh / install.ps1 pin to unsloth>=2026.5.7 (#5753)

PyPI release unsloth 2026.5.7 is now live. Bumps the pinned floor in
install.sh and install.ps1 from unsloth>=2026.5.6 to unsloth>=2026.5.7
so fresh installs resolve to the new wheel.

Tagged on main as v0.1.416-beta.

* Catch attached `-np<N>` form in backend pass-through validator

The CLI-side `_expand_attached_np_short` rewrites `-np8` to `-np 8`
before Click parses, but HTTP /load `llama_extra_args=["-np8"]` goes
straight to `validate_extra_args` which only matched the exact token.
Reproducer: `validate_extra_args(["-np8"])` previously returned
`["-np8"]` instead of raising; once forwarded to llama-server it
last-win-overrode Studio's slot count while
`app.state.llama_parallel_slots` stayed at the typer value.

Normalise `-np<digits>` to `-np` in `_flag_name` so the denylist
catches the attached form alongside `-np`, `-np=8`, `--parallel`,
`--parallel=8`, and `--n-parallel`. Tests parametrize the new form
including out-of-range values.

* Restore _consume_legacy_short_aliases unit tests + _expand_attached_np_short tests

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

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

* Restore .github/workflows from origin/main

Earlier merge from claude_review's staging-scrub commits accidentally
deleted production CI workflows. Restore them to main's state.

* Scrub .github/workflows for staging push (matches staging base)

* Sync .github/workflows with upstream author branch

* Round 5+6: broaden -np gate to exact basenames + runtime parallel test

Reviewer-flagged improvements squashed into one commit so the auto-push
review bot doesn't keep stomping the branch:

- unsloth_cli/__init__.py: exact-basename match instead of
  endswith('cli.py'). Covers unsloth, unsloth.exe, unsloth-cli,
  unsloth-cli.exe, cli.py, unsloth-cli.py. A third-party mycli.py that
  happens to import unsloth_cli no longer has its argv mutated.

- unsloth_cli/tests/test_studio_run_parallel_flag.py: parametrised
  runtime test (N in {1, 4, 8, 64}) that fakes the in-venv path and
  asserts run_server is invoked with llama_parallel_slots=N.
  Complements the existing source-text check so refactors that preserve
  runtime semantics don't trip a false failure.

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

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

* Round 7: respect '--' end-of-options and reject flag-as-value

Round 7 reviewer flagged three legitimate edge cases:

- _expand_attached_np_short rewrote post-'--' tokens. Convention: '--'
  ends option processing; payload after it is raw. Stop the loop there.

- _consume_legacy_short_aliases promoted post-'--' legacy aliases for
  the same reason. Treat post-'--' tail as raw.

- Legacy '-m -fa' silently consumed '-fa' as the model name, hiding
  the real CLI shape error. Reject any next-token that starts with '-'
  (except the lone '-' stdin/path sentinel) with a clear BadParameter.

Also expanded the missing-model error string to mention the still-
supported legacy '-m' / '-hfr' aliases so users hitting that diagnostic
on legacy scripts get the right migration hint.

Added four regression tests covering each new behaviour.

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

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

* Round 8: soften flag-as-value to long-form only + normalise is_managed_flag

Round 8 reviewer flagged two cleanups:

- _consume_legacy_short_aliases rejected any next token starting with
  '-' as a flag, which would break legitimate values like '-foo'
  (path or model name with leading dash). Narrow the rejection to
  '--long' tokens only; '-x' short forms still pass through.

- is_managed_flag did raw _DENYLIST membership while validate_extra_args
  goes through _flag_name first, so '-np8' / '--parallel=8' /
  '--port=9000' classified as not-managed by the helper but rejected
  by the validator. Route is_managed_flag through _flag_name so the
  two helpers agree on every form callers might use.

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

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

* Round 9: also catch -np-1 / -np+1 signed attached forms in denylist

Round 9 reviewer noticed _flag_name normalised -np<digits> but missed
signed variants -np-1 and -np+1, so validate_extra_args waved them
through while rejecting --parallel -1. llama.cpp would error out on
negative slot counts anyway, but the validator should classify every
form of the managed flag identically so the boundary is consistent.

* Round 10: signed -np in CLI canonicaliser + reject empty inline aliases

Round 10 reviewer flagged two real issues:

- _expand_attached_np_short rewrote only -np<digits>; signed forms
  -np-1 / -np+1 fell through. Backend _flag_name already classifies
  them as managed, so the CLI rewriter must too -- otherwise Click
  clusters -np-1 into -n -p -1 (port=-1) and never reaches the
  backend validator at all.

- -m= / -hfr= / -f= empty inline forms were accepted and produced
  --model '' / --frontend '' (then Path('') silently became '.') on
  re-exec. Reject empty inline values at the preprocessor with a
  clear BadParameter so the malformed input fails fast.

Both behaviours pinned with parametrised regression tests.

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

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

* Expose --parallel on plain `unsloth studio` for API-path parity

The PR added --parallel to `unsloth studio run` but the plain
`unsloth studio` callback (used for API-only / bare-server launches)
still hardcoded llama_parallel_slots to its run_server default. With
--parallel now denied as a llama_extra_args pass-through, that flow
had no first-class way to raise concurrency.

- unsloth_cli/commands/studio.py: add --parallel / --n-parallel typer
  Option (default 4, range 1..64) to studio_default, forward through
  the venv re-exec, and pass llama_parallel_slots= to run_server in
  the in-venv path.
- studio/backend/run.py: argparse --parallel / --n-parallel with the
  same range guard so the spawned child accepts the forwarded flag.
- unsloth_cli/tests/test_studio_run_parallel_flag.py: test pins the
  new option presence, aliases, default and range guards.

* Round 12: narrow entry-point gate, preserve pre-PR plain-studio default, drop brittle source-text test

Three Opus subagent reviewers (security / backcompat / code-quality)
flagged the same handful of real issues. Consensus fixes:

- unsloth_cli/__init__.py: narrow the -np canonicaliser gate to just
  {unsloth, unsloth.exe} (the only pyproject-declared console_script).
  The previous cli.py / unsloth-cli.py entries would silently rewrite
  sys.argv for any third-party myproj/cli.py that happens to import
  unsloth_cli. Dev users running python cli.py ... -np N still work
  via the space form, which parses without the rewrite.

- unsloth_cli/commands/studio.py + studio/backend/run.py: restore the
  pre-PR llama_parallel_slots default of 1 on plain unsloth studio and
  python studio/backend/run.py. unsloth studio run keeps its
  hardcoded-pre-PR default of 4. Without this, my earlier API-path
  parity commit silently dropped per-call context to ctx/4 for the
  plain-studio flow.

- unsloth_cli/tests/test_studio_run_parallel_flag.py: drop the brittle
  source-text grep test (test_run_kwargs_use_parallel_value). The
  parametrised runtime test test_in_venv_path_passes_parallel_to_run_server
  already pins the same intent against actual behaviour.

- unsloth_cli/tests/test_studio_run_short_alias_clashes.py: pin the
  narrow entry-point gate with a parametrised negative test covering
  seven third-party argv[0] basenames (cli.py, /path/myproj/cli.py,
  pytest, unsloth-cli, etc.). Re-broadening the gate now trips a
  test instead of silently mutating an unrelated CLI's argv.

* Round 13: shared parallel constants, denylist invariant test, defence-in-depth

Three Opus subagent reviewers (adversarial-user / maintenance /
cross-file consistency) flagged a consistent set of cleanups; folded
into one commit to avoid the pre-commit.ci force-push race.

unsloth_cli/commands/studio.py:
- Extract _PARALLEL_MIN / _PARALLEL_MAX / _PARALLEL_DEFAULT_RUN /
  _PARALLEL_DEFAULT_PLAIN module-level constants and use them in both
  typer Options (plain studio_default = 1, studio run = 4).
- _expand_attached_np_short now rewrites -np<junk> when the suffix
  starts with a digit (or signed digit) so '-np8x' surfaces as a
  clean '-np takes an int' typer error instead of a baffling
  '--port invalid' complaint after Click clusters '-n -p 8x'.
- Re-exec forwarding emits --load-in-4bit / --no-load-in-4bit
  explicitly in both directions; previously the True default relied
  on both layers sharing the same default forever.
- run() docstring now explicitly says --parallel / -np pass-through
  via llama_extra_args is denied (use the typer flag above).

studio/backend/run.py:
- Mirror the parallel constants and route the argparse default,
  range check, and error message through them. Help text mentions
  the asymmetry with 'unsloth studio run' so direct-launch dev users
  aren't confused by Default 1 in isolation.

studio/backend/core/inference/llama_server_args.py:
- _flag_name strips surrounding whitespace before denylist lookup so
  a caller can't slip a managed flag past the boundary with a
  trailing space (the trimmed form is what downstream parsers see).

Tests:
- New typer-aliases-subset-of-denylist invariant: every alias the
  typer Option claims as --parallel on run() MUST be in the backend
  parallel denylist group. Catches the failure mode where someone
  adds a new alias and forgets the boundary.
- Extended denylist parametrize to cover ~14 previously untested
  aliases (-mu, -dr, -hfv/-hfrv/-hffv family, -mmu, full --ui group,
  --models-preset / --models-autoload / --no-models-autoload).
- Whitespace-padded denylist rejection (' --parallel', '-np ', etc).
- --load-in-4bit re-exec test pinning both polarities + default.
- -np<junk> argv rewriter regression tests.
- Cross-reference headers between the two test files.

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

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

* fix: repair mlx studio base export save_method (#5727)

* Round 14: align backend -np recogniser with CLI rewriter + reject parent --parallel

Round 14 (reviewer.py --parallel 20 with gpt-5.3-codex-spark) flagged
two real P1s and a stale-rebase warning. All three addressed.

- studio/backend/core/inference/llama_server_args.py: widen
  _flag_name so -np<digit-prefix> with trailing junk (-np8x,
  -np-1foo, -np+1bar, -np9zzz) classifies as managed flag -np,
  matching the CLI _expand_attached_np_short rewriter. Without this,
  POST /api/inference/load with llama_extra_args=['-np8x'] slipped
  past the boundary while the CLI canonicalised the same form. The
  two sides now agree on every digit-prefix form.

- unsloth_cli/commands/studio.py: reject --parallel on the
  studio group when a subcommand is invoked. Pre-PR the studio
  callback had no --parallel; my Round 12 addition made
  'unsloth studio --parallel 8 run ...' silently drop the 8
  because typer doesn't propagate parent options into subcommand
  kwargs. Now errors with exit 2 and a message pointing the
  operator at the correct invocation
  ('unsloth studio run --parallel 8 ...').

- Picked up origin/main via merge (parent commit 0caf0526): the
  pre-flight stale-rebase detector found 2 lines on main in
  studio/backend/core/export/export.py missing from PR HEAD.
  Merged cleanly with no conflicts.

Tests:
- Parametrised denylist coverage for -np<digit-prefix>+junk forms.
- New runtime test confirms exit 2 + helpful error when the group
  --parallel is supplied alongside an invoked subcommand.
- Test that the default group --parallel value still lets a
  subcommand resolve (no false-positive rejection).

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

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

* Studio: tighten code comments across --parallel PR

Comment-only pass over the seven PR-touched files; trim verbose
docstrings, collapse multi-line section dividers, and drop
redundant prose that the code already conveys. No behaviour change.

* Studio: trim remaining verbose docstrings missed in last pass

Shorten the test_studio_run_parallel_flag.py module docstring and
the `Re-exec arg-builder coverage` block. No behaviour change.

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

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

* Studio: second comment-tightening pass across PR-touched code

Trim docstrings and inline comments in studio.py, run.py,
llama_server_args.py, and unsloth_cli/__init__.py. No behaviour change;
all 215 tests still pass.

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

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

* Studio: deny --embedding / --rerank / --tools pass-through

`--embedding` and `--rerank` flip llama-server into single-endpoint
mode, which breaks Studio's /v1/chat/completions hop. llama-server's
own `--tools` flag silently stacks on top of Studio's tool policy
resolved by `--enable-tools` / `--disable-tools`.

Add all three (plus the `--embeddings` / `--reranking` plural aliases)
to the boundary denylist so HTTP /load and pass-through extras both
reject them cleanly instead of silently desyncing the server surface.

Test added to the existing `test_denylist_rejects_all_aliases`
parametrize. 220 tests pass.

* Studio: make PR-touched tests robust to minimal envs + Windows

Two cross-OS CI findings:

1. `test_typer_parallel_aliases_are_subset_of_backend_denylist` was
   doing `from core.inference.llama_server_args import _DENYLIST_GROUPS`
   which triggers `core/inference/__init__.py` and pulls in the full
   backend chain (fastapi / structlog / loggers / utils.hardware).
   The invariant only needs the constants tuple, so load the module
   directly via `importlib.util.spec_from_file_location` -- the test
   now runs with just typer + pytest installed.

2. `test_legacy_frontend_alias_still_promotes_to_frontend` asserted
   the literal string `"/tmp/dist"` after the value round-trips through
   `Path()`. On Windows `str(Path("/tmp/dist"))` is `"\tmp\dist"`, so
   the assertion tripped on the same logical path. Compare via
   `Path(x) == Path("/tmp/dist")` so the test passes on every OS.

Both surfaced by the staging-4 cross-OS CI; no production-code change.
220 tests still pass locally.

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

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

* Studio: load llama_server_args.py directly in its unit tests

Same fix as the previous CLI-test commit: import the module via
`importlib.util.spec_from_file_location` instead of
`from core.inference.llama_server_args import ...`, so the test no
longer needs the full backend chain (fastapi / structlog / loggers /
utils.hardware) installed via `core/inference/__init__.py`.

The boundary validator is intentionally dependency-free; its unit
tests should reflect that.

* Fix test_main_composer_has_dir_auto anchor after PR #5784

PR #5784 ("Improve image generation UI") rewrote the message-input
textarea's static `aria-label="Message input"` into a JSX conditional
`aria-label={overlay ? "Image edit instructions" : "Message input"}`
but did not update the RTL bidi-attribute regression test, leaving
the literal-string `find('aria-label="Message input"')` anchor with
no match. The `Repo tests (CPU)` job has been red on main since.

Anchor on the inner `"Message input"` string literal instead -- it
survives both spellings and still pins the same textarea element so
the `dir="auto"` assertion has the right block to inspect.

Verified by re-running the exact CI command:
  954 passed, 3 skipped, 23 deselected (was 948 passed, 1 failed).

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Long Yixing <longyixing331@gmail.com>
turbo-tan pushed a commit to turbo-tan/llama.cpp-tq3 that referenced this pull request Jun 2, 2026
…default (ggml-org#23462)

* cmake : remove STATIC from impl libraries, allow BUILD_SHARED_LIBS control

Remove explicit STATIC from all -impl libraries (server, cli, completion, bench,
batched-bench, fit-params, quantize, perplexity) so BUILD_SHARED_LIBS controls
shared vs static linkage.

Add WINDOWS_EXPORT_ALL_SYMBOLS ON for proper DLL export on Windows.

Assisted-by: llama.cpp:local pi

* cmake : enable LLAMA_BUILD_APP by default

Assisted-by: llama.cpp:local pi

* ci : disable app in build-cmake-pkg.yml
rsd-darshan pushed a commit to rsd-darshan/unsloth that referenced this pull request Jun 3, 2026
…nslothai#5741 comments (unslothai#5746)

* ci: broaden Linux llama.cpp runtime pattern to lib*.so*

unslothai#5741 patched the explicit Linux pattern list to add
``libllama-*-impl.so*`` after ggml-org/llama.cpp#23462 (between
b9279 and b9283) split each binary's entry code into a paired
``lib<binary>-impl.so`` shared library. Same class of upstream
repackaging will hit us again whenever a new shared lib is added.

Mirror what macOS already does and replace the per-lib list with a
single ``lib*.so*`` glob. ``copy_globs`` (line 3614) unions
patterns, so the per-variant ``libggml-cuda.so*`` / ``libggml-hip.so*``
entries were never filtering anything; the spec lives in
``runtime_payload_health_groups`` (line 5209) which keeps the
explicit minimum-required list per variant.

Dry-run against b9296-bin-ubuntu-x64.tar.gz: 40 files copied (all
ggml, llama, mtmd, impl variants + the two binaries we ship), 22
skipped (other CLIs, rpc-server, LICENSE). Functionally equal to
the post-unslothai#5741 set.

* cleanup: trim unslothai#5741 comments on the pydantic split

Comments added in unslothai#5741 explained the original bug in full each
time. They are mostly redundant with the commit message and the PR.
Trim them to one short paragraph per site.

No behavior change.

* ci: narrow Windows runtime pattern to llama-server.exe + llama-quantize.exe

Studio only invokes llama-server and llama-quantize. Mac and Linux
already filter to those two binaries; Windows was the odd one out
with ``*.exe`` copying every CLI upstream ships (llama-cli,
llama-bench, llama-mtmd-cli, ...).

Dry-run on b9296 (win cpu-x64, cpu-arm64, cuda-13.1, hip-radeon):
20 unused EXEs skipped per variant, all DLLs (incl. the new
llama-*-impl.dll family) still copied via ``*.dll``.

``existing_install_matches_choice`` already checks llama-server.exe
exists explicitly (line 5297), so the health gate is unchanged.
zommiommy pushed a commit to zommiommy/llama.cpp that referenced this pull request Aug 18, 2026
…default (ggml-org#23462)

* cmake : remove STATIC from impl libraries, allow BUILD_SHARED_LIBS control

Remove explicit STATIC from all -impl libraries (server, cli, completion, bench,
batched-bench, fit-params, quantize, perplexity) so BUILD_SHARED_LIBS controls
shared vs static linkage.

Add WINDOWS_EXPORT_ALL_SYMBOLS ON for proper DLL export on Windows.

Assisted-by: llama.cpp:local pi

* cmake : enable LLAMA_BUILD_APP by default

Assisted-by: llama.cpp:local pi

* ci : disable app in build-cmake-pkg.yml
danielhanchen added a commit to unslothai/unsloth that referenced this pull request Sep 8, 2026
… the probe asks

Two of the three items from the last round, both measured against a b10360
managed install rather than a fixture.

1. The Linux impl split had no payload group.

ggml-org/llama.cpp#23462 moved each binary's entry code into a lib<binary>-impl
library. Windows already required llama-server-impl.dll, gated on the build the
split landed in. Linux got the same treatment upstream and none of it here: the
groups name the shared libraries only, so libllama-server-impl.so and
libllama-quantize-impl.so were required by nothing.

Measured on a copy of the managed install, removing either one:

  installed_runtime_health -> (True, "")
  _existing_install_runs   -> False

which is the disagreement the whole probe exists to remove, in the direction
that hurts: preflight says Ready and llama-server dies in the loader.

_linux_split_entrypoint_groups adds both, gated the same way the Windows side is
and on the same build number, since it is one upstream commit rather than one
platform's packaging. A source build ships neither whatever the tag says, and a
pre-split archive is healthy without them, so requiring one of those would
reinstall on every check forever.

2. The exact-release reuse decision asked exists() where the probe asks for the
execute bit.

installed_runtime_health asks _entrypoint_is_runnable, because that is what
_find_llama_server_binary asks. existing_install_matches_choice asked exists(),
and its Linux ldd gate reads a non-executable ELF quite happily. Clearing the
execute bit on llama-server, again on a real copy:

  installed_runtime_health -> (False, "llama_runtime_binaries_missing")
  _existing_install_runs   -> False
  exists()                 -> True
  preflight_linux_installed_binaries -> passes

so preflight says broken, repair says the exact release is already installed,
nothing is downloaded, and the next launch says broken again. Both attempts land
in the same place. It now calls _entrypoint_is_runnable, which is the same test
both other answers use.

Tests:

- test_quarantining_a_split_entrypoint_library_is_reported_broken, per library,
  against a managed copy, asserting the two answers agree afterwards.
- test_an_older_monolithic_linux_release_is_not_asked_for_the_impl_libraries,
  which pins the gate rather than the requirement: b9279 owes neither, b9283
  owes both, a source build owes neither.
- test_a_stripped_execute_bit_is_not_reused_as_an_exact_release_match, checking
  the two gates directly.
- The fixture tables gain both libraries, and the shape writers now chmod the
  entrypoints, which a real extraction does and the reuse decision now reads.

tests/studio/install: 5991 passed, 6 skipped. cargo test --bin unsloth-studio:
571 passed.

The third item, a versionless libllama.so surviving its SONAME's removal, does
not apply: the release ships exactly two names per library, libllama.so.0 beside
libllama.so.0.0.10360, with no versionless file and no symlinks. Replied on the
thread.
zbrad pushed a commit to zbrad/llama.cpp that referenced this pull request Sep 10, 2026
…default (ggml-org#23462)

* cmake : remove STATIC from impl libraries, allow BUILD_SHARED_LIBS control

Remove explicit STATIC from all -impl libraries (server, cli, completion, bench,
batched-bench, fit-params, quantize, perplexity) so BUILD_SHARED_LIBS controls
shared vs static linkage.

Add WINDOWS_EXPORT_ALL_SYMBOLS ON for proper DLL export on Windows.

Assisted-by: llama.cpp:local pi

* cmake : enable LLAMA_BUILD_APP by default

Assisted-by: llama.cpp:local pi

* ci : disable app in build-cmake-pkg.yml
danielhanchen added a commit to unslothai/unsloth that referenced this pull request Sep 22, 2026
…t model load (#10494)

* Repair a quarantined llama.cpp runtime at launch instead of failing at model load

Smart App Control and antivirus quarantine individual files out of the managed
llama.cpp tree while leaving the marker and the rest of the tree in place. The
desktop's preflight decided staleness on the managed Python alone, so it reported
Ready, launched, and the missing DLL only surfaced later at model load as an
error that looked like a bad GGUF or an out of memory. Repairing it meant running
`unsloth studio update` by hand, which nothing told the user to do.

install_llama_prebuilt gains installed_runtime_health(), which reuses the same
payload tables the keep-install path already uses rather than duplicating the
required file list. The CLI reports it in the desktop capability payload and the
Tauri preflight treats an explicit false as stale, so the existing repair path
picks it up.

Only an explicit false is stale. None covers both "no runtime installed yet" and
"this CLI predates the field", and neither is a broken runtime: treating them as
stale would send every existing install through repair on its first launch after
an upgrade. For the same reason desktop_manageability_version is not bumped.

The capability cache had to move too. Its fingerprint covered the managed venv
only, so a file quarantined out of the runtime left it identical and preflight
answered Ready from cache without ever asking the CLI. The runtime's file count
and total size now join the fingerprint, and the cache schema goes to 4.

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

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

* Close the gaps the simulations found, and prove the repair cannot loop

The launch probe has to be no stricter than the setup scripts' own
keep-or-reinstall decision. A tree the probe rejects but _existing_install_runs
keeps would be repaired, kept unchanged by the repair and rejected again on the
next launch, which is a loop with no way out. That property is now stated where
the code is and tested as a property over the whole matrix.

Three real gaps closed:

The payload groups are libraries, and on Linux and macOS they name no executable
at all, so a quarantined llama-server passed as a complete install. Both
entrypoints are now looked for, which is what _existing_install_runs already
requires, so the check stays on the safe side of the rule above.

platform_only_host() left macos_version unset while detect_host() derives it
from platform.mac_ver(), which reads no hardware. The parity test would have
failed on a macOS runner.

A relative UNSLOTH_LLAMA_CPP_PATH resolved against the desktop's working
directory, which is not the one the CLI child runs under. Fingerprinting
whatever sits beside the desktop is worse than not fingerprinting, so it now
degrades to no coverage. A leading ~ is expanded, since the Python reader calls
expanduser.

Deliberately not changed, and pinned by a test so it is not "fixed" later: a
marker that exists but does not parse still reads as nothing installed.
Reporting it broken looks right, but confirm_install_tree only checks that the
marker file exists, so the offline keep path keeps such a tree, and broken plus
kept is exactly the loop above.

Tests. 1361 cases over [Windows, Linux, macOS] x [cpu, cuda, rocm, vulkan,
metal] x thirteen shipped marker shapes x damage modes, asserting both the
verdict and the invariant, with the invariant's sensitivity checked by mutation
rather than assumed. Healthy installs of every shipped shape stay healthy on
every platform, which is the backwards-compatibility half. The verdict is
unchanged across NVIDIA, AMD, Intel and no GPU at all. Rust gains JSON round
trips proving a pre-PR payload still parses and reads Ready, that an old cache
file is ignored rather than fatal, and that a newer CLI's unknown keys are
inert. The frontend no longer tells a user with a quarantined runtime that their
install is too old.

* Format the new install-health files with the pinned ruff

scripts/run_ruff_format.py runs whatever ruff is on the path; the hook pins
0.6.9, and these four files were written under a newer one. The two disagree
about assert-message wrapping and about when a def signature goes one parameter
per line, which is what pre-commit.ci reported as "files were modified by this
hook".

* Fix the review round, and two defects the simulations found under it

Seven review items were genuine and are fixed here. Two of them contradicted
calls I had made earlier and the review was right both times.

A marker that exists but does not parse is now graded rather than reported as
nothing installed. I had argued the opposite, on the grounds that the offline
keep path keeps such a tree and broken plus kept is a loop. The narrower split
is the correct one: an absent marker file is a runtime nobody installed, a
present one that will not parse is a real tree, and when it has lost a library
as well, answering None left preflight Ready with no repair offered. The keep
path already reads such a marker as an unknown backend and grades the same
payload, so nothing here asks for more than it does.

A relative UNSLOTH_LLAMA_CPP_PATH was falling through to the legacy tree, which
is a regression I introduced in the commit that was meant to prevent exactly
that. Dropping it to None is not the fix either: None matches None, so the cache
keeps hitting while the real runtime rots. It is anchored to this process's
working directory now, which is where relative_override_pins joins the child's
copy from. On Windows the tilde resolves through USERPROFILE rather than
dirs::home_dir(), because that is what the child uses and a portable profile
moves the second one; process.rs carries the same note at its own read.

A quarantined llama-server was already covered. Managed runtime health is now
reported only when the managed runtime is the one that would actually load,
since _find_llama_server_binary prefers LLAMA_SERVER_PATH and the folder chosen
in settings ahead of it, and grading the managed tree regardless sent a user
running their own build into a repair that offline cannot even succeed.

Two further defects came out of the simulations rather than the review.

A dead Linux runtime was reported healthy. A release ships libllama.so.0 beside
libllama.so.0.0.10360 and the group pattern matches both, so quarantining the
SONAME, which is the name the loader asks for, left the group satisfied by the
twin. Measured against a real install: llama-server exits 127 with a loader
error while the probe said healthy. No fixture could show it, because a fixture
writes one file per library and a release writes two. Dangling symlinks in a
versioned chain were passing for the same reason and are covered by the same
check.

The repair could not terminate. setup.sh skipped the source rebuild whenever the
two entrypoints were executable, which is true of every tree that lost only a
library, so offline the repair returned the tree byte for byte identical and
reported success while the next launch rejected it again. It asks
reusable_existing_install first now. That branch only runs once the prebuilt
path has failed, where keeping a tree that cannot load is never right, and a
tree with no marker is a real source build and keeps the old test.

Tests: 5965 in tests/studio/install, 561 in the Tauri crate. The convergence
census that pinned the loop as a fact now pins its absence, and the strict xfail
holding the SONAME defect is an ordinary assertion.

* Give a runtime tree with no build/bin its own fingerprint

read_dir(build/bin).ok()? answered None for two different states: no llama.cpp
runtime at all, and a runtime tree whose binary directory is gone. The capability
verdict is not the same in both. With no marker, installed_runtime_health answers
None, llama_runtime_ok stays null, and desktop_capability_stale_reason
deliberately does not call that stale, so a Ready gets cached. Once a marker sits
over a missing build/bin the CLI would answer llama_runtime_dir_missing, but the
fingerprint had not moved, the cache still matched, and preflight served the old
Ready without ever asking.

The root's own entries now stand in when build/bin cannot be read, so the marker
arriving moves the key. None still means the tree is not there, which is the
NotInstalled case.

* Catch up the fingerprint tests with the bin and nobin prefixes

The previous commit gave a runtime tree with no build/bin its own fingerprint
and prefixed the walk, which left five assertions on the old shape and the crate
red at 557 of 562.

Three are the format: bin: now leads the count. Two are the semantics the change
introduced, and they are worth stating rather than patching. "No runtime at all"
is the absence of the tree, so its test points at a path that does not exist
instead of an empty directory, which is now the broken-install state. A root that
is present but whose binary directory cannot be read is broken too, not absent:
sharing None with a machine that never installed a runtime is exactly what let a
cache written before an install keep matching after it broke.

* Tighten the comments on the runtime health preflight

Shorten every comment and docstring this branch added, keeping the
reasoning that is load bearing and dropping the restatements of the code
beside them. No code, only comments and whitespace.

* Address review: per-library macOS payload groups, ~user paths, blank runtime override

- installed_runtime_health now checks each macOS library on its own instead of
  three broad globs, so a bundle missing libllama-common, libggml-base or
  libggml-cpu is graded incomplete rather than healthy. Verified against the real
  macos-arm64 bundle: complete is healthy, removing any one library is not, and
  dropping the metal, blas and rpc backends stays healthy.
- llama_runtime_override_from resolves ~user/... through getpwnam on unix rather
  than treating it as a relative path.
- A blank UNSLOTH_LLAMA_CPP_PATH no longer counts as an override.
- The CLI capability payload only reports the managed runtime when the managed
  runtime is the one that would actually run.

Fixtures that built macOS trees from the old broad names now write the names a
real bundle ships.

* Address review: macOS SONAME links, the execute bit, and which runtime is graded

- A dylib chain now follows the same rule as an .so: losing the middle
  libggml.0.dylib left the group satisfied by the terminal libggml.0.23.0.dylib
  while dyld could not launch, since the LC_LOAD_DYLIB entry names the link.
- llama-server and llama-quantize must still carry the execute bit on POSIX.
  _existing_install_runs already asks that way, so an install with the bit
  cleared was rejected there and reported healthy here.
- The CLI capability helper follows the finder's own file status rather than the
  directory entry, so a pinned symlink whose target was quarantined falls
  through to the managed tree the backend really loads instead of suppressing
  its verdict.
- UNSLOTH_LLAMA_CPP_PATH outranks the stored custom folder in the finder and
  default_managed_llama_dir points at exactly that tree, so it is graded even
  when an older selection is still in the settings database.
- ~name on Windows resolves to the sibling profile the same way process.rs and
  ntpath.expanduser resolve it, instead of being fingerprinted under the cwd.
- A runtime verdict the CLI skipped is no longer cached, since nothing in the
  fingerprint watches which runtime is selected and clearing a custom selection
  would otherwise keep serving a Ready nobody had graded.

* Ask for the execute bit by the tree's platform, not the interpreter's

A Windows bundle unpacked on a POSIX filesystem carries no execute bit, and the
host is a parameter here, so os.access X_OK graded every real windows-x64 bundle
in the history suite as missing its binaries. Windows has no execute bit for
os.access to read anyway, so presence is the whole question there.

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

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

* Address review: the fingerprint's blind spots, and which tree it watches

- The runtime fingerprint counted regular files only, so deleting an install-name
  link (libggml.0.dylib, or the SONAME link on Linux) or taking the execute bit
  off llama-server left it identical. Both are states installed_runtime_health
  rejects, so a cached Ready went on matching a tree that could no longer launch.
  Links and executable files are counted now.
- An installer-managed venv under a custom root names its own runtime. The
  desktop strips UNSLOTH_STUDIO_HOME before spawning the CLI, but the CLI
  re-infers the root from sys.prefix and points UNSLOTH_LLAMA_CPP_PATH at its
  llama.cpp, so reading only the ambient environment fingerprinted the legacy
  tree and quarantine in the real one never invalidated anything.
- An override holding no server does not end the search, so it is no longer
  treated as final on either side. The CLI names the tree it grades rather than
  answering yes or no, so the two cannot pick different trees.
- ~name resolves through getpwnam_r. getpwnam hands back process-global storage
  and this runs on a Tauri worker thread, where a concurrent lookup could
  overwrite pw_dir mid-copy.

* Address review: entrypoints that are files, the mode bits, and one override spelling

installed_runtime_health asked os.access(X_OK), which a directory named llama-server
answers true for because directories are searchable, while the finder asks is_file()
and rejects the tree. Both now go through one helper that _existing_install_runs
shares, so the keep-or-reinstall decision and the launch verdict cannot disagree.

The runtime fingerprint counted files with any execute bit, but os.access(X_OK) asks
whether this user may run one: 0755 to 0655 leaves group and other executable and the
owner unable to run it, so the count never moved for a state health rejects. The
permission bits themselves are summed instead.

_find_llama_server_binary read UNSLOTH_LLAMA_CPP_PATH literally while
default_managed_llama_dir, get_stored_custom_llama_cpp_path and the desktop's pinning
all expand it, so a ~/llama.cpp written into a service unit or a .env sent discovery to
a folder named ~ beside the working directory and every other component reported on a
different tree. It expands now, and so does the orphan reaper's allowlist, which has to
name the tree discovery spawns from.

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

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

* Address review: the split entrypoint libraries, and reuse asking what the probe asks

Two of the three items from the last round, both measured against a b10360
managed install rather than a fixture.

1. The Linux impl split had no payload group.

ggml-org/llama.cpp#23462 moved each binary's entry code into a lib<binary>-impl
library. Windows already required llama-server-impl.dll, gated on the build the
split landed in. Linux got the same treatment upstream and none of it here: the
groups name the shared libraries only, so libllama-server-impl.so and
libllama-quantize-impl.so were required by nothing.

Measured on a copy of the managed install, removing either one:

  installed_runtime_health -> (True, "")
  _existing_install_runs   -> False

which is the disagreement the whole probe exists to remove, in the direction
that hurts: preflight says Ready and llama-server dies in the loader.

_linux_split_entrypoint_groups adds both, gated the same way the Windows side is
and on the same build number, since it is one upstream commit rather than one
platform's packaging. A source build ships neither whatever the tag says, and a
pre-split archive is healthy without them, so requiring one of those would
reinstall on every check forever.

2. The exact-release reuse decision asked exists() where the probe asks for the
execute bit.

installed_runtime_health asks _entrypoint_is_runnable, because that is what
_find_llama_server_binary asks. existing_install_matches_choice asked exists(),
and its Linux ldd gate reads a non-executable ELF quite happily. Clearing the
execute bit on llama-server, again on a real copy:

  installed_runtime_health -> (False, "llama_runtime_binaries_missing")
  _existing_install_runs   -> False
  exists()                 -> True
  preflight_linux_installed_binaries -> passes

so preflight says broken, repair says the exact release is already installed,
nothing is downloaded, and the next launch says broken again. Both attempts land
in the same place. It now calls _entrypoint_is_runnable, which is the same test
both other answers use.

Tests:

- test_quarantining_a_split_entrypoint_library_is_reported_broken, per library,
  against a managed copy, asserting the two answers agree afterwards.
- test_an_older_monolithic_linux_release_is_not_asked_for_the_impl_libraries,
  which pins the gate rather than the requirement: b9279 owes neither, b9283
  owes both, a source build owes neither.
- test_a_stripped_execute_bit_is_not_reused_as_an_exact_release_match, checking
  the two gates directly.
- The fixture tables gain both libraries, and the shape writers now chmod the
  entrypoints, which a real extraction does and the reuse decision now reads.

tests/studio/install: 5991 passed, 6 skipped. cargo test --bin unsloth-studio:
571 passed.

The third item, a versionless libllama.so surviving its SONAME's removal, does
not apply: the release ships exactly two names per library, libllama.so.0 beside
libllama.so.0.0.10360, with no versionless file and no symlinks. Replied on the
thread.

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

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

* Address review: resolve a named-user override the same way on both sides of a spawn

preflight::managed resolves UNSLOTH_LLAMA_CPP_PATH=~alice/llama.cpp through
getpwnam_r, the way posixpath.expanduser does. process.rs, pinning the same
variable for the child it is about to move, read it with the Windows rule, which
leaves ~alice alone off Windows, so it fell through to being anchored under the
directory being left.

Measured, with the two halves side by side on this host:

  preflight::managed  ~root/llama.cpp -> /root/llama.cpp
  process.rs pin      ~root/llama.cpp -> /mnt/work/session/~root/llama.cpp

The child is handed the pinned value, so it grades and runs one tree while the
cache fingerprints another. Quarantining a file from the tree actually in use
then never invalidates a Ready verdict, which is the whole failure this
fingerprint exists to catch.

expand_user now makes the split every other reader in that function already
makes, Windows rules on Windows and the native ones off it, and expand_posix_user
resolves the named form through the same lookup preflight::managed uses, which is
now pub(crate) rather than duplicated. An unknown name is left exactly as it
arrived, which is what expanduser does, so both halves fall through to anchoring
it and still agree. Bare ~ and ~/rest are unchanged: they went through
expand_windows_user's empty-name arm, which answers home on any platform, and
they go through the posix one now.

Windows was already aligned. named_user_home's Windows arm delegates to
named_windows_user_home, which is the same rule expand_windows_user applies.

Tests, all three unix-only since they turn on the password database:

- a_named_user_override_is_pinned_where_the_fingerprint_watches, asserting the
  pin is not under the directory being left and equals what the fingerprint side
  reaches through the same lookup.
- an_unknown_named_user_is_left_alone_and_anchored_by_both_halves.
- a_bare_tilde_override_still_reaches_home_off_windows, for the form that already
  worked.

cargo test --bin unsloth-studio: 574 passed. tests/studio/install: 5991 passed,
6 skipped.

* Address review: the SONAME beside a flattened versionless copy, and an override that cannot expand

Two items, and the first one corrects a rebuttal of mine that was wrong.

1. A versionless copy kept the group satisfied after the SONAME was quarantined.

I argued against this last round on the grounds that the bundle ships exactly two
names per library. That was true of the tree I measured, the b10360 install on
this machine, and it is not true of what the installer produces today. Two things
moved underneath it:

  - b10840 ships the trio. libllama.so -> libllama.so.0 -> libllama.so.0.4.0, as
    symlinks in the tarball.
  - copy_globs flattens them. It selects with is_file() and copies with
    shutil.copy2, which follows symlinks, so all three arrive in build/bin as
    independent regular files. My reproduction moved the extracted tree instead,
    which kept the links, and a dangling link is dropped by is_file() on its own.
    That made the argument look decisive about a shape no install has.

Extracting b10840 with prebuilt_core.extract_archive and installing it through
copy_globs, which is the path install_prebuilt takes:

  baseline                          health=(True, "")
  quarantine libllama.so.0          health=(True, "")   runs=False
  quarantine libggml.so.0           health=(True, "")   runs=False
  quarantine libllama-common.so.0   health=(True, "")   runs=False
  quarantine libggml-base.so.0      health=(True, "")   runs=False
  quarantine libmtmd.so.0           health=(True, "")   runs=False

_payload_match_is_loadable can no longer answer from the name alone, because
libllama.so means one thing in a family that has a SONAME and another in a family
that never had one. It now reads the family: a versionless name counts only when
nothing versioned sits beside it. Any versioned sibling disqualifies it, not only
a SONAME-shaped one, since asking for a SONAME specifically would read the family
as versionless again exactly when the SONAME is the file that went missing.

Same rule on macOS, where dyld asks for the install name in LC_ID_DYLIB:
libggml.dylib does not stand in for libggml.0.dylib.

2. An unresolvable named user made runtime discovery throw.

Path.expanduser() raises RuntimeError when it cannot determine the home, which
includes every unknown named user, unlike os.path.expanduser, which hands the
value back:

  Path("~no-such-account-anywhere/llama.cpp").expanduser()
  RuntimeError: Could not determine home directory.

Every reader of UNSLOTH_LLAMA_CPP_PATH used the raising form, so a pin left in a
service unit or a .env after an account rename took discovery down with it rather
than being treated as unusable, and several callers have no exception boundary.
Four sites, not the one reported: the search, the process allowlist that has to
name the same tree, the settings reader and the status payload the UI renders.

expanded_user_path is the non-raising expansion, shared by all four so they cannot
diverge, and it leaves an unresolvable name exactly as it arrived, which is what
the desktop's pinning does with one too.

Tests:

- test_the_flattened_trio_installs_healthy and
  test_quarantining_a_soname_beside_a_versionless_copy_is_reported_broken, per
  library, built from the b10840 asset through copy_globs, asserting the three
  names really are flattened regular files first, so the test cannot pass for the
  dangling-link reason instead. Skipped where the asset is absent, as the rest of
  that file is.
- test_a_family_that_only_ever_ships_one_name_is_still_loadable and the macOS
  counterpart, both artifact-free, for the half a blunter rule would break:
  libggml-cpu-x64.so has no versioned copy anywhere and is the name the loader
  asks for.
- The expanduser guard, including the status payload and discovery reaching its
  own answer rather than raising.
- test_a_dangling_library_symlink_does_not_count_as_present now builds all three
  rungs, which is what its own docstring already described; it had been building
  the outer two, and the SONAME rung is what the new rule needs to see.

tests/studio/install: 5999 passed, 6 skipped. studio/backend/tests -k "llama_cpp
or path_settings": 1928 passed, 4 skipped. cargo test --bin unsloth-studio: 574
passed.

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

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

* Address review: the HIP module by name, both impl halves, and a marker that will not parse

Six items, each measured against a real bundle built through copy_globs rather
than reasoned about.

ggml-hip.dll by name, not "*hip*.dll". app-b10798-mix-659e406-windows-x64-rocm-gfx1150
ships amdhip64_7.dll, hipblas.dll and libhipblaslt.dll beside it, and all four
match that pattern, so quarantining the one module ggml actually loads left the
group satisfied by three libraries that cannot stand in for it. Measured:
installed_runtime_health answered (True, "") with the backend gone.

llama-quantize-impl.dll under the same split-build gate as the server's.
llama-quantize.exe links against its own impl library exactly as llama-server.exe
does, and every post-b9283 bundle ships both, so requiring one let a quarantined
quantize implementation read as healthy while quantization could not start.

A marker that exists but will not parse names no source, and the source-gated
groups were dropped with it: on Windows llama.dll stood in for the whole payload,
so an interrupted marker plus a quarantined ggml-base.dll answered healthy.
Assuming "published" instead would be worse, because a statically linked source
build ships none of those names and would fail health on a tree the setup scripts
keep, repairing on every launch with nothing to change. _tree_looks_prebuilt reads
the source off the tree from a name only a release ships, and stays lenient when
quarantine has taken the evidence too.

The runtime fingerprint now includes entry names. Its four counters are all
aggregates, and a rename in place moves none of them, so security software that
renames ggml-base.dll beside itself left the cached Ready matching a tree
installed_runtime_health rejects, and preflight answered Ready without running the
capability probe. FNV-1a rather than DefaultHasher, so a toolchain bump does not
invalidate every cached verdict; summed rather than folded, because read_dir order
is unspecified and the counters beside it are order-independent for that reason.

The CLI classifies UNSLOTH_LLAMA_CPP_PATH instead of trusting the marker.
studio/backend/main.py calls mark_managed_llama_cpp_path before discovery, which
marks any override equal to the managed tree however it got there, and the finder
then skips it; _ensure_studio_env_exported writes exactly that value under a
custom STUDIO_HOME and sets no marker. So preflight graded the managed tree as a
user pin while the backend walked past it to the stored selection, blocking launch
over a tree nothing would open. Compared against the studio home's own path with
the override out of the way, never default_managed_llama_dir(), which reads the
override first and would call every user pin managed.

expanded_user_path at the last Path.expanduser left in the doctor, a gap in the
earlier pass: a "~name" naming no account raises RuntimeError out of a command
whose whole job is to answer.

Also: the stored-selection stub in test_desktop_capability_llama_runtime was
missing expanded_user_path, so the helper's import raised and every test in that
file was silently grading the except fallback rather than the code it names.

tests/studio: 10171 passed, 198 skipped. cargo test --bin unsloth-studio: 575
passed. The four new payload tests and the CLI one all fail on the previous head.

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

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

* Gate the PowerShell reuse shortcut on runtime health, as the shell already is

The POSIX half of this loop was closed and the Windows half was not. setup.ps1
reaches "already built" through an elseif on Test-PathQuiet $LlamaServerBin, so a
Windows repair that fell through to the source stage kept a tree whose
ggml-base.dll had been quarantined, returned it byte for byte identical, and
reported success. Desktop preflight grades that same tree on every launch, so it
offered the same repair forever.

Test-LlamaTreeStillHealthy makes the same --check-existing-install call setup.sh
makes, through Invoke-LlamaHelper, so there is one definition of healthy rather
than two that can disagree. It answers healthy on any failure to ask: a helper
that cannot run must not turn into a multi-gigabyte source build on a machine
whose only fault was an unreadable tree.

The reason this survived is in the convergence model's own comment, which said
setup.ps1 had no reuse step and that a Windows update reaching the source stage
always builds. That was wrong, and the model therefore never exercised the
Windows branch. It now models both shells, and reverting the gate in the model
fails every Windows cell of
test_no_damaged_tree_survives_the_shell_rebuild_skip, which is what it should
have done all along.

A model cannot catch a shell losing its gate, so
test_both_shells_gate_their_reuse_shortcut_on_the_same_check reads both scripts
and asserts the check sits on the shortcut itself rather than somewhere else in
the file.

tests/studio/install: 6007 passed, 6 skipped.

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

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

* Reject a library renamed in place instead of letting it satisfy its own group

* Grade the root entrypoint, and plan the Windows rebuild the health gate forces

* Grade the studio root the desktop scrubbed, and fix the Windows fixture

* Reject empty payload files, and stop grading a runtime no repair can reach

* Give the entrypoint check one owner, and cache only what the probe read

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

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

* Close the repair loop between the launch check and the update

Found by simulating the cycle a user walks rather than by testing the new
check on its own, and reproduced before being fixed.

installed_runtime_health rejects a root llama-server or llama-quantize that
lost its execute bit, so the desktop marks the install stale and offers a
repair. existing_install_current_without_plan is the shortcut
install_prebuilt returns on before reinstalling anything, and it checked
os.access(X_OK) under build/bin ONLY, while _find_llama_server_binary
reaches the install root's copy FIRST. So the repair ran, changed nothing,
and the next launch was stale again.

Measured, with an undamaged install accepted as the control:

  root llama-server -x    launch health broken, shortcut True,  full False
  root llama-quantize -x  launch health broken, shortcut True,  full False
  build/bin copy -x       launch health broken, shortcut False, full False

_damaged_entrypoint now answers for the shortcut too, which is what its own
docstring already asks for: one owner, so a tree one check rejects and
another keeps cannot be repaired by changing nothing. It is four stat plus
os.access calls and spawns nothing, so the shortcut stays a shortcut.

test_the_execute_bit_is_checked_on_the_runtime_copies_only pinned the old
boundary, so it is rewritten as
test_the_execute_bit_is_checked_on_every_entrypoint, keeping the visual
server as the control that stops the fix becoming "reject everything".

tests/studio/install plus test_llama_cpp_path_settings.py: 8787 passed,
65 skipped.

* Require the split entrypoint dylibs on macOS

The impl split that gave Windows llama-server-impl.dll and Linux
libllama-server-impl.so gives macOS libllama-server-impl.dylib and
libllama-quantize-impl.dylib, and the macOS branch of
runtime_payload_health_groups was the one that did not ask for them. Quarantining
either left every library group satisfied and both execute-bit checks passing while
dyld could not start the entrypoint, so installed_runtime_health answered Ready on
the platform this probe exists for.

Verified against the real bundles rather than assumed: both
llama-b10840-mix-d5c17a0 and llama-b11007-mix-3e83366 macos-arm64 ship the two,
unversioned, beside the entrypoints. _PREBUILT_TREE_EVIDENCE already reads
libllama-server-impl*.dylib as proof of a prebuilt macOS tree.

_macos_split_entrypoint_groups delegates the gate to its Linux counterpart rather
than repeating it, so the source labels and the b9283 floor cannot drift.

The three fixtures that claimed to list a real macOS bundle were short of these two
files and now carry them, and the matrix states the requirement independently with
the matching pre-split pruning. New test with the loop-direction control: a pre-split
upstream b6099 bundle never shipped them and stays healthy.

* Shorten the macOS split docstring

Comment-only; the AST gate reports install_llama_prebuilt.py code-unchanged.

* Grade the macOS fixture against a real bundle, and stop a test carrying one machine's path

Merging main up brought in test_macos_loader_probe_record.py from #11237, whose
install fixture writes three dylibs. This branch replaced main's three loose
macOS globs with one group per library, so the fixture is now thinner than any
install the code grades and 18 of its 26 tests failed on the merge.

The tightening is the right side of that: on the shipped
llama-b11030-mix-5ff778e-bin-macos-arm64 bundle, quarantining libggml-base
leaves main answering healthy, because libggml-blas, libggml-metal and
libggml-rpc still satisfy libggml*.dylib, and answers
llama_runtime_payload_incomplete here. The fixture now writes the libraries that
bundle actually carries, including the two impl dylibs the unparseable release
tag requires under the same gate Windows has always used.

test_llama_runtime_install_history.py defaulted its asset directory to one
machine's workspace path. The default is now under the user's own cache, and the
presence check no longer lets a PermissionError through: the file promises to
skip when an artifact is not available, and an unreadable directory failed 24
tests instead.

tests/studio/install: 8796 passed, 76 skipped.

* Name the folder the antivirus advice is asking the user to allow

The message told the user to allow "the folder" without saying which one, and
finding it is the whole work of acting on that sentence. It now names the
default root, spelled the way the platform spells it, off the same navigator
test the roaming-profile hint beside it already uses.

Hedged with "usually" rather than asserted: UNSLOTH_LLAMA_CPP_PATH can move the
root and preflight does not carry the path, so a definite statement would be
wrong for anyone who set it. No navigator at all keeps the POSIX spelling rather
than dropping the advice.

Three tests: every reason names a folder, Windows gets the Windows spelling and
no POSIX one (a path a user cannot paste into an exclusion list is not advice),
and a missing navigator still answers. 15 of 15 in the file.

* Grade a root entrypoint only when discovery would start it

The root check was stricter than the resolver it was modelling. It rejected any
root copy that existed and was not runnable, but resolve_llama_server_binary
returns the first USABLE candidate, and _usable_binary is is_file() plus
os.access(X_OK) off Windows. A root wrapper whose execute bit is gone is walked
past, build/bin runs, and the tree is fine; grading it damaged sent a working
runtime through a repair, and where repair is unavailable it blocked the launch.

Reproduced against the real resolver on a Linux tree before changing anything:

  root wrapper executable            backend picks <root>/llama-server           health (True, '')
  root wrapper chmod 0644            backend picks <root>/build/bin/llama-server health (False, binaries_missing)
  root wrapper executable but empty  backend picks <root>/llama-server           health (False, binaries_missing)

The middle row is the false positive. The third is the one that matters and is
kept: an empty wrapper keeps is_file() and its execute bit, so discovery does
select it and the exec dies on ENOEXEC.

_discovery_would_select mirrors _usable_binary, so the root copy is graded only
when the resolver would stop there. Windows keeps the current behaviour, since
_usable_binary does not consult the execute bit there and an empty .exe is still
what gets selected.

test_a_rotten_root_entrypoint_is_not_saved_by_a_healthy_build_bin asserted the
old behaviour on the strength of "the finder reaches the root first", which is
true but incomplete: it reaches it and skips it. Replaced with the empty-wrapper
case, plus a test that a 0644 wrapper and a directory of that name are both
walked past, the Windows empty-exe case, and a parity test against the backend's
own _usable_binary so the two cannot drift.

tests/studio/install/test_installed_runtime_health.py: 53 passed.

* Scope the root-entrypoint relaxation to the launch verdict

The first cut changed _damaged_entrypoint for all three callers, which also
relaxed the installer's keep decision and the release-reuse check. Those ask a
different question: whether this tree can be reused as it stands, and a rotten
root wrapper is worth replacing, which is what a reinstall is for.

selected_root_only is the launch probe's rule alone. The launch verdict follows
the resolver, so it walks past a wrapper with no execute bit and does not mark a
working runtime stale; the keep decision stays as it was. The direction is the
safe one: the launch verdict is never stricter than the repair that answers it,
which is the property that stops a tree being repaired by changing nothing and
rejected again on the next launch.

test_a_damaged_root_entrypoint_stops_the_release_being_reused keeps its point,
that the keep decision must reject where the shortcut once kept, and gains the
empty-wrapper row, which is the damage the resolver does select and run.

tests/studio/install: 8970 passed, 76 skipped, 1 failed, and that one
(test_an_unserved_preferred_arch_source_builds_rather_than_using_the_igpus_bundle)
fails identically on a clean origin/main worktree.

* Grade the runtime a master root install actually uses

Under UNSLOTH_HOME the runtimes sit beside studio/, and the CLI exports
<master>/llama.cpp while UNSLOTH_STUDIO_HOME stays <master>/studio. The comparison
path dropped the override and called default_managed_llama_dir(), which reads only the
studio home, so it answered <master>/studio/llama.cpp: the installer's own export did
not match, the tree in use graded as a user pin, and master root installs never reached
the health check.

The rule now lives in _master_root_llama_dir() and both the export and the grading read
it, so there is one answer rather than two that can drift.

* Fingerprint the runtime a master root install actually loads

The Rust half of the same split: inferred_studio_llama_root() answered
<master>/studio/llama.cpp for an install recorded under UNSLOTH_HOME, while the CLI
grades and the backend loads <master>/llama.cpp. One healthy cached result then
survived quarantine in the runtime in use, so preflight kept returning Ready and the
failure only appeared at model load, with no repair offered.

It now reads share/.unsloth-master-root under the same containment rule the CLI applies
(the recorded root must exist and contain this Studio tree) and fingerprints the
sibling runtime. Tests cover the note, a note naming somebody else's root, an empty one
and one naming a directory that is not there.

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: danielhanchen <unslothshared@gmail.com>
Co-authored-by: oobabooga <112222186+oobabooga@users.noreply.github.com>
Co-authored-by: danielhanchen <unslothai@gmail.com>
Co-authored-by: danielhanchen <moonshotaisubstack@gmail.com>
shaojun pushed a commit to shaojun/unsloth that referenced this pull request Sep 29, 2026
…t model load (unslothai#10494)

* Repair a quarantined llama.cpp runtime at launch instead of failing at model load

Smart App Control and antivirus quarantine individual files out of the managed
llama.cpp tree while leaving the marker and the rest of the tree in place. The
desktop's preflight decided staleness on the managed Python alone, so it reported
Ready, launched, and the missing DLL only surfaced later at model load as an
error that looked like a bad GGUF or an out of memory. Repairing it meant running
`unsloth studio update` by hand, which nothing told the user to do.

install_llama_prebuilt gains installed_runtime_health(), which reuses the same
payload tables the keep-install path already uses rather than duplicating the
required file list. The CLI reports it in the desktop capability payload and the
Tauri preflight treats an explicit false as stale, so the existing repair path
picks it up.

Only an explicit false is stale. None covers both "no runtime installed yet" and
"this CLI predates the field", and neither is a broken runtime: treating them as
stale would send every existing install through repair on its first launch after
an upgrade. For the same reason desktop_manageability_version is not bumped.

The capability cache had to move too. Its fingerprint covered the managed venv
only, so a file quarantined out of the runtime left it identical and preflight
answered Ready from cache without ever asking the CLI. The runtime's file count
and total size now join the fingerprint, and the cache schema goes to 4.

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

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

* Close the gaps the simulations found, and prove the repair cannot loop

The launch probe has to be no stricter than the setup scripts' own
keep-or-reinstall decision. A tree the probe rejects but _existing_install_runs
keeps would be repaired, kept unchanged by the repair and rejected again on the
next launch, which is a loop with no way out. That property is now stated where
the code is and tested as a property over the whole matrix.

Three real gaps closed:

The payload groups are libraries, and on Linux and macOS they name no executable
at all, so a quarantined llama-server passed as a complete install. Both
entrypoints are now looked for, which is what _existing_install_runs already
requires, so the check stays on the safe side of the rule above.

platform_only_host() left macos_version unset while detect_host() derives it
from platform.mac_ver(), which reads no hardware. The parity test would have
failed on a macOS runner.

A relative UNSLOTH_LLAMA_CPP_PATH resolved against the desktop's working
directory, which is not the one the CLI child runs under. Fingerprinting
whatever sits beside the desktop is worse than not fingerprinting, so it now
degrades to no coverage. A leading ~ is expanded, since the Python reader calls
expanduser.

Deliberately not changed, and pinned by a test so it is not "fixed" later: a
marker that exists but does not parse still reads as nothing installed.
Reporting it broken looks right, but confirm_install_tree only checks that the
marker file exists, so the offline keep path keeps such a tree, and broken plus
kept is exactly the loop above.

Tests. 1361 cases over [Windows, Linux, macOS] x [cpu, cuda, rocm, vulkan,
metal] x thirteen shipped marker shapes x damage modes, asserting both the
verdict and the invariant, with the invariant's sensitivity checked by mutation
rather than assumed. Healthy installs of every shipped shape stay healthy on
every platform, which is the backwards-compatibility half. The verdict is
unchanged across NVIDIA, AMD, Intel and no GPU at all. Rust gains JSON round
trips proving a pre-PR payload still parses and reads Ready, that an old cache
file is ignored rather than fatal, and that a newer CLI's unknown keys are
inert. The frontend no longer tells a user with a quarantined runtime that their
install is too old.

* Format the new install-health files with the pinned ruff

scripts/run_ruff_format.py runs whatever ruff is on the path; the hook pins
0.6.9, and these four files were written under a newer one. The two disagree
about assert-message wrapping and about when a def signature goes one parameter
per line, which is what pre-commit.ci reported as "files were modified by this
hook".

* Fix the review round, and two defects the simulations found under it

Seven review items were genuine and are fixed here. Two of them contradicted
calls I had made earlier and the review was right both times.

A marker that exists but does not parse is now graded rather than reported as
nothing installed. I had argued the opposite, on the grounds that the offline
keep path keeps such a tree and broken plus kept is a loop. The narrower split
is the correct one: an absent marker file is a runtime nobody installed, a
present one that will not parse is a real tree, and when it has lost a library
as well, answering None left preflight Ready with no repair offered. The keep
path already reads such a marker as an unknown backend and grades the same
payload, so nothing here asks for more than it does.

A relative UNSLOTH_LLAMA_CPP_PATH was falling through to the legacy tree, which
is a regression I introduced in the commit that was meant to prevent exactly
that. Dropping it to None is not the fix either: None matches None, so the cache
keeps hitting while the real runtime rots. It is anchored to this process's
working directory now, which is where relative_override_pins joins the child's
copy from. On Windows the tilde resolves through USERPROFILE rather than
dirs::home_dir(), because that is what the child uses and a portable profile
moves the second one; process.rs carries the same note at its own read.

A quarantined llama-server was already covered. Managed runtime health is now
reported only when the managed runtime is the one that would actually load,
since _find_llama_server_binary prefers LLAMA_SERVER_PATH and the folder chosen
in settings ahead of it, and grading the managed tree regardless sent a user
running their own build into a repair that offline cannot even succeed.

Two further defects came out of the simulations rather than the review.

A dead Linux runtime was reported healthy. A release ships libllama.so.0 beside
libllama.so.0.0.10360 and the group pattern matches both, so quarantining the
SONAME, which is the name the loader asks for, left the group satisfied by the
twin. Measured against a real install: llama-server exits 127 with a loader
error while the probe said healthy. No fixture could show it, because a fixture
writes one file per library and a release writes two. Dangling symlinks in a
versioned chain were passing for the same reason and are covered by the same
check.

The repair could not terminate. setup.sh skipped the source rebuild whenever the
two entrypoints were executable, which is true of every tree that lost only a
library, so offline the repair returned the tree byte for byte identical and
reported success while the next launch rejected it again. It asks
reusable_existing_install first now. That branch only runs once the prebuilt
path has failed, where keeping a tree that cannot load is never right, and a
tree with no marker is a real source build and keeps the old test.

Tests: 5965 in tests/studio/install, 561 in the Tauri crate. The convergence
census that pinned the loop as a fact now pins its absence, and the strict xfail
holding the SONAME defect is an ordinary assertion.

* Give a runtime tree with no build/bin its own fingerprint

read_dir(build/bin).ok()? answered None for two different states: no llama.cpp
runtime at all, and a runtime tree whose binary directory is gone. The capability
verdict is not the same in both. With no marker, installed_runtime_health answers
None, llama_runtime_ok stays null, and desktop_capability_stale_reason
deliberately does not call that stale, so a Ready gets cached. Once a marker sits
over a missing build/bin the CLI would answer llama_runtime_dir_missing, but the
fingerprint had not moved, the cache still matched, and preflight served the old
Ready without ever asking.

The root's own entries now stand in when build/bin cannot be read, so the marker
arriving moves the key. None still means the tree is not there, which is the
NotInstalled case.

* Catch up the fingerprint tests with the bin and nobin prefixes

The previous commit gave a runtime tree with no build/bin its own fingerprint
and prefixed the walk, which left five assertions on the old shape and the crate
red at 557 of 562.

Three are the format: bin: now leads the count. Two are the semantics the change
introduced, and they are worth stating rather than patching. "No runtime at all"
is the absence of the tree, so its test points at a path that does not exist
instead of an empty directory, which is now the broken-install state. A root that
is present but whose binary directory cannot be read is broken too, not absent:
sharing None with a machine that never installed a runtime is exactly what let a
cache written before an install keep matching after it broke.

* Tighten the comments on the runtime health preflight

Shorten every comment and docstring this branch added, keeping the
reasoning that is load bearing and dropping the restatements of the code
beside them. No code, only comments and whitespace.

* Address review: per-library macOS payload groups, ~user paths, blank runtime override

- installed_runtime_health now checks each macOS library on its own instead of
  three broad globs, so a bundle missing libllama-common, libggml-base or
  libggml-cpu is graded incomplete rather than healthy. Verified against the real
  macos-arm64 bundle: complete is healthy, removing any one library is not, and
  dropping the metal, blas and rpc backends stays healthy.
- llama_runtime_override_from resolves ~user/... through getpwnam on unix rather
  than treating it as a relative path.
- A blank UNSLOTH_LLAMA_CPP_PATH no longer counts as an override.
- The CLI capability payload only reports the managed runtime when the managed
  runtime is the one that would actually run.

Fixtures that built macOS trees from the old broad names now write the names a
real bundle ships.

* Address review: macOS SONAME links, the execute bit, and which runtime is graded

- A dylib chain now follows the same rule as an .so: losing the middle
  libggml.0.dylib left the group satisfied by the terminal libggml.0.23.0.dylib
  while dyld could not launch, since the LC_LOAD_DYLIB entry names the link.
- llama-server and llama-quantize must still carry the execute bit on POSIX.
  _existing_install_runs already asks that way, so an install with the bit
  cleared was rejected there and reported healthy here.
- The CLI capability helper follows the finder's own file status rather than the
  directory entry, so a pinned symlink whose target was quarantined falls
  through to the managed tree the backend really loads instead of suppressing
  its verdict.
- UNSLOTH_LLAMA_CPP_PATH outranks the stored custom folder in the finder and
  default_managed_llama_dir points at exactly that tree, so it is graded even
  when an older selection is still in the settings database.
- ~name on Windows resolves to the sibling profile the same way process.rs and
  ntpath.expanduser resolve it, instead of being fingerprinted under the cwd.
- A runtime verdict the CLI skipped is no longer cached, since nothing in the
  fingerprint watches which runtime is selected and clearing a custom selection
  would otherwise keep serving a Ready nobody had graded.

* Ask for the execute bit by the tree's platform, not the interpreter's

A Windows bundle unpacked on a POSIX filesystem carries no execute bit, and the
host is a parameter here, so os.access X_OK graded every real windows-x64 bundle
in the history suite as missing its binaries. Windows has no execute bit for
os.access to read anyway, so presence is the whole question there.

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

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

* Address review: the fingerprint's blind spots, and which tree it watches

- The runtime fingerprint counted regular files only, so deleting an install-name
  link (libggml.0.dylib, or the SONAME link on Linux) or taking the execute bit
  off llama-server left it identical. Both are states installed_runtime_health
  rejects, so a cached Ready went on matching a tree that could no longer launch.
  Links and executable files are counted now.
- An installer-managed venv under a custom root names its own runtime. The
  desktop strips UNSLOTH_STUDIO_HOME before spawning the CLI, but the CLI
  re-infers the root from sys.prefix and points UNSLOTH_LLAMA_CPP_PATH at its
  llama.cpp, so reading only the ambient environment fingerprinted the legacy
  tree and quarantine in the real one never invalidated anything.
- An override holding no server does not end the search, so it is no longer
  treated as final on either side. The CLI names the tree it grades rather than
  answering yes or no, so the two cannot pick different trees.
- ~name resolves through getpwnam_r. getpwnam hands back process-global storage
  and this runs on a Tauri worker thread, where a concurrent lookup could
  overwrite pw_dir mid-copy.

* Address review: entrypoints that are files, the mode bits, and one override spelling

installed_runtime_health asked os.access(X_OK), which a directory named llama-server
answers true for because directories are searchable, while the finder asks is_file()
and rejects the tree. Both now go through one helper that _existing_install_runs
shares, so the keep-or-reinstall decision and the launch verdict cannot disagree.

The runtime fingerprint counted files with any execute bit, but os.access(X_OK) asks
whether this user may run one: 0755 to 0655 leaves group and other executable and the
owner unable to run it, so the count never moved for a state health rejects. The
permission bits themselves are summed instead.

_find_llama_server_binary read UNSLOTH_LLAMA_CPP_PATH literally while
default_managed_llama_dir, get_stored_custom_llama_cpp_path and the desktop's pinning
all expand it, so a ~/llama.cpp written into a service unit or a .env sent discovery to
a folder named ~ beside the working directory and every other component reported on a
different tree. It expands now, and so does the orphan reaper's allowlist, which has to
name the tree discovery spawns from.

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

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

* Address review: the split entrypoint libraries, and reuse asking what the probe asks

Two of the three items from the last round, both measured against a b10360
managed install rather than a fixture.

1. The Linux impl split had no payload group.

ggml-org/llama.cpp#23462 moved each binary's entry code into a lib<binary>-impl
library. Windows already required llama-server-impl.dll, gated on the build the
split landed in. Linux got the same treatment upstream and none of it here: the
groups name the shared libraries only, so libllama-server-impl.so and
libllama-quantize-impl.so were required by nothing.

Measured on a copy of the managed install, removing either one:

  installed_runtime_health -> (True, "")
  _existing_install_runs   -> False

which is the disagreement the whole probe exists to remove, in the direction
that hurts: preflight says Ready and llama-server dies in the loader.

_linux_split_entrypoint_groups adds both, gated the same way the Windows side is
and on the same build number, since it is one upstream commit rather than one
platform's packaging. A source build ships neither whatever the tag says, and a
pre-split archive is healthy without them, so requiring one of those would
reinstall on every check forever.

2. The exact-release reuse decision asked exists() where the probe asks for the
execute bit.

installed_runtime_health asks _entrypoint_is_runnable, because that is what
_find_llama_server_binary asks. existing_install_matches_choice asked exists(),
and its Linux ldd gate reads a non-executable ELF quite happily. Clearing the
execute bit on llama-server, again on a real copy:

  installed_runtime_health -> (False, "llama_runtime_binaries_missing")
  _existing_install_runs   -> False
  exists()                 -> True
  preflight_linux_installed_binaries -> passes

so preflight says broken, repair says the exact release is already installed,
nothing is downloaded, and the next launch says broken again. Both attempts land
in the same place. It now calls _entrypoint_is_runnable, which is the same test
both other answers use.

Tests:

- test_quarantining_a_split_entrypoint_library_is_reported_broken, per library,
  against a managed copy, asserting the two answers agree afterwards.
- test_an_older_monolithic_linux_release_is_not_asked_for_the_impl_libraries,
  which pins the gate rather than the requirement: b9279 owes neither, b9283
  owes both, a source build owes neither.
- test_a_stripped_execute_bit_is_not_reused_as_an_exact_release_match, checking
  the two gates directly.
- The fixture tables gain both libraries, and the shape writers now chmod the
  entrypoints, which a real extraction does and the reuse decision now reads.

tests/studio/install: 5991 passed, 6 skipped. cargo test --bin unsloth-studio:
571 passed.

The third item, a versionless libllama.so surviving its SONAME's removal, does
not apply: the release ships exactly two names per library, libllama.so.0 beside
libllama.so.0.0.10360, with no versionless file and no symlinks. Replied on the
thread.

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

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

* Address review: resolve a named-user override the same way on both sides of a spawn

preflight::managed resolves UNSLOTH_LLAMA_CPP_PATH=~alice/llama.cpp through
getpwnam_r, the way posixpath.expanduser does. process.rs, pinning the same
variable for the child it is about to move, read it with the Windows rule, which
leaves ~alice alone off Windows, so it fell through to being anchored under the
directory being left.

Measured, with the two halves side by side on this host:

  preflight::managed  ~root/llama.cpp -> /root/llama.cpp
  process.rs pin      ~root/llama.cpp -> /mnt/work/session/~root/llama.cpp

The child is handed the pinned value, so it grades and runs one tree while the
cache fingerprints another. Quarantining a file from the tree actually in use
then never invalidates a Ready verdict, which is the whole failure this
fingerprint exists to catch.

expand_user now makes the split every other reader in that function already
makes, Windows rules on Windows and the native ones off it, and expand_posix_user
resolves the named form through the same lookup preflight::managed uses, which is
now pub(crate) rather than duplicated. An unknown name is left exactly as it
arrived, which is what expanduser does, so both halves fall through to anchoring
it and still agree. Bare ~ and ~/rest are unchanged: they went through
expand_windows_user's empty-name arm, which answers home on any platform, and
they go through the posix one now.

Windows was already aligned. named_user_home's Windows arm delegates to
named_windows_user_home, which is the same rule expand_windows_user applies.

Tests, all three unix-only since they turn on the password database:

- a_named_user_override_is_pinned_where_the_fingerprint_watches, asserting the
  pin is not under the directory being left and equals what the fingerprint side
  reaches through the same lookup.
- an_unknown_named_user_is_left_alone_and_anchored_by_both_halves.
- a_bare_tilde_override_still_reaches_home_off_windows, for the form that already
  worked.

cargo test --bin unsloth-studio: 574 passed. tests/studio/install: 5991 passed,
6 skipped.

* Address review: the SONAME beside a flattened versionless copy, and an override that cannot expand

Two items, and the first one corrects a rebuttal of mine that was wrong.

1. A versionless copy kept the group satisfied after the SONAME was quarantined.

I argued against this last round on the grounds that the bundle ships exactly two
names per library. That was true of the tree I measured, the b10360 install on
this machine, and it is not true of what the installer produces today. Two things
moved underneath it:

  - b10840 ships the trio. libllama.so -> libllama.so.0 -> libllama.so.0.4.0, as
    symlinks in the tarball.
  - copy_globs flattens them. It selects with is_file() and copies with
    shutil.copy2, which follows symlinks, so all three arrive in build/bin as
    independent regular files. My reproduction moved the extracted tree instead,
    which kept the links, and a dangling link is dropped by is_file() on its own.
    That made the argument look decisive about a shape no install has.

Extracting b10840 with prebuilt_core.extract_archive and installing it through
copy_globs, which is the path install_prebuilt takes:

  baseline                          health=(True, "")
  quarantine libllama.so.0          health=(True, "")   runs=False
  quarantine libggml.so.0           health=(True, "")   runs=False
  quarantine libllama-common.so.0   health=(True, "")   runs=False
  quarantine libggml-base.so.0      health=(True, "")   runs=False
  quarantine libmtmd.so.0           health=(True, "")   runs=False

_payload_match_is_loadable can no longer answer from the name alone, because
libllama.so means one thing in a family that has a SONAME and another in a family
that never had one. It now reads the family: a versionless name counts only when
nothing versioned sits beside it. Any versioned sibling disqualifies it, not only
a SONAME-shaped one, since asking for a SONAME specifically would read the family
as versionless again exactly when the SONAME is the file that went missing.

Same rule on macOS, where dyld asks for the install name in LC_ID_DYLIB:
libggml.dylib does not stand in for libggml.0.dylib.

2. An unresolvable named user made runtime discovery throw.

Path.expanduser() raises RuntimeError when it cannot determine the home, which
includes every unknown named user, unlike os.path.expanduser, which hands the
value back:

  Path("~no-such-account-anywhere/llama.cpp").expanduser()
  RuntimeError: Could not determine home directory.

Every reader of UNSLOTH_LLAMA_CPP_PATH used the raising form, so a pin left in a
service unit or a .env after an account rename took discovery down with it rather
than being treated as unusable, and several callers have no exception boundary.
Four sites, not the one reported: the search, the process allowlist that has to
name the same tree, the settings reader and the status payload the UI renders.

expanded_user_path is the non-raising expansion, shared by all four so they cannot
diverge, and it leaves an unresolvable name exactly as it arrived, which is what
the desktop's pinning does with one too.

Tests:

- test_the_flattened_trio_installs_healthy and
  test_quarantining_a_soname_beside_a_versionless_copy_is_reported_broken, per
  library, built from the b10840 asset through copy_globs, asserting the three
  names really are flattened regular files first, so the test cannot pass for the
  dangling-link reason instead. Skipped where the asset is absent, as the rest of
  that file is.
- test_a_family_that_only_ever_ships_one_name_is_still_loadable and the macOS
  counterpart, both artifact-free, for the half a blunter rule would break:
  libggml-cpu-x64.so has no versioned copy anywhere and is the name the loader
  asks for.
- The expanduser guard, including the status payload and discovery reaching its
  own answer rather than raising.
- test_a_dangling_library_symlink_does_not_count_as_present now builds all three
  rungs, which is what its own docstring already described; it had been building
  the outer two, and the SONAME rung is what the new rule needs to see.

tests/studio/install: 5999 passed, 6 skipped. studio/backend/tests -k "llama_cpp
or path_settings": 1928 passed, 4 skipped. cargo test --bin unsloth-studio: 574
passed.

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

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

* Address review: the HIP module by name, both impl halves, and a marker that will not parse

Six items, each measured against a real bundle built through copy_globs rather
than reasoned about.

ggml-hip.dll by name, not "*hip*.dll". app-b10798-mix-659e406-windows-x64-rocm-gfx1150
ships amdhip64_7.dll, hipblas.dll and libhipblaslt.dll beside it, and all four
match that pattern, so quarantining the one module ggml actually loads left the
group satisfied by three libraries that cannot stand in for it. Measured:
installed_runtime_health answered (True, "") with the backend gone.

llama-quantize-impl.dll under the same split-build gate as the server's.
llama-quantize.exe links against its own impl library exactly as llama-server.exe
does, and every post-b9283 bundle ships both, so requiring one let a quarantined
quantize implementation read as healthy while quantization could not start.

A marker that exists but will not parse names no source, and the source-gated
groups were dropped with it: on Windows llama.dll stood in for the whole payload,
so an interrupted marker plus a quarantined ggml-base.dll answered healthy.
Assuming "published" instead would be worse, because a statically linked source
build ships none of those names and would fail health on a tree the setup scripts
keep, repairing on every launch with nothing to change. _tree_looks_prebuilt reads
the source off the tree from a name only a release ships, and stays lenient when
quarantine has taken the evidence too.

The runtime fingerprint now includes entry names. Its four counters are all
aggregates, and a rename in place moves none of them, so security software that
renames ggml-base.dll beside itself left the cached Ready matching a tree
installed_runtime_health rejects, and preflight answered Ready without running the
capability probe. FNV-1a rather than DefaultHasher, so a toolchain bump does not
invalidate every cached verdict; summed rather than folded, because read_dir order
is unspecified and the counters beside it are order-independent for that reason.

The CLI classifies UNSLOTH_LLAMA_CPP_PATH instead of trusting the marker.
studio/backend/main.py calls mark_managed_llama_cpp_path before discovery, which
marks any override equal to the managed tree however it got there, and the finder
then skips it; _ensure_studio_env_exported writes exactly that value under a
custom STUDIO_HOME and sets no marker. So preflight graded the managed tree as a
user pin while the backend walked past it to the stored selection, blocking launch
over a tree nothing would open. Compared against the studio home's own path with
the override out of the way, never default_managed_llama_dir(), which reads the
override first and would call every user pin managed.

expanded_user_path at the last Path.expanduser left in the doctor, a gap in the
earlier pass: a "~name" naming no account raises RuntimeError out of a command
whose whole job is to answer.

Also: the stored-selection stub in test_desktop_capability_llama_runtime was
missing expanded_user_path, so the helper's import raised and every test in that
file was silently grading the except fallback rather than the code it names.

tests/studio: 10171 passed, 198 skipped. cargo test --bin unsloth-studio: 575
passed. The four new payload tests and the CLI one all fail on the previous head.

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

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

* Gate the PowerShell reuse shortcut on runtime health, as the shell already is

The POSIX half of this loop was closed and the Windows half was not. setup.ps1
reaches "already built" through an elseif on Test-PathQuiet $LlamaServerBin, so a
Windows repair that fell through to the source stage kept a tree whose
ggml-base.dll had been quarantined, returned it byte for byte identical, and
reported success. Desktop preflight grades that same tree on every launch, so it
offered the same repair forever.

Test-LlamaTreeStillHealthy makes the same --check-existing-install call setup.sh
makes, through Invoke-LlamaHelper, so there is one definition of healthy rather
than two that can disagree. It answers healthy on any failure to ask: a helper
that cannot run must not turn into a multi-gigabyte source build on a machine
whose only fault was an unreadable tree.

The reason this survived is in the convergence model's own comment, which said
setup.ps1 had no reuse step and that a Windows update reaching the source stage
always builds. That was wrong, and the model therefore never exercised the
Windows branch. It now models both shells, and reverting the gate in the model
fails every Windows cell of
test_no_damaged_tree_survives_the_shell_rebuild_skip, which is what it should
have done all along.

A model cannot catch a shell losing its gate, so
test_both_shells_gate_their_reuse_shortcut_on_the_same_check reads both scripts
and asserts the check sits on the shortcut itself rather than somewhere else in
the file.

tests/studio/install: 6007 passed, 6 skipped.

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

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

* Reject a library renamed in place instead of letting it satisfy its own group

* Grade the root entrypoint, and plan the Windows rebuild the health gate forces

* Grade the studio root the desktop scrubbed, and fix the Windows fixture

* Reject empty payload files, and stop grading a runtime no repair can reach

* Give the entrypoint check one owner, and cache only what the probe read

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

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

* Close the repair loop between the launch check and the update

Found by simulating the cycle a user walks rather than by testing the new
check on its own, and reproduced before being fixed.

installed_runtime_health rejects a root llama-server or llama-quantize that
lost its execute bit, so the desktop marks the install stale and offers a
repair. existing_install_current_without_plan is the shortcut
install_prebuilt returns on before reinstalling anything, and it checked
os.access(X_OK) under build/bin ONLY, while _find_llama_server_binary
reaches the install root's copy FIRST. So the repair ran, changed nothing,
and the next launch was stale again.

Measured, with an undamaged install accepted as the control:

  root llama-server -x    launch health broken, shortcut True,  full False
  root llama-quantize -x  launch health broken, shortcut True,  full False
  build/bin copy -x       launch health broken, shortcut False, full False

_damaged_entrypoint now answers for the shortcut too, which is what its own
docstring already asks for: one owner, so a tree one check rejects and
another keeps cannot be repaired by changing nothing. It is four stat plus
os.access calls and spawns nothing, so the shortcut stays a shortcut.

test_the_execute_bit_is_checked_on_the_runtime_copies_only pinned the old
boundary, so it is rewritten as
test_the_execute_bit_is_checked_on_every_entrypoint, keeping the visual
server as the control that stops the fix becoming "reject everything".

tests/studio/install plus test_llama_cpp_path_settings.py: 8787 passed,
65 skipped.

* Require the split entrypoint dylibs on macOS

The impl split that gave Windows llama-server-impl.dll and Linux
libllama-server-impl.so gives macOS libllama-server-impl.dylib and
libllama-quantize-impl.dylib, and the macOS branch of
runtime_payload_health_groups was the one that did not ask for them. Quarantining
either left every library group satisfied and both execute-bit checks passing while
dyld could not start the entrypoint, so installed_runtime_health answered Ready on
the platform this probe exists for.

Verified against the real bundles rather than assumed: both
llama-b10840-mix-d5c17a0 and llama-b11007-mix-3e83366 macos-arm64 ship the two,
unversioned, beside the entrypoints. _PREBUILT_TREE_EVIDENCE already reads
libllama-server-impl*.dylib as proof of a prebuilt macOS tree.

_macos_split_entrypoint_groups delegates the gate to its Linux counterpart rather
than repeating it, so the source labels and the b9283 floor cannot drift.

The three fixtures that claimed to list a real macOS bundle were short of these two
files and now carry them, and the matrix states the requirement independently with
the matching pre-split pruning. New test with the loop-direction control: a pre-split
upstream b6099 bundle never shipped them and stays healthy.

* Shorten the macOS split docstring

Comment-only; the AST gate reports install_llama_prebuilt.py code-unchanged.

* Grade the macOS fixture against a real bundle, and stop a test carrying one machine's path

Merging main up brought in test_macos_loader_probe_record.py from unslothai#11237, whose
install fixture writes three dylibs. This branch replaced main's three loose
macOS globs with one group per library, so the fixture is now thinner than any
install the code grades and 18 of its 26 tests failed on the merge.

The tightening is the right side of that: on the shipped
llama-b11030-mix-5ff778e-bin-macos-arm64 bundle, quarantining libggml-base
leaves main answering healthy, because libggml-blas, libggml-metal and
libggml-rpc still satisfy libggml*.dylib, and answers
llama_runtime_payload_incomplete here. The fixture now writes the libraries that
bundle actually carries, including the two impl dylibs the unparseable release
tag requires under the same gate Windows has always used.

test_llama_runtime_install_history.py defaulted its asset directory to one
machine's workspace path. The default is now under the user's own cache, and the
presence check no longer lets a PermissionError through: the file promises to
skip when an artifact is not available, and an unreadable directory failed 24
tests instead.

tests/studio/install: 8796 passed, 76 skipped.

* Name the folder the antivirus advice is asking the user to allow

The message told the user to allow "the folder" without saying which one, and
finding it is the whole work of acting on that sentence. It now names the
default root, spelled the way the platform spells it, off the same navigator
test the roaming-profile hint beside it already uses.

Hedged with "usually" rather than asserted: UNSLOTH_LLAMA_CPP_PATH can move the
root and preflight does not carry the path, so a definite statement would be
wrong for anyone who set it. No navigator at all keeps the POSIX spelling rather
than dropping the advice.

Three tests: every reason names a folder, Windows gets the Windows spelling and
no POSIX one (a path a user cannot paste into an exclusion list is not advice),
and a missing navigator still answers. 15 of 15 in the file.

* Grade a root entrypoint only when discovery would start it

The root check was stricter than the resolver it was modelling. It rejected any
root copy that existed and was not runnable, but resolve_llama_server_binary
returns the first USABLE candidate, and _usable_binary is is_file() plus
os.access(X_OK) off Windows. A root wrapper whose execute bit is gone is walked
past, build/bin runs, and the tree is fine; grading it damaged sent a working
runtime through a repair, and where repair is unavailable it blocked the launch.

Reproduced against the real resolver on a Linux tree before changing anything:

  root wrapper executable            backend picks <root>/llama-server           health (True, '')
  root wrapper chmod 0644            backend picks <root>/build/bin/llama-server health (False, binaries_missing)
  root wrapper executable but empty  backend picks <root>/llama-server           health (False, binaries_missing)

The middle row is the false positive. The third is the one that matters and is
kept: an empty wrapper keeps is_file() and its execute bit, so discovery does
select it and the exec dies on ENOEXEC.

_discovery_would_select mirrors _usable_binary, so the root copy is graded only
when the resolver would stop there. Windows keeps the current behaviour, since
_usable_binary does not consult the execute bit there and an empty .exe is still
what gets selected.

test_a_rotten_root_entrypoint_is_not_saved_by_a_healthy_build_bin asserted the
old behaviour on the strength of "the finder reaches the root first", which is
true but incomplete: it reaches it and skips it. Replaced with the empty-wrapper
case, plus a test that a 0644 wrapper and a directory of that name are both
walked past, the Windows empty-exe case, and a parity test against the backend's
own _usable_binary so the two cannot drift.

tests/studio/install/test_installed_runtime_health.py: 53 passed.

* Scope the root-entrypoint relaxation to the launch verdict

The first cut changed _damaged_entrypoint for all three callers, which also
relaxed the installer's keep decision and the release-reuse check. Those ask a
different question: whether this tree can be reused as it stands, and a rotten
root wrapper is worth replacing, which is what a reinstall is for.

selected_root_only is the launch probe's rule alone. The launch verdict follows
the resolver, so it walks past a wrapper with no execute bit and does not mark a
working runtime stale; the keep decision stays as it was. The direction is the
safe one: the launch verdict is never stricter than the repair that answers it,
which is the property that stops a tree being repaired by changing nothing and
rejected again on the next launch.

test_a_damaged_root_entrypoint_stops_the_release_being_reused keeps its point,
that the keep decision must reject where the shortcut once kept, and gains the
empty-wrapper row, which is the damage the resolver does select and run.

tests/studio/install: 8970 passed, 76 skipped, 1 failed, and that one
(test_an_unserved_preferred_arch_source_builds_rather_than_using_the_igpus_bundle)
fails identically on a clean origin/main worktree.

* Grade the runtime a master root install actually uses

Under UNSLOTH_HOME the runtimes sit beside studio/, and the CLI exports
<master>/llama.cpp while UNSLOTH_STUDIO_HOME stays <master>/studio. The comparison
path dropped the override and called default_managed_llama_dir(), which reads only the
studio home, so it answered <master>/studio/llama.cpp: the installer's own export did
not match, the tree in use graded as a user pin, and master root installs never reached
the health check.

The rule now lives in _master_root_llama_dir() and both the export and the grading read
it, so there is one answer rather than two that can drift.

* Fingerprint the runtime a master root install actually loads

The Rust half of the same split: inferred_studio_llama_root() answered
<master>/studio/llama.cpp for an install recorded under UNSLOTH_HOME, while the CLI
grades and the backend loads <master>/llama.cpp. One healthy cached result then
survived quarantine in the runtime in use, so preflight kept returning Ready and the
failure only appeared at model load, with no repair offered.

It now reads share/.unsloth-master-root under the same containment rule the CLI applies
(the recorded root must exist and contain this Studio tree) and fingerprints the
sibling runtime. Tests cover the note, a note naming somebody else's root, an empty one
and one naming a directory that is not there.

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: danielhanchen <unslothshared@gmail.com>
Co-authored-by: oobabooga <112222186+oobabooga@users.noreply.github.com>
Co-authored-by: danielhanchen <unslothai@gmail.com>
Co-authored-by: danielhanchen <moonshotaisubstack@gmail.com>
frostyautumnleaf pushed a commit to frostyautumnleaf/llama.cpp that referenced this pull request Oct 5, 2026
…default (ggml-org#23462)

* cmake : remove STATIC from impl libraries, allow BUILD_SHARED_LIBS control

Remove explicit STATIC from all -impl libraries (server, cli, completion, bench,
batched-bench, fit-params, quantize, perplexity) so BUILD_SHARED_LIBS controls
shared vs static linkage.

Add WINDOWS_EXPORT_ALL_SYMBOLS ON for proper DLL export on Windows.

Assisted-by: llama.cpp:local pi

* cmake : enable LLAMA_BUILD_APP by default

Assisted-by: llama.cpp:local pi

* ci : disable app in build-cmake-pkg.yml
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build Compilation issues devops improvements to build systems and github actions examples server

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants