Studio: source CPU llama.cpp prebuilts from unslothai/llama.cpp (#6311)

* Studio: source CPU llama.cpp prebuilts from the unslothai fork

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

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

* Studio: reject unknown Linux CPU arches and keep ROCm-tooling hosts off the CPU prebuilt

* Studio: extend the resolve-prebuilt ROCm-tooling guard to Windows

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

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

* Studio: let ROCm-SDK-only CPU hosts take the fork CPU prebuilt

* Studio: accept windows-arm64 prebuilt kind and refresh stale fork-routing comments

* Studio: correct stale fork-routing comments and --resolve-prebuilt help

* Refresh stale ggml-org routing comments

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
This commit is contained in:
oobabooga 2026-07-08 09:34:59 -03:00 committed by GitHub
parent 41dd95ea0a
commit fcb1152c76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 322 additions and 265 deletions

View file

@ -6,9 +6,9 @@
# windows-latest runner:
#
# 1. install.ps1 --local --no-torch installs Studio AND auto-fetches
# the prebuilt llama.cpp Windows binary (llama-bNNNN-bin-win-cpu-
# x64 from ggml-org/llama.cpp). Hitting the source-build fallback
# is treated as an Unsloth bug -- Studio must always pick the
# the prebuilt llama.cpp Windows binary (app-<tag>-windows-x64-cpu
# from unslothai/llama.cpp). Hitting the source-build fallback is
# treated as an Unsloth bug -- Studio must always pick the
# prebuilt on Windows.
# 2. unsloth studio update --local is idempotent. Two consecutive
# runs both report "prebuilt up to date and validated", no

View file

@ -1,7 +1,8 @@
# 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.
"""install_llama_prebuilt.py: the --resolve-prebuilt probe (plans against the fork
by default; --published-repo overrides).
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
@ -24,9 +25,7 @@ if str(_studio) not in sys.path:
ilp = importlib.import_module("install_llama_prebuilt")
if not hasattr(ilp, "published_repo_for_host") or not hasattr(
ilp, "resolve_simple_install_release_plans"
):
if 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
@ -56,73 +55,6 @@ def _host(**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 test_macos_intel_and_arm_both_route_to_fork():
# macOS uses the unslothai fork's own Mac prebuilts for BOTH arm64 and Intel;
# there is no longer any upstream-on-macOS default path, so the obsolete
# pre-macOS-26 pin (b9415) is gone.
assert (
ilp.published_repo_for_host(
_host(system = "Darwin", is_macos = True, is_arm64 = True, machine = "arm64")
)
== FORK
)
assert (
ilp.published_repo_for_host(
_host(system = "Darwin", is_macos = True, is_x86_64 = True, machine = "x86_64")
)
== FORK
)
def test_macos_upstream_pin_only_for_explicit_pre26_upstream():
pre26 = _host(
system = "Darwin",
@ -188,15 +120,13 @@ def test_resolve_prebuilt_unavailable(monkeypatch, capsys):
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")
def _run_resolve_capture_host(monkeypatch, capsys):
"""Drive --resolve-prebuilt and return the host the resolver was handed."""
seen = {}
def _resolver(tag, host, repo, published_release_tag):
seen["repo"] = repo
seen["host"] = host
raise ilp.PrebuiltFallback("no asset")
monkeypatch.setattr(ilp, "resolve_simple_install_release_plans", _resolver)
@ -207,10 +137,33 @@ def test_resolve_prebuilt_linux_amd_tooling_routes_to_fork(monkeypatch, capsys):
)
assert ilp.main() == ilp.EXIT_SUCCESS
out = json.loads(capsys.readouterr().out.strip().splitlines()[-1])
return seen, out
def test_resolve_prebuilt_cpu_linux_routes_to_fork(monkeypatch, capsys):
# CPU-only Linux host (no GPU): the dispatch routes to the fork, which now
# ships the CPU prebuilt -- it no longer falls back to ggml-org upstream.
monkeypatch.setattr(ilp, "detect_host", lambda: _host(is_linux = True, is_x86_64 = True))
seen, out = _run_resolve_capture_host(monkeypatch, capsys)
assert seen["repo"] == FORK
assert out["repo"] == FORK
def test_resolve_prebuilt_rocm_sdk_only_host_still_offered_cpu(monkeypatch, capsys):
# A CPU-only host that merely has ROCm/HIP SDK tools on PATH (no AMD GPU, so
# detect_host leaves has_rocm False) is a valid CPU-prebuilt target. The probe
# must NOT reclassify it as ROCm from tool presence alone and suppress the CPU
# bundle -- that would deny the fork CPU prebuilt to a legitimate CPU source
# build. The host is left CPU-only and resolves against the fork.
monkeypatch.setattr(ilp, "detect_host", lambda: _host(is_linux = True, is_x86_64 = True))
monkeypatch.setattr(
ilp.shutil, "which", lambda tool: "/opt/rocm/bin/hipconfig" if tool == "hipconfig" else None
)
seen, out = _run_resolve_capture_host(monkeypatch, capsys)
assert seen["repo"] == FORK
assert seen["host"].has_rocm is False
# Blackwell floor is sm_100 (data-center B100/B200, B300/GB300), below consumer
# sm_120 -- 120 wrongly excluded data-center hosts from the prebuilt selection.

View file

@ -137,9 +137,9 @@ def test_read_install_marker_finds_windows_cmake_layout(tmp_path):
@pytest.mark.parametrize("repo", ["unslothai/llama.cpp", "ggml-org/llama.cpp"])
def test_read_install_marker_carries_published_repo_dynamically(tmp_path, repo):
# The freshness check queries whichever release repo the marker records,
# so CUDA (unslothai), CPU/macOS (ggml-org), and ROCm all get the right
# "latest" tag.
# The freshness check queries whichever release repo the marker records:
# new installs record the fork, legacy CPU/macOS markers still say ggml-org,
# and both must get the right "latest" tag.
install_dir = tmp_path / "llama.cpp"
_write_marker(install_dir, tag = "b9000", published_repo = repo)
bin_path = _fake_binary(install_dir, layout = "cmake")

View file

@ -594,9 +594,10 @@ def test_install_cmd_fork_rocm_marker_forwards_has_rocm(monkeypatch, tmp_path):
def test_install_cmd_ggml_cpu_marker_has_no_cpu_fallback(monkeypatch, tmp_path):
# CPU installs come from ggml-org. Re-running into the same install-dir/repo
# reproduces the same CPU bundle; --cpu-fallback (which force-drops GPU
# detection) is reserved for setup.sh's arm64 rescue and must not appear here.
# Legacy CPU installs recorded a ggml-org marker (new installs use the fork).
# Re-running into the same install-dir/repo reproduces the same CPU bundle;
# --cpu-fallback (which force-drops GPU detection) is reserved for setup.sh's
# arm64 rescue and must not appear here.
cmd = _capture_install_cmd(
monkeypatch,
tmp_path,

View file

@ -165,9 +165,9 @@ def env_int(
# errors. Only use "master" temporarily when the latest release is missing
# support for a new model architecture.
DEFAULT_LLAMA_TAG = os.environ.get("UNSLOTH_LLAMA_TAG", "latest")
# Default published repo for prebuilt release resolution. Linux uses
# Unsloth prebuilts; setup.sh/setup.ps1 pass --published-repo explicitly
# for macOS/Windows to override with ggml-org/llama.cpp when needed.
# Default published repo for prebuilt release resolution. Every host plans
# its prebuilt against the Unsloth fork; setup.sh/setup.ps1 pass it via
# --published-repo. ggml-org is reachable only via an explicit override.
DEFAULT_PUBLISHED_REPO = "unslothai/llama.cpp"
DEFAULT_PUBLISHED_TAG = os.environ.get("UNSLOTH_LLAMA_RELEASE_TAG")
DEFAULT_PUBLISHED_MANIFEST_ASSET = os.environ.get(
@ -3135,21 +3135,6 @@ def _apply_host_overrides(
return host
def published_repo_for_host(host: HostInfo, *, linux_amd_tooling_present: bool = False) -> str:
"""The release repo setup.sh / setup.ps1 pick for this host: macOS always the
fork (ggml-org macOS bundles need too-new macOS); else CPU-only Linux/Windows
-> ggml-org upstream (the fork ships no CPU bundle) and any usable GPU (NVIDIA
or ROCm) -> the fork. linux_amd_tooling_present mirrors setup.sh routing Linux
hosts that expose AMD tooling (rocminfo/amd-smi/hipconfig/hipinfo) to the fork
even when the probe cannot confirm an active GPU. Mirrors the shell routing."""
if host.is_macos:
return DEFAULT_PUBLISHED_REPO
has_gpu = (
host.has_usable_nvidia or host.has_rocm or (host.is_linux and linux_amd_tooling_present)
)
return DEFAULT_PUBLISHED_REPO if has_gpu else UPSTREAM_REPO
def pick_windows_cuda_runtime(host: HostInfo) -> str | None:
if not host.driver_cuda_version:
return None
@ -4015,6 +4000,9 @@ def resolve_release_asset_choice(
published_choice = published_rocm_choice_for_host(release, host, "windows-rocm")
else:
published_choice = published_asset_choice_for_kind(release, "windows-cpu")
elif host.is_windows and host.is_arm64:
# Windows arm64 has no GPU prebuilt, so it always takes the CPU bundle.
published_choice = published_asset_choice_for_kind(release, "windows-arm64")
elif host.is_macos and host.is_arm64:
published_choice = published_asset_choice_for_kind(release, "macos-arm64")
elif host.is_macos and host.is_x86_64:
@ -6127,8 +6115,13 @@ def _linux_published_attempts(host: HostInfo, bundle: PublishedReleaseBundle) ->
# CPU-only host. A usable-NVIDIA host never reaches here -- if its CUDA
# selection produced nothing we want an empty attempt list so the caller
# source-builds with CUDA, not a CPU-only binary silently installed on a
# GPU host (mirrors the ROCm branch, and Windows NVIDIA).
cpu_choice = published_asset_choice_for_kind(bundle, "linux-cpu")
# GPU host (mirrors the ROCm branch, and Windows NVIDIA). Only x86_64 and
# arm64 have a CPU bundle; any other Linux arch (ppc64le, riscv64, s390x)
# has none, so leave attempts empty and source-build rather than hand it
# the x86_64 linux-cpu binary (the Linux preflight checks libraries, not
# ELF arch, so a wrong-arch binary would not be caught).
kind = "linux-cpu" if host.is_x86_64 else "linux-arm64" if host.is_arm64 else None
cpu_choice = published_asset_choice_for_kind(bundle, kind) if kind else None
if cpu_choice is not None:
attempts.append(cpu_choice)
return attempts
@ -6143,9 +6136,9 @@ def _fork_manifest_release_plans(
max_release_fallbacks: int = DEFAULT_MAX_PREBUILT_RELEASE_FALLBACKS,
) -> tuple[str, list[InstallReleasePlan]]:
"""Manifest-reading branch of resolve_simple_install_release_plans, used for
the fork's bundles whose GPU/arch coverage lives in
llama-prebuilt-manifest.json rather than in the filename: arm64 CUDA, Windows
CUDA, per-gfx ROCm, and macOS. Linux x64 takes the faster filename path."""
every fork host: all of the fork's bundles describe their GPU/arch coverage
in llama-prebuilt-manifest.json rather than in the asset filename (CPU,
x64/arm64 CUDA, Windows CUDA, per-gfx ROCm, and macOS)."""
requested_tag = normalized_requested_llama_tag(llama_tag)
allow_older_release_fallback = requested_tag == "latest" and not published_release_tag
release_limit = max(1, max_release_fallbacks)
@ -6714,8 +6707,8 @@ def install_prebuilt(
log(
f"no existing llama.cpp install detected at {install_dir}; performing fresh prebuilt install"
)
# Single resolver: linux-x64 takes the fast filename path internally,
# every other fork host reads the manifest.
# Single resolver: every fork host selects from the release manifest;
# an explicit ggml-org override selects by asset filename instead.
requested_tag, release_plans = resolve_simple_install_release_plans(
llama_tag,
host,
@ -6903,8 +6896,8 @@ def parse_args() -> argparse.Namespace:
const = "latest",
help = (
"Report whether an official prebuilt exists for this host without "
"downloading. Picks the host's published repo when --published-repo "
"is left at the default. Use --output-format json."
"downloading. Plans against --published-repo (defaults to the "
"fork). Use --output-format json."
),
)
parser.add_argument(
@ -6992,24 +6985,16 @@ def main() -> int:
return EXIT_SUCCESS
if args.resolve_prebuilt is not None:
# Host-aware "is a prebuilt available" probe, no download. A default repo
# means "pick the repo for this host"; PrebuiltFallback == source build.
# Host-aware "is a prebuilt available" probe, no download. Every host now
# plans against the fork (args.published_repo defaults to it); an explicit
# --published-repo overrides. PrebuiltFallback == source build.
host = _apply_host_overrides(
detect_host(),
override_has_rocm = args.has_rocm,
override_rocm_gfx = args.rocm_gfx,
force_cpu = args.cpu_fallback,
)
# setup.sh routes Linux hosts with AMD tooling to the fork even when no GPU
# is probed; mirror that so a HIP source build is not offered a CPU prebuilt.
amd_tooling = host.is_linux and any(
shutil.which(t) for t in ("rocminfo", "amd-smi", "hipconfig", "hipinfo")
)
repo = (
published_repo_for_host(host, linux_amd_tooling_present = amd_tooling)
if args.published_repo == DEFAULT_PUBLISHED_REPO
else args.published_repo
)
repo = args.published_repo
try:
_requested, plans = resolve_simple_install_release_plans(
args.resolve_prebuilt, host, repo, args.published_release_tag or ""

View file

@ -3088,12 +3088,11 @@ $LlamaCppDir = Join-Path $UnslothHome "llama.cpp"
$NeedLlamaSourceBuild = $false
$SkipPrebuiltInstall = $false
$RequestedLlamaTag = if ($env:UNSLOTH_LLAMA_TAG) { $env:UNSLOTH_LLAMA_TAG } else { $DefaultLlamaTag }
# GPU Windows (CUDA / ROCm) installs the fork's app-* prebuilts; CPU-only stays
# on ggml-org (the fork ships no windows-cpu bundle). Mirrors setup.sh's routing.
# A resolved gfx arch counts as a GPU host even when $HasROCm is false (Adrenalin
# driver only, no HIP runtime): the fork's per-gfx bundle ships its own runtime,
# so route there instead of ggml-org / a CPU build.
$HelperReleaseRepo = if ($HasNvidiaSmi -or $HasROCm -or $script:ROCmGfxArch) { "unslothai/llama.cpp" } else { "ggml-org/llama.cpp" }
# Every host installs the fork's app-* prebuilts now: GPU Windows (CUDA / ROCm)
# already did, and the fork now also ships the CPU bundles for Windows x64 and
# arm64 (windows-cpu / windows-arm64). ggml-org artifacts are no longer used by
# default. Mirrors setup.sh's routing.
$HelperReleaseRepo = "unslothai/llama.cpp"
$LlamaPr = if ($env:UNSLOTH_LLAMA_PR) { $env:UNSLOTH_LLAMA_PR.Trim() } else { "" }
$LlamaPrForce = if ($env:UNSLOTH_LLAMA_PR_FORCE) { $env:UNSLOTH_LLAMA_PR_FORCE.Trim() } else { $DefaultLlamaPrForce }
@ -3283,11 +3282,13 @@ if ($LocalLlamaCppLinked) {
# treat a valid ROCm install as mismatched. A name-inferred gfx
# arch (Adrenalin-only, no confirmed runtime) still counts as
# ROCm-capable -- the ROCm prebuilt bundles its own runtime,
# mirroring the --rocm-gfx forward below. NOTE: this block is
# currently inert -- write_prebuilt_metadata does not persist an
# install_kind key, so $existingKind is always null. If that changes,
# add the remaining host kinds (e.g. windows-arm64) before relying on it.
$expectedKinds = if ($HasROCm -or $script:ROCmGfxArch) { @("windows-rocm", "windows-hip") } elseif ($HasNvidiaSmi) { @("windows-cuda") } else { @("windows-cpu") }
# mirroring the --rocm-gfx forward below. The CPU branch covers both
# the x64 windows-cpu and arm64 windows-arm64 bundles (Windows arm64
# has no GPU prebuilt). NOTE: this block is currently inert --
# write_prebuilt_metadata does not persist an install_kind key, so
# $existingKind is always null; keep $expectedKinds in sync with the
# kinds install_llama_prebuilt.py installs before relying on it.
$expectedKinds = if ($HasROCm -or $script:ROCmGfxArch) { @("windows-rocm", "windows-hip") } elseif ($HasNvidiaSmi) { @("windows-cuda") } else { @("windows-cpu", "windows-arm64") }
if ($existingKind -and ($existingKind -notin $expectedKinds)) {
substep "Removing mismatched llama.cpp install (found '$existingKind', need one of: $($expectedKinds -join ', '))..."
Remove-Item -Recurse -Force -LiteralPath $LlamaCppDir -ErrorAction SilentlyContinue

View file

@ -1176,63 +1176,19 @@ _REQUESTED_LLAMA_TAG="${UNSLOTH_LLAMA_TAG:-${_DEFAULT_LLAMA_TAG}}"
_HOST_SYSTEM="$(uname -s 2>/dev/null || true)"
_HOST_MACHINE="$(uname -m 2>/dev/null || true)"
# Pick the release repo install_llama_prebuilt.py plans against.
# The fork ships CUDA (Linux x64/arm64, Windows), ROCm (Linux/Windows) and
# macOS bundles. Only the plain CPU/Vulkan bundles still come from ggml-org, so
# CPU-only Linux (x86_64 and arm64) routes there; GPU Linux, Windows and macOS
# use unslothai.
_LINUX_HAS_GPU=false
# Route to the fork only for a usable GPU. NVIDIA counts only when a device is
# actually enumerated and not hidden via CUDA_VISIBLE_DEVICES=""/-1
# (_setup_nvidia_usable, from _setup_has_usable_nvidia_gpu above) -- mirroring
# install_llama_prebuilt.py's has_usable_nvidia. Mere nvidia-smi presence
# (CPU-only CUDA-toolkit containers, broken drivers) or a hidden GPU therefore
# takes the ggml-org CPU prebuilt instead of a slow source build. AMD is
# deliberately left on tooling presence, not usability: an unusable NVIDIA host
# has a good CPU prebuilt to fall back to, whereas tightening AMD would regress
# ROCm hosts exposing only hipconfig/hipinfo into an unnecessary CPU build.
if [ "$_setup_nvidia_usable" = true ]; then
_LINUX_HAS_GPU=true
else
for _GPU_TOOL in rocminfo amd-smi hipconfig hipinfo; do
if command -v "$_GPU_TOOL" >/dev/null 2>&1; then
_LINUX_HAS_GPU=true
break
fi
done
fi
# Pick the release repo install_llama_prebuilt.py plans against. Every host this
# installer supports now pulls its llama.cpp prebuilt from the unslothai fork: it
# ships the CUDA (Linux x64/arm64, Windows), ROCm (Linux/Windows) and macOS
# bundles, plus the CPU bundles for Linux/Windows on both x86_64 and arm64.
# ggml-org artifacts are no longer used by default.
_HELPER_RELEASE_REPO="unslothai/llama.cpp"
# UNSLOTH_ROCM_GFX_ARCH may be set on a host where no probe fired, so the override
# nested in the AMD-detected branch above never ran and _setup_gfx is still empty.
# Honour it here so the routing guard below and the --rocm-gfx forwarding both see
# it (install_llama_prebuilt.py reads the same env var as the --rocm-gfx default).
if [ "$_setup_nvidia_usable" != true ] && [ -z "${_setup_gfx:-}" ] && [ -n "${UNSLOTH_ROCM_GFX_ARCH:-}" ]; then
# Honour it here so the --rocm-gfx forwarding below still sees it
# (install_llama_prebuilt.py reads the same env var as the --rocm-gfx default).
if [ "${_setup_nvidia_usable:-}" != true ] && [ -z "${_setup_gfx:-}" ] && [ -n "${UNSLOTH_ROCM_GFX_ARCH:-}" ]; then
_setup_gfx="${UNSLOTH_ROCM_GFX_ARCH}"
fi
# A resolved/forwarded gfx arch (UNSLOTH_ROCM_GFX_ARCH) means an AMD GPU even when
# no ROCm tooling is on PATH; route it to the fork so the per-gfx prebuilt is
# picked instead of ggml-org / a source build.
if [ "$_LINUX_HAS_GPU" = false ] && [ -n "${_setup_gfx:-}" ]; then
_LINUX_HAS_GPU=true
fi
if [ "$_HOST_SYSTEM" = "Linux" ] \
&& [ "$_HOST_MACHINE" = "x86_64" ] \
&& [ "$_LINUX_HAS_GPU" = false ]; then
_HELPER_RELEASE_REPO="ggml-org/llama.cpp"
elif [ "$_HOST_SYSTEM" = "Linux" ] \
&& { [ "$_HOST_MACHINE" = "aarch64" ] || [ "$_HOST_MACHINE" = "arm64" ]; } \
&& [ "$_LINUX_HAS_GPU" = false ]; then
# CPU-only Linux ARM64 (Ampere Altra, Raspberry Pi 5, GitHub
# `ubuntu-24.04-arm`, CPU-only Jetson rescue mode, ...). The fork ships no
# arm64 CPU bundle, so without this branch the prebuilt resolver returns 0
# attempts and the installer falls back to a source build. ggml-org ships
# llama-bNNNN-bin-ubuntu-arm64.tar.gz from at least b9072 onward.
_HELPER_RELEASE_REPO="ggml-org/llama.cpp"
else
# GPU Linux (x64 CUDA/ROCm, arm64 CUDA), Windows (CUDA/ROCm), and macOS.
_HELPER_RELEASE_REPO="unslothai/llama.cpp"
fi
unset _GPU_TOOL
_LLAMA_PR="${UNSLOTH_LLAMA_PR:-}"
_SKIP_PREBUILT_INSTALL=false
_LLAMA_PR_FORCE="${UNSLOTH_LLAMA_PR_FORCE:-${_DEFAULT_LLAMA_PR_FORCE}}"
@ -1939,19 +1895,19 @@ else
fi # end _SKIP_GGUF_BUILD check
# ── arm64 Linux GPU: CPU prebuilt as a last resort ──
# arm64 Linux with a GPU has no CUDA prebuilt anywhere (the unslothai fork is
# x64 only; ggml-org ships no Linux CUDA build), so it source-builds for the
# GPU above. If that produced no binary, install ggml-org's arm64 CPU prebuilt
# instead of leaving the host without llama.cpp.
# An arm64 Linux GPU host source-builds for the GPU above. If that produced no
# binary, install the fork's arm64 CPU prebuilt (app-<tag>-linux-arm64-cpu.tar.gz)
# instead of leaving the host without llama.cpp. --cpu-fallback drops the GPU
# attributes so the CPU bundle is selected rather than re-attempting CUDA.
if [ "$_LLAMA_CPP_DEGRADED" = true ] \
&& [ "$_HOST_SYSTEM" = "Linux" ] \
&& { [ "$_HOST_MACHINE" = "aarch64" ] || [ "$_HOST_MACHINE" = "arm64" ]; }; then
substep "GPU source build unavailable; trying ggml-org arm64 CPU prebuilt..."
substep "GPU source build unavailable; trying arm64 CPU prebuilt..."
_ARM64_CPU_CMD=(
python "$SCRIPT_DIR/install_llama_prebuilt.py"
--install-dir "$LLAMA_CPP_DIR"
--llama-tag "$_REQUESTED_LLAMA_TAG"
--published-repo "ggml-org/llama.cpp"
--published-repo "unslothai/llama.cpp"
--cpu-fallback
)
# Trust the installer's exit code: it validates the server before exiting 0,

View file

@ -362,8 +362,11 @@ class TestSourcePatternsSh:
assert '_LLAMA_SOURCE="${_DEFAULT_LLAMA_SOURCE}"' in self.content
def test_release_repo_override_removed(self):
# No env-based release-repo override, and CPU-only hosts no longer fall
# back to ggml-org -- every host now routes to the fork.
assert "UNSLOTH_LLAMA_RELEASE_REPO:-unslothai/llama.cpp" not in self.content
assert '_HELPER_RELEASE_REPO="ggml-org/llama.cpp"' in self.content
assert '_HELPER_RELEASE_REPO="unslothai/llama.cpp"' in self.content
assert '_HELPER_RELEASE_REPO="ggml-org/llama.cpp"' not in self.content
def test_force_compile_skips_prebuilt_resolution_early(self):
assert 'if [ "$_LLAMA_FORCE_COMPILE" = "1" ]; then' in self.content
@ -425,12 +428,11 @@ class TestSourcePatternsPs1:
assert "$LlamaSource = $DefaultLlamaSource" in self.content
def test_release_repo_override_removed(self):
# Repo chosen by GPU detection (GPU -> fork, CPU -> ggml-org), no env override.
# No env-based release-repo override; every host now routes to the fork
# (the CPU-only ggml-org fallback was removed), mirroring setup.sh.
assert "$HelperReleaseRepo = if ($env:UNSLOTH_LLAMA_RELEASE_REPO)" not in self.content
assert (
"$HelperReleaseRepo = if ($HasNvidiaSmi -or $HasROCm -or $script:ROCmGfxArch) "
'{ "unslothai/llama.cpp" } else { "ggml-org/llama.cpp" }' in self.content
)
assert '$HelperReleaseRepo = "unslothai/llama.cpp"' in self.content
assert "$HelperReleaseRepo = if (" not in self.content
def test_force_compile_skips_prebuilt_resolution_early(self):
assert 'if ($env:UNSLOTH_LLAMA_FORCE_COMPILE -eq "1") {' in self.content

View file

@ -658,15 +658,21 @@ class TestSourceCodePatterns:
assert "_HELPER_RELEASE_REPO}/releases/latest" not in content
assert "ggml-org/llama.cpp/releases/latest" not in content
def test_setup_sh_routes_to_fork_only_on_usable_gpu(self):
"""Linux routing gates NVIDIA on GPU usability, not nvidia-smi presence, so
CPU-only/hidden-GPU hosts get the ggml CPU prebuilt. Guards the old presence-only loop."""
def test_setup_sh_routes_every_host_to_fork(self):
"""CPU-only Linux (the last ggml-org artifact consumer) now routes to the
fork like every other host, so the release-repo decision is unconditional.
Guards against a silent reintroduction of a ggml-org CPU routing branch.
GPU usability detection (used for PyTorch / source decisions) must stay."""
content = SETUP_SH.read_text()
assert '_HELPER_RELEASE_REPO="unslothai/llama.cpp"' in content
assert '_HELPER_RELEASE_REPO="ggml-org/llama.cpp"' not in content
# Usability gating (not routing) still distinguishes a hidden GPU.
assert '[ "$_setup_nvidia_usable" = true ]' in content
assert "CUDA_VISIBLE_DEVICES" in content
# nvidia-smi must NOT be back in the bare presence loop.
assert "for _GPU_TOOL in nvidia-smi" not in content
assert "for _GPU_TOOL in rocminfo amd-smi hipconfig hipinfo" in content
# The GPU-tooling probe (PR #4562) stays: ROCm detection goes through
# command -v, not a bare presence loop that mishandled a hidden nvidia-smi.
assert "command -v rocminfo" in content
assert "command -v amd-smi" in content
def test_setup_sh_reports_installed_prebuilt_release(self):
"""Shell wrapper should report the installed prebuilt release from metadata."""

View file

@ -3164,21 +3164,25 @@ class TestRocmGfxForwarding:
assert "--rocm-gfx" in source
assert "$script:ROCmGfxArch" in source
def test_setup_sh_routes_inferred_gfx_to_fork(self):
# An inferred gfx arch must route to the fork even without ROCm tooling.
# Pin the specific guard (a bare "${_setup_gfx:-}" also appears elsewhere).
def test_setup_sh_routes_unconditionally_to_fork(self):
# CPU-only hosts no longer fall back to ggml-org -- the release-repo
# decision is an unconditional fork assignment now. Pin the line text.
source = _SETUP_SH_PATH.read_text(encoding = "utf-8")
assert '[ "$_LINUX_HAS_GPU" = false ] && [ -n "${_setup_gfx:-}" ]' in source
assert '_HELPER_RELEASE_REPO="unslothai/llama.cpp"' in source
assert '_HELPER_RELEASE_REPO="ggml-org/llama.cpp"' not in source
def test_setup_ps1_routes_inferred_gfx_to_fork(self):
# On Windows, a resolved $script:ROCmGfxArch counts as a fork install
# even when $HasROCm is false (Adrenalin-only, no HIP runtime).
def test_setup_ps1_routes_unconditionally_to_fork(self):
# Same on Windows: the fork now ships the windows-cpu / windows-arm64
# bundles, so $HelperReleaseRepo is an unconditional fork assignment.
source = _SETUP_PS1_PATH.read_text(encoding = "utf-8")
assert "$HasNvidiaSmi -or $HasROCm -or $script:ROCmGfxArch" in source
assert '$HelperReleaseRepo = "unslothai/llama.cpp"' in source
assert "$HelperReleaseRepo = if (" not in source
# The assertions above pin the guard *text*; the tests below *execute* the
# real routing block and assert the resolved repo, so a refactor that keeps
# the literal but breaks the inferred-gfx -> fork decision is still caught.
# The text pins above guard the literal. The tests below *execute* the real
# routing line from setup.sh / setup.ps1 and assert the resolved release repo,
# so a refactor that reintroduces a conditional (or a ggml-org branch) is still
# caught. Inputs are varied -- CPU-only, inferred/forwarded gfx, usable NVIDIA --
# to prove no host slips back onto ggml-org. No GPU, no tooling, no network.
@staticmethod
def _resolve_setup_sh_repo(
@ -3187,15 +3191,18 @@ class TestRocmGfxForwarding:
setup_gfx,
rocm_gfx_arch_env = "",
):
"""Run setup.sh's routing block under bash with PATH emptied (no ROCm tooling) and return _HELPER_RELEASE_REPO."""
"""Run setup.sh's release-repo routing block under bash and return the
resolved _HELPER_RELEASE_REPO. PATH is emptied so any stray tooling probe
misses; routing is unconditional, so the GPU inputs only prove no branch
reroutes a host to ggml-org."""
import shutil
bash = shutil.which("bash")
if bash is None:
pytest.skip("bash not available")
source = _SETUP_SH_PATH.read_text(encoding = "utf-8")
start = source.index("\n_LINUX_HAS_GPU=false\n") + 1
end = source.index("\nunset _GPU_TOOL", start) + len("\nunset _GPU_TOOL")
start = source.index('\n_HELPER_RELEASE_REPO="unslothai/llama.cpp"\n') + 1
end = source.index("\n_LLAMA_PR=", start)
block = source[start:end]
assert "_HELPER_RELEASE_REPO" in block, "setup.sh routing anchors not found"
env = {
@ -3217,25 +3224,31 @@ class TestRocmGfxForwarding:
assert result.returncode == 0, result.stderr
return result.stdout.strip()
def test_setup_sh_inferred_gfx_resolves_to_fork(self):
# Only a name-inferred gfx arch -> route to the fork's per-gfx prebuilt
# (not ggml-org). x64 and arm64 share the fork branch.
assert self._resolve_setup_sh_repo("x86_64", False, "gfx1100") == "unslothai/llama.cpp"
assert self._resolve_setup_sh_repo("aarch64", False, "gfx1100") == "unslothai/llama.cpp"
def test_setup_sh_env_forwarded_gfx_resolves_to_fork(self):
# No probe fired but UNSLOTH_ROCM_GFX_ARCH is set: adopt the env arch
# and route to the fork, same as name-inference.
repo = self._resolve_setup_sh_repo("x86_64", False, "", rocm_gfx_arch_env = "gfx1100")
assert repo == "unslothai/llama.cpp"
def test_setup_sh_cpu_host_still_resolves_to_ggml(self):
# A real CPU host (no GPU, no inferred gfx, no env override) must keep routing to ggml-org.
assert self._resolve_setup_sh_repo("x86_64", False, "") == "ggml-org/llama.cpp"
@pytest.mark.parametrize(
"machine, nvidia_usable, setup_gfx, env_gfx",
[
("x86_64", False, "", ""), # plain CPU host (used to take ggml-org)
("aarch64", False, "", ""), # plain CPU arm64 host (used to take ggml-org)
("x86_64", False, "gfx1100", ""), # name-inferred gfx
("x86_64", False, "", "gfx1100"), # env-forwarded gfx
("x86_64", True, "", ""), # usable NVIDIA
],
)
def test_setup_sh_routing_block_always_resolves_to_fork(
self, machine, nvidia_usable, setup_gfx, env_gfx
):
assert (
self._resolve_setup_sh_repo(
machine, nvidia_usable, setup_gfx, rocm_gfx_arch_env = env_gfx
)
== "unslothai/llama.cpp"
)
@staticmethod
def _resolve_setup_ps1_repo(has_nvidia, has_rocm, gfx_arch):
"""Run setup.ps1's $HelperReleaseRepo selection under pwsh and return the resolved repo."""
def _resolve_setup_ps1_repo():
"""Run setup.ps1's $HelperReleaseRepo assignment under pwsh and return the
resolved repo. The assignment is unconditional now, so there are no host
inputs to vary."""
import shutil
pwsh = shutil.which("pwsh")
@ -3243,21 +3256,11 @@ class TestRocmGfxForwarding:
pytest.skip("pwsh not available")
source = _SETUP_PS1_PATH.read_text(encoding = "utf-8")
line = next(
(
ln
for ln in source.splitlines()
if ln.strip().startswith("$HelperReleaseRepo = if (")
),
(ln for ln in source.splitlines() if ln.strip().startswith("$HelperReleaseRepo =")),
None,
)
assert line is not None, "$HelperReleaseRepo selection not found in setup.ps1"
harness = (
f"$HasNvidiaSmi = ${'true' if has_nvidia else 'false'}\n"
f"$HasROCm = ${'true' if has_rocm else 'false'}\n"
f"$script:ROCmGfxArch = '{gfx_arch}'\n"
f"{line}\n"
"Write-Output $HelperReleaseRepo"
)
harness = f"{line}\nWrite-Output $HelperReleaseRepo"
result = subprocess.run(
[pwsh, "-NoProfile", "-Command", harness],
capture_output = True,
@ -3267,13 +3270,10 @@ class TestRocmGfxForwarding:
assert result.returncode == 0, result.stderr
return result.stdout.strip()
def test_setup_ps1_inferred_gfx_resolves_to_fork(self):
# Adrenalin-only host: $HasROCm false but gfx inferred -> fork's windows-rocm bundle.
assert self._resolve_setup_ps1_repo(False, False, "gfx1100") == "unslothai/llama.cpp"
def test_setup_ps1_cpu_host_still_resolves_to_ggml(self):
# No NVIDIA, no ROCm, no inferred gfx -> CPU host stays on ggml-org.
assert self._resolve_setup_ps1_repo(False, False, "") == "ggml-org/llama.cpp"
def test_setup_ps1_routing_resolves_to_fork(self):
# Windows routing is unconditional now: CPU-only Windows (x64 and arm64)
# uses the fork's windows-cpu / windows-arm64 bundles, not ggml-org.
assert self._resolve_setup_ps1_repo() == "unslothai/llama.cpp"
# TEST: _pick_rocm_gfx_target -- visible-device selection from rocminfo output.

View file

@ -1720,8 +1720,10 @@ class TestResolveInstallAttempts:
assert approved.release_tag == "llama-prebuilt-latest"
def test_linux_cpu_fork_without_bundle_raises_no_upstream_fallback(self, monkeypatch):
# CPU-only Linux on the fork must not fall back to the ggml-org CPU asset; with
# no fork CPU bundle the resolver raises rather than reaching upstream.
# A CPU-only Linux host on the fork never falls back to the ggml-org CPU
# asset. CPU-only Linux now routes to the fork, but if a release manifest
# happens to ship no CPU bundle the resolver raises rather than quietly
# reaching for an upstream asset.
host = make_host(
has_usable_nvidia = False,
has_physical_nvidia = False,
@ -1846,6 +1848,151 @@ class TestResolveInstallAttempts:
assert attempts[0].name == asset_name
assert attempts[0].source_label == "published"
@pytest.mark.parametrize(
"system, machine, asset_name, install_kind, bundle_profile",
[
# CPU-only Linux x64 -> fork linux-cpu (was ggml-org ubuntu-x64).
("Linux", "x86_64", "app-b9625-linux-x64-cpu.tar.gz", "linux-cpu", "linux-cpu-x64"),
# CPU-only Linux arm64 -> fork linux-arm64 (was ggml-org ubuntu-arm64).
(
"Linux",
"aarch64",
"app-b9625-linux-arm64-cpu.tar.gz",
"linux-arm64",
"linux-cpu-arm64",
),
# CPU-only Windows arm64 -> fork windows-arm64 (was ggml-org win-cpu-arm64).
(
"Windows",
"arm64",
"app-b9625-windows-arm64-cpu.zip",
"windows-arm64",
"windows-cpu-arm64",
),
],
)
def test_cpu_host_prefers_published_fork_asset(
self, monkeypatch, system, machine, asset_name, install_kind, bundle_profile
):
# CPU-only hosts now select the fork's CPU bundle from the manifest and
# must never query ggml-org upstream assets. Windows x64 CPU is covered
# separately by test_windows_cpu_prefers_published_asset.
host = make_host(
system = system,
machine = machine,
has_usable_nvidia = False,
has_physical_nvidia = False,
nvidia_smi = None,
)
release = make_release(
[
make_artifact(
asset_name,
install_kind = install_kind,
runtime_line = None,
coverage_class = None,
supported_sms = [],
min_sm = None,
max_sm = None,
bundle_profile = bundle_profile,
rank = 1000,
)
],
release_tag = "llama-prebuilt-latest",
upstream_tag = "b9625",
assets = {asset_name: f"https://published.example/{asset_name}"},
)
checksums = make_checksums_with_source(
[asset_name],
release_tag = release.release_tag,
upstream_tag = "b9625",
)
monkeypatch.setattr(
INSTALL_LLAMA_PREBUILT,
"iter_resolved_published_releases",
lambda requested_tag, published_repo, published_release_tag = "": iter(
[
INSTALL_LLAMA_PREBUILT.ResolvedPublishedRelease(
bundle = release,
checksums = checksums,
)
]
),
)
monkeypatch.setattr(
INSTALL_LLAMA_PREBUILT,
"github_release_assets",
lambda repo, tag: (_ for _ in ()).throw(
AssertionError("fork CPU host must not query upstream assets")
),
)
_requested_tag, resolved_tag, attempts, _approved = resolve_install_attempts(
"latest",
host,
"unslothai/llama.cpp",
"",
)
assert resolved_tag == "b9625"
assert attempts[0].name == asset_name
assert attempts[0].install_kind == install_kind
assert attempts[0].source_label == "published"
def test_cpu_only_unsupported_arch_source_builds(self, monkeypatch):
# A CPU-only Linux host that is neither x86_64 nor arm64 (ppc64le,
# riscv64, s390x) has no compatible CPU bundle. It must source-build, not
# receive the x86_64 linux-cpu binary (the Linux preflight checks libs,
# not ELF arch, so a wrong-arch binary would slip through).
host = make_host(
machine = "ppc64le",
has_usable_nvidia = False,
has_physical_nvidia = False,
nvidia_smi = None,
)
assert not host.is_x86_64 and not host.is_arm64
x64_asset = "app-b9625-linux-x64-cpu.tar.gz"
release = make_release(
[
make_artifact(
x64_asset,
install_kind = "linux-cpu",
runtime_line = None,
coverage_class = None,
supported_sms = [],
min_sm = None,
max_sm = None,
bundle_profile = "linux-cpu-x64",
rank = 1000,
)
],
release_tag = "llama-prebuilt-latest",
upstream_tag = "b9625",
assets = {x64_asset: f"https://published.example/{x64_asset}"},
)
checksums = make_checksums_with_source(
[x64_asset],
release_tag = release.release_tag,
upstream_tag = "b9625",
)
monkeypatch.setattr(
INSTALL_LLAMA_PREBUILT,
"iter_resolved_published_releases",
lambda requested_tag, published_repo, published_release_tag = "": iter(
[
INSTALL_LLAMA_PREBUILT.ResolvedPublishedRelease(
bundle = release,
checksums = checksums,
)
]
),
)
with pytest.raises(PrebuiltFallback, match = "no compatible Linux prebuilt asset was found"):
resolve_install_attempts("latest", host, "unslothai/llama.cpp", "")
def test_macos_prefers_published_asset(self, monkeypatch):
host = make_host(
system = "Darwin",
@ -3447,8 +3594,9 @@ class TestLinuxArm64ForkFallsBackToSource:
assert plans == ["plan"]
def test_arm64_cpu_on_ggml_org_is_not_blocked(self, monkeypatch):
# CPU-only arm64 routes to ggml-org, so the guard must not fire; it reaches the
# iterator (empty here -> generic message).
# ggml-org is reachable only via an explicit --published-repo override now,
# but the guard must still not fire on arm64 there; it reaches the iterator
# (empty here -> generic message).
monkeypatch.setattr(
INSTALL_LLAMA_PREBUILT,
"iter_release_payloads_by_time",
@ -3474,7 +3622,7 @@ class TestLinuxArm64ForkFallsBackToSource:
class TestCpuFallback:
"""--cpu-fallback drops GPU attributes so the host's OS/arch CPU prebuilt is selected, letting an arm64 GPU host install ggml-org's arm64 CPU build when its source build produced no binary."""
"""--cpu-fallback drops GPU attributes so the host's OS/arch CPU prebuilt is selected, letting an arm64 GPU host install the fork's arm64 CPU bundle when its source build produced no binary."""
_SETUP_SH = PACKAGE_ROOT / "studio" / "setup.sh"
@ -3553,10 +3701,15 @@ class TestCpuFallback:
def test_setup_sh_has_arm64_cpu_prebuilt_fallback(self):
source = self._SETUP_SH.read_text(encoding = "utf-8")
assert "--cpu-fallback" in source
# Fallback targets ggml-org (only repo with an arm64 Linux build), gated on a
# degraded arm64 source build.
assert "ggml-org/llama.cpp" in source
# The arm64 GPU last-resort CPU fallback now pulls the fork's arm64 CPU
# bundle (app-<tag>-linux-arm64-cpu.tar.gz), not ggml-org's, and is gated
# on a degraded source build for arm64.
start = source.index("_ARM64_CPU_CMD=(")
end = source.index(")", start)
block = source[start:end]
assert "--cpu-fallback" in block
assert '--published-repo "unslothai/llama.cpp"' in block
assert '--published-repo "ggml-org/llama.cpp"' not in block
assert "_LLAMA_CPP_DEGRADED" in source