unsloth/studio/backend/utils/llama_cpp_path_settings.py
Daniel Han acf2de64c3
Repair a quarantined llama.cpp runtime at launch instead of failing at 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>
2026-09-22 00:08:50 -07:00

360 lines
15 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""Persist and validate the llama.cpp directory selected in Unsloth settings."""
from __future__ import annotations
import os
import re
import sys
import threading
from contextlib import contextmanager
from pathlib import Path
from typing import Any, Callable, Iterable, Iterator, Optional
CUSTOM_LLAMA_CPP_PATH_SETTING_KEY = "custom_llama_cpp_path"
MAX_CUSTOM_LLAMA_CPP_PATH_LENGTH = 32767
MANAGED_LLAMA_CPP_PATH_MARKER = "UNSLOTH_STUDIO_MANAGED_LLAMA_CPP_PATH"
_settings_lock = threading.RLock()
_path_revision = 0
def expanded_user_path(value: Path | str) -> Path:
"""``~`` and ``~name`` expanded, never raising on a name that does not resolve.
``Path.expanduser()`` raises ``RuntimeError`` when the home cannot be
determined, which includes every unknown named user: ``~deleted-user/llama.cpp``
left in a service unit or a ``.env`` after an account rename took runtime
discovery down with it, so a model operation reported the expansion error
rather than falling through to another runtime or saying none was found.
``os.path.expanduser`` is the reader that does not raise: it hands an
unresolvable name straight back, which is also what the desktop's pinning does
with one, so the two halves of a launch still agree about the value. An
unexpandable override then reaches the search as an ordinary path, finds
nothing, and the documented order continues.
"""
return Path(os.path.expanduser(str(value)))
def mark_managed_llama_cpp_path(directory: Path | str) -> bool:
"""Mark Unsloth's inherited install path without hiding a real env override."""
configured = os.environ.get("UNSLOTH_LLAMA_CPP_PATH", "").strip()
if not configured:
os.environ.pop(MANAGED_LLAMA_CPP_PATH_MARKER, None)
return False
try:
managed = expanded_user_path(directory).resolve(strict = False)
inherited = expanded_user_path(configured).resolve(strict = False)
is_managed = inherited == managed
except (OSError, RuntimeError, ValueError):
is_managed = False
if is_managed:
os.environ[MANAGED_LLAMA_CPP_PATH_MARKER] = "1"
else:
os.environ.pop(MANAGED_LLAMA_CPP_PATH_MARKER, None)
return is_managed
@contextmanager
def llama_cpp_path_selection_guard() -> Iterator[None]:
"""Serialize a runtime path snapshot with a settings write.
Model loads and UI saves share this lock so reload status sees one snapshot.
"""
with _settings_lock:
yield
def llama_server_binary_name(platform: Optional[str] = None) -> str:
return "llama-server.exe" if (platform or sys.platform) == "win32" else "llama-server"
def llama_server_candidates(
directory: Path | str, *, platform: Optional[str] = None
) -> tuple[Path, ...]:
"""Supported llama.cpp build layouts, in the runtime's search order."""
root = Path(directory)
binary_name = llama_server_binary_name(platform)
windows = (platform or sys.platform) == "win32"
candidates = [root / binary_name]
# build/ first, then the per-backend build dirs a source checkout keeps side by side (#5941).
for build in ("build", "build-cuda", "build-hip", "build-rocm", "build-vulkan"):
candidates.append(root / build / "bin" / binary_name)
if windows:
candidates.append(root / build / "bin" / "Release" / binary_name)
return tuple(candidates)
def _usable_binary(path: Path, *, platform: Optional[str] = None) -> bool:
try:
if not path.is_file():
return False
except OSError:
return False
return (platform or sys.platform) == "win32" or os.access(path, os.X_OK)
_GPU_BACKEND_LIB_RE = re.compile(
r"^(?:lib)?ggml-(cann|cuda|hip|metal|musa|opencl|sycl|virtgpu|vulkan)"
r"(?:\.dll|\.so(?:\.\d+)*|(?:\.\d+)*\.dylib)$"
)
_CPU_BACKEND_LIB_RE = re.compile(r"^(?:lib)?ggml-(?:cpu|base)(?:[-.]|$)")
# Backends bound to one vendor; vulkan, opencl and virtgpu run on any GPU.
_BACKEND_VENDOR = {
"cuda": "nvidia",
"hip": "amd",
"sycl": "intel",
"metal": "apple",
"musa": "mthreads",
"cann": "huawei",
}
_DRM_VENDOR = {"0x10de": "nvidia", "0x1002": "amd", "0x8086": "intel"}
_WINDOWS_VENDOR_DLLS = {
"nvidia": ("nvcuda.dll",),
"amd": ("amdhip64*.dll", "amdocl64.dll"),
"intel": ("ze_intel_gpu64.dll", "igdrcl64.dll"),
}
_DRM_ROOT = "/sys/class/drm"
# WSL2 reaches the GPU through /dev/dxg (no DRM card, no /dev/kfd or /dev/nvidiactl): the
# vendor is the runtime that drives it, librocdxg for AMD and the WSL libcuda for NVIDIA.
_WSL_ROCM_LIB_DIRS = ("/opt/rocm/lib", "/opt/rocm/lib64")
_WSL_CUDA_LIB_DIR = "/usr/lib/wsl/lib"
_HOST: Any = object() # prefer_gpu_capable's default: read the vendors from this host
def binary_gpu_backends(binary: Path | str) -> Optional[set[str]]:
"""GPU backend libraries beside the binary; empty for a split build with only CPU
libraries, None when the layout says nothing (a static build or a wrapper)."""
try:
names = [p.name for p in Path(binary).resolve().parent.iterdir() if p.is_file()]
except OSError:
return None
gpu = {m.group(1) for m in map(_GPU_BACKEND_LIB_RE.match, names) if m}
if gpu or any(_CPU_BACKEND_LIB_RE.match(name) for name in names):
return gpu
return None
def binary_gpu_verdict(binary: Path | str) -> str:
"""``gpu``, ``cpu`` (a split-library build with no GPU backend beside it) or ``unknown``."""
backends = binary_gpu_backends(binary)
return "unknown" if backends is None else ("gpu" if backends else "cpu")
def _mask_hides_all(*names: str) -> bool:
"""Whether the first set mask among names (their precedence order) hides every device."""
mask = next((os.environ[n] for n in names if n in os.environ), "x")
return mask.strip() in ("", "-1")
def _amd_mask_hides_all() -> bool:
"""HIP_ and ROCR_VISIBLE_DEVICES stack (ROCR filters the agents HIP then indexes), so an
empty one at either layer hides everything; CUDA_VISIBLE_DEVICES stands in only when
neither is set (_active_gpu_visibility_mask)."""
if any(n in os.environ for n in ("HIP_VISIBLE_DEVICES", "ROCR_VISIBLE_DEVICES")):
return _mask_hides_all("HIP_VISIBLE_DEVICES") or _mask_hides_all("ROCR_VISIBLE_DEVICES")
return _mask_hides_all("CUDA_VISIBLE_DEVICES")
def host_gpu_vendors() -> Optional[set[str]]:
"""GPU vendors this process can reach: DRM sysfs vendors whose device node is present and
whose visibility mask is not empty on Linux, the vendors' driver DLLs on Windows. None when
nothing was detected at all (an unknown host filters nothing); an empty set when GPUs were
detected but none is reachable, so only vendor-agnostic backends fit."""
vendors: set[str] = set()
if sys.platform == "darwin":
return {"apple"}
if sys.platform == "win32":
system32 = Path(os.environ.get("SystemRoot", r"C:\Windows")) / "System32"
for vendor, patterns in _WINDOWS_VENDOR_DLLS.items():
if any(next(system32.glob(pattern), None) for pattern in patterns):
vendors.add(vendor)
else:
for vendor_file in Path(_DRM_ROOT).glob("card*/device/vendor"):
try:
vendor = _DRM_VENDOR.get(vendor_file.read_text(encoding = "utf-8").strip().lower())
except OSError:
continue
if vendor:
vendors.add(vendor)
if os.path.isdir("/proc/driver/nvidia"):
vendors.add("nvidia")
wsl = os.path.exists("/dev/dxg")
wsl_amd = wsl and any(
os.path.exists(os.path.join(d, n))
for d in _WSL_ROCM_LIB_DIRS
for n in ("librocdxg.so", "librocdxg.so.1")
)
wsl_nvidia = wsl and any(Path(_WSL_CUDA_LIB_DIR).glob("libcuda.so*"))
vendors |= {v for v, on in (("amd", wsl_amd), ("nvidia", wsl_nvidia)) if on}
detected = set(vendors)
# A container that exposes only one vendor's device nodes, or a mask hiding a vendor,
# must not make its backend look runnable on a hybrid box.
if "nvidia" in vendors and (
not (os.path.exists("/dev/nvidiactl") or wsl_nvidia)
or _mask_hides_all("CUDA_VISIBLE_DEVICES")
):
vendors.discard("nvidia")
if "amd" in vendors and (
not (os.path.exists("/dev/kfd") or wsl_amd) or _amd_mask_hides_all()
):
vendors.discard("amd")
return vendors if detected else None
return vendors or None
def _fits_host(backends: set[str], vendors: Optional[set[str]]) -> bool:
if vendors is None:
return True
return any(_BACKEND_VENDOR.get(b) is None or _BACKEND_VENDOR[b] in vendors for b in backends)
def prefer_gpu_capable(
candidates: Iterable[Path],
usable: Callable[[Path], bool],
vendors: Any = _HOST,
) -> list[Path]:
"""Search order, except that a first hit proven CPU-only, or proven built for a GPU vendor
this host lacks, yields to a later usable build shipping a GPU backend this host's vendor
runs (#5941: a CPU build/ beside build-cuda/; a stale build-cuda/ must not shadow
build-hip/ on an AMD box, whether or not a build/ precedes it). Unknown layouts stay put."""
ordered = list(candidates)
first = next((c for c in ordered if usable(c)), None)
if first is None:
return ordered
verdict = binary_gpu_verdict(first)
if verdict == "unknown":
return ordered
if vendors is _HOST:
vendors = host_gpu_vendors()
if verdict == "gpu" and _fits_host(binary_gpu_backends(first), vendors):
return ordered
for candidate in ordered:
if candidate == first or not usable(candidate):
continue
backends = binary_gpu_backends(candidate)
if backends and _fits_host(backends, vendors):
return [candidate] + [c for c in ordered if c != candidate]
return ordered
def resolve_llama_server_binary(
directory: Path | str, *, platform: Optional[str] = None
) -> Optional[Path]:
"""Return the first executable llama-server in a supported layout, GPU-capable preferred."""
usable = lambda candidate: _usable_binary(candidate, platform = platform) # noqa: E731
ordered = prefer_gpu_capable(llama_server_candidates(directory, platform = platform), usable)
return next((candidate for candidate in ordered if usable(candidate)), None)
def get_stored_custom_llama_cpp_path() -> Optional[Path]:
"""The Unsloth-selected directory, or ``None`` when automatic discovery is active."""
try:
from storage.studio_db import get_app_setting
from utils.account_context import OWNER, run_as
value = run_as(OWNER, get_app_setting, CUSTOM_LLAMA_CPP_PATH_SETTING_KEY, None)
except Exception:
# A settings DB problem must not take the bundled runtime down with it.
return None
if not isinstance(value, str):
return None
value = value.strip()
if not value or len(value) > MAX_CUSTOM_LLAMA_CPP_PATH_LENGTH:
return None
return expanded_user_path(value)
def _environment_override() -> tuple[Optional[str], Optional[str], bool]:
"""``(path, variable, direct_binary)`` for the existing environment pins."""
direct = os.environ.get("LLAMA_SERVER_PATH", "").strip()
if direct:
return direct, "LLAMA_SERVER_PATH", True
directory = os.environ.get("UNSLOTH_LLAMA_CPP_PATH", "").strip()
if directory and os.environ.get(MANAGED_LLAMA_CPP_PATH_MARKER) != "1":
return directory, "UNSLOTH_LLAMA_CPP_PATH", False
return None, None, False
def custom_llama_cpp_path_source() -> str:
"""The active custom-path authority: environment, studio, or default."""
env_path, _variable, _direct = _environment_override()
if env_path is not None:
return "environment"
if get_stored_custom_llama_cpp_path() is not None:
return "studio"
return "default"
def _canonical_directory(value: str) -> Path:
raw = value.strip()
if not raw:
raise ValueError("Choose a llama.cpp folder or use the bundled runtime.")
if len(raw) > MAX_CUSTOM_LLAMA_CPP_PATH_LENGTH:
raise ValueError("The llama.cpp folder path is too long.")
try:
directory = expanded_user_path(raw).resolve(strict = True)
except (OSError, RuntimeError, ValueError) as exc:
raise ValueError("The llama.cpp folder does not exist or cannot be accessed.") from exc
if not directory.is_dir():
raise ValueError("The custom llama.cpp path must be a folder.")
if resolve_llama_server_binary(directory) is None:
binary_name = llama_server_binary_name()
raise ValueError(
f"No executable {binary_name} was found in that folder or its build/bin directory."
)
return directory
def set_custom_llama_cpp_path(value: Optional[str]) -> Optional[Path]:
"""Store a validated directory. ``None`` restores automatic discovery."""
global _path_revision
env_path, variable, _direct = _environment_override()
if env_path is not None:
raise RuntimeError(f"The llama.cpp path is managed by the {variable} environment variable.")
directory = _canonical_directory(value) if value is not None else None
with _settings_lock:
from storage.studio_db import upsert_app_settings
upsert_app_settings(
{CUSTOM_LLAMA_CPP_PATH_SETTING_KEY: (str(directory) if directory is not None else None)}
)
_path_revision += 1
return directory
def custom_llama_cpp_path_revision() -> int:
"""In-process revision used to retire sidecars launched before a path save."""
with _settings_lock:
return _path_revision
def custom_llama_cpp_path_status() -> dict:
"""UI payload describing the effective custom-path selection."""
env_path, variable, direct_binary = _environment_override()
source = "default"
path: Optional[Path] = None
binary: Optional[Path] = None
if env_path is not None:
source = "environment"
path = expanded_user_path(env_path)
if direct_binary:
binary = path if _usable_binary(path) else None
else:
binary = resolve_llama_server_binary(path)
else:
path = get_stored_custom_llama_cpp_path()
if path is not None:
source = "studio"
binary = resolve_llama_server_binary(path)
return {
"path": str(path) if path is not None else None,
"source": source,
"editable": source != "environment",
"available": source == "default" or binary is not None,
"resolved_binary": str(binary) if binary is not None else None,
"environment_variable": variable,
}