* Studio: offer the in-app llama.cpp update for source-build (markerless) installs Source-build installs have no UNSLOTH_PREBUILT_INFO.json marker, so freshness reported supported=False and the Update button never showed (notably on macOS, where the fork shipped no prebuilt before b9585 and setup fell back to a source build). When an install has no marker but an official prebuilt now exists for the host, surface the update and let one click swap it in place. - install_llama_prebuilt.py: published_repo_for_host() (the setup.sh host->repo rule in Python) and a --resolve-prebuilt mode that reports whether a prebuilt exists for this host without downloading. - llama_cpp_update.py: markerless branch in get_update_status/start_update, version-suppressed so source builds already newer than latest are not nagged; fail-open throughout. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Studio: run llama update detection off the event loop, expose source_build The markerless source-build check probes the host and reads GitHub, so run get_update_status and start_update in a worker thread to keep the API responsive. Expose source_build in the status response so the banner can label the source-build switch. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Keep the llama-route auth stub out of sys.modules for the rest of the suite test_llama_route.py replaced sys.modules['auth.authentication'] with a bare stub at collection time and never restored it, so every later test importing create_access_token got the stub: 17 failures across test_desktop_auth, test_middleware, test_openai_tool_passthrough and test_rag_preview on all four Backend CI Python versions. Import the real module when its deps are available and only stub in minimal envs, popping the stubs after the standalone route load either way. * Studio: address review on the source-build update path - published_repo_for_host: route CPU-only Windows to ggml-org too (mirrors setup.ps1; the fork ships no win-cpu bundle), macOS always the fork. - markerless detection compares/display the upstream llama_tag, not a possible fork wrapper release_tag, so a source build is not wrongly judged newer. - do not offer when there is no resolvable install root (a pinned LLAMA_SERVER_PATH outside a managed dir): an apply would not take effect. * Ignore version probes in the update tests' subprocess capture The status polls in these tests trigger the new source-build detection, which shells out to llama-server --version through the same patched subprocess.run. On slow runners that probe lands after the installer call and clobbers the single captured argv, failing the flag assertions (seen on the 3.10/3.11 Backend CI jobs). Skip probe calls in all three fakes so only the installer invocation is captured. * Skip markerless re-detection while the update job is swapping the tree On a source-build install the frontend polls update-status every 3s during an apply, and each poll ran _source_build_status, which execs the very llama-server binary the job is concurrently replacing. On Windows that exec can hold the exe long enough to fail the installer's os.replace; everywhere it is a per-poll subprocess spawn for a status the poller does not read (it only consumes job progress). Gate the markerless branch on the job not running; the marked path is probe-free and still returns the live job state. * Studio: tighten source-build update root, repo routing, and downgrade guard Only manage a markerless install when the active binary lives under a resolvable llama.cpp root (marker dir, UNSLOTH_LLAMA_CPP_PATH it sits in, or a llama.cpp ancestor); a pinned LLAMA_SERVER_PATH or a PATH/system binary is left alone so an apply cannot install where it would not take effect. Gate start_update on the same suppression as detection so a direct POST cannot downgrade a source build newer than the latest prebuilt. Route Linux hosts with AMD tooling (rocminfo/amd-smi/hipconfig/ hipinfo) to the fork in --resolve-prebuilt, matching setup.sh, so a HIP source build is not offered an upstream CPU prebuilt. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Studio: cover inactive env root and pinned llama.cpp checkout in update root tests --------- Co-authored-by: danielhanchen <michaelhan2050@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
173 lines
5.9 KiB
Python
173 lines
5.9 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
|
|
|
|
"""install_llama_prebuilt.py: host->repo mapping and the --resolve-prebuilt mode.
|
|
|
|
These back the in-app update for source-build (markerless) installs: the backend
|
|
asks the installer whether an official prebuilt exists for this host without
|
|
downloading. Network and host detection are stubbed; no GPU or internet needed.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
_studio = Path(__file__).resolve().parent.parent.parent
|
|
if str(_studio) not in sys.path:
|
|
sys.path.insert(0, str(_studio))
|
|
|
|
ilp = importlib.import_module("install_llama_prebuilt")
|
|
|
|
if not hasattr(ilp, "published_repo_for_host") or not hasattr(
|
|
ilp, "resolve_simple_install_release_plans"
|
|
):
|
|
pytest.skip("PR symbols not present - check branch", allow_module_level = True)
|
|
|
|
FORK = ilp.DEFAULT_PUBLISHED_REPO # unslothai/llama.cpp
|
|
UPSTREAM = ilp.UPSTREAM_REPO # ggml-org/llama.cpp
|
|
|
|
|
|
def _host(**kw):
|
|
base = dict(
|
|
system = "Linux",
|
|
machine = "x86_64",
|
|
is_windows = False,
|
|
is_linux = False,
|
|
is_macos = False,
|
|
is_x86_64 = False,
|
|
is_arm64 = False,
|
|
nvidia_smi = None,
|
|
driver_cuda_version = None,
|
|
compute_caps = [],
|
|
visible_cuda_devices = None,
|
|
has_physical_nvidia = False,
|
|
has_usable_nvidia = False,
|
|
has_rocm = False,
|
|
rocm_gfx_target = None,
|
|
macos_version = None,
|
|
)
|
|
base.update(kw)
|
|
return ilp.HostInfo(**base)
|
|
|
|
|
|
def test_published_repo_for_host():
|
|
# CPU-only Linux (x64 and arm64) -> ggml-org upstream.
|
|
assert ilp.published_repo_for_host(_host(is_linux = True, is_x86_64 = True)) == UPSTREAM
|
|
assert (
|
|
ilp.published_repo_for_host(_host(is_linux = True, is_arm64 = True, machine = "aarch64"))
|
|
== UPSTREAM
|
|
)
|
|
# GPU Linux -> fork.
|
|
assert (
|
|
ilp.published_repo_for_host(_host(is_linux = True, is_x86_64 = True, has_usable_nvidia = True))
|
|
== FORK
|
|
)
|
|
assert ilp.published_repo_for_host(_host(is_linux = True, is_x86_64 = True, has_rocm = True)) == FORK
|
|
# CPU-only Windows -> ggml-org (setup.ps1: the fork ships no win-cpu bundle).
|
|
assert (
|
|
ilp.published_repo_for_host(_host(system = "Windows", is_windows = True, is_x86_64 = True))
|
|
== UPSTREAM
|
|
)
|
|
# GPU Windows -> fork.
|
|
assert (
|
|
ilp.published_repo_for_host(
|
|
_host(system = "Windows", is_windows = True, is_x86_64 = True, has_usable_nvidia = True)
|
|
)
|
|
== FORK
|
|
)
|
|
# macOS -> fork regardless of GPU (ggml-org macOS bundles need too-new macOS).
|
|
assert (
|
|
ilp.published_repo_for_host(
|
|
_host(system = "Darwin", is_macos = True, is_arm64 = True, machine = "arm64")
|
|
)
|
|
== FORK
|
|
)
|
|
# Linux with AMD tooling but no probed GPU -> fork (setup.sh routes on tooling).
|
|
assert (
|
|
ilp.published_repo_for_host(
|
|
_host(is_linux = True, is_x86_64 = True), linux_amd_tooling_present = True
|
|
)
|
|
== FORK
|
|
)
|
|
# The tooling hint is Linux-only: Windows CPU stays on ggml-org.
|
|
assert (
|
|
ilp.published_repo_for_host(
|
|
_host(system = "Windows", is_windows = True, is_x86_64 = True),
|
|
linux_amd_tooling_present = True,
|
|
)
|
|
== UPSTREAM
|
|
)
|
|
|
|
|
|
def _run_resolve(monkeypatch, capsys, plans_or_exc):
|
|
monkeypatch.setattr(
|
|
ilp,
|
|
"detect_host",
|
|
lambda: _host(system = "Darwin", is_macos = True, is_arm64 = True, machine = "arm64"),
|
|
)
|
|
|
|
def _resolver(tag, host, repo, published_release_tag):
|
|
if isinstance(plans_or_exc, Exception):
|
|
raise plans_or_exc
|
|
return ("b9585", plans_or_exc)
|
|
|
|
monkeypatch.setattr(ilp, "resolve_simple_install_release_plans", _resolver)
|
|
monkeypatch.setattr(
|
|
sys,
|
|
"argv",
|
|
["install_llama_prebuilt.py", "--resolve-prebuilt", "latest", "--output-format", "json"],
|
|
)
|
|
rc = ilp.main()
|
|
assert rc == ilp.EXIT_SUCCESS
|
|
return json.loads(capsys.readouterr().out.strip().splitlines()[-1])
|
|
|
|
|
|
def test_resolve_prebuilt_available(monkeypatch, capsys):
|
|
plan = SimpleNamespace(
|
|
release_tag = "b9585",
|
|
llama_tag = "b9585",
|
|
attempts = [
|
|
SimpleNamespace(name = "llama-b9585-bin-macos-arm64.tar.gz", install_kind = "macos-arm64")
|
|
],
|
|
)
|
|
out = _run_resolve(monkeypatch, capsys, [plan])
|
|
assert out["prebuilt_available"] is True
|
|
assert out["repo"] == FORK
|
|
assert out["release_tag"] == "b9585"
|
|
assert out["asset"] == "llama-b9585-bin-macos-arm64.tar.gz"
|
|
assert out["install_kind"] == "macos-arm64"
|
|
|
|
|
|
def test_resolve_prebuilt_unavailable(monkeypatch, capsys):
|
|
out = _run_resolve(monkeypatch, capsys, ilp.PrebuiltFallback("no macOS asset"))
|
|
assert out["prebuilt_available"] is False
|
|
assert out["repo"] == FORK
|
|
|
|
|
|
def test_resolve_prebuilt_linux_amd_tooling_routes_to_fork(monkeypatch, capsys):
|
|
# CPU-probed Linux host but rocminfo on PATH: the dispatch must route to the
|
|
# fork so a HIP source build is not offered an upstream CPU prebuilt.
|
|
monkeypatch.setattr(ilp, "detect_host", lambda: _host(is_linux = True, is_x86_64 = True))
|
|
monkeypatch.setattr(ilp.shutil, "which", lambda tool: tool == "rocminfo")
|
|
seen = {}
|
|
|
|
def _resolver(tag, host, repo, published_release_tag):
|
|
seen["repo"] = repo
|
|
raise ilp.PrebuiltFallback("no asset")
|
|
|
|
monkeypatch.setattr(ilp, "resolve_simple_install_release_plans", _resolver)
|
|
monkeypatch.setattr(
|
|
sys,
|
|
"argv",
|
|
["install_llama_prebuilt.py", "--resolve-prebuilt", "latest", "--output-format", "json"],
|
|
)
|
|
assert ilp.main() == ilp.EXIT_SUCCESS
|
|
out = json.loads(capsys.readouterr().out.strip().splitlines()[-1])
|
|
assert seen["repo"] == FORK
|
|
assert out["repo"] == FORK
|