diff --git a/.github/workflows/studio-windows-update-smoke.yml b/.github/workflows/studio-windows-update-smoke.yml index 888b3d70a3..5b92f1a3e0 100644 --- a/.github/workflows/studio-windows-update-smoke.yml +++ b/.github/workflows/studio-windows-update-smoke.yml @@ -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--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 diff --git a/studio/backend/tests/test_install_resolve_prebuilt.py b/studio/backend/tests/test_install_resolve_prebuilt.py index e9941d9e62..b825172a63 100644 --- a/studio/backend/tests/test_install_resolve_prebuilt.py +++ b/studio/backend/tests/test_install_resolve_prebuilt.py @@ -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. diff --git a/studio/backend/tests/test_llama_cpp_freshness.py b/studio/backend/tests/test_llama_cpp_freshness.py index 2a2e113585..08e1334ac9 100644 --- a/studio/backend/tests/test_llama_cpp_freshness.py +++ b/studio/backend/tests/test_llama_cpp_freshness.py @@ -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") diff --git a/studio/backend/tests/test_llama_cpp_update.py b/studio/backend/tests/test_llama_cpp_update.py index 4ceffbf75b..5138e90471 100644 --- a/studio/backend/tests/test_llama_cpp_update.py +++ b/studio/backend/tests/test_llama_cpp_update.py @@ -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, diff --git a/studio/install_llama_prebuilt.py b/studio/install_llama_prebuilt.py index e40cb3083e..6c75e6c394 100644 --- a/studio/install_llama_prebuilt.py +++ b/studio/install_llama_prebuilt.py @@ -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 "" diff --git a/studio/setup.ps1 b/studio/setup.ps1 index bb1e88cc4e..07dcb17335 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -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 diff --git a/studio/setup.sh b/studio/setup.sh index 6a74cd2296..d244e3cdcf 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -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--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, diff --git a/tests/studio/install/test_llama_pr_force_and_source.py b/tests/studio/install/test_llama_pr_force_and_source.py index 8f40c9d720..4ff8c349c3 100644 --- a/tests/studio/install/test_llama_pr_force_and_source.py +++ b/tests/studio/install/test_llama_pr_force_and_source.py @@ -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 diff --git a/tests/studio/install/test_pr4562_bugfixes.py b/tests/studio/install/test_pr4562_bugfixes.py index a8cec0cb85..0d2b092924 100644 --- a/tests/studio/install/test_pr4562_bugfixes.py +++ b/tests/studio/install/test_pr4562_bugfixes.py @@ -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.""" diff --git a/tests/studio/install/test_rocm_support.py b/tests/studio/install/test_rocm_support.py index bfc8132683..5cabf41f57 100644 --- a/tests/studio/install/test_rocm_support.py +++ b/tests/studio/install/test_rocm_support.py @@ -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. diff --git a/tests/studio/install/test_selection_logic.py b/tests/studio/install/test_selection_logic.py index 464aa9a578..92f08c646b 100644 --- a/tests/studio/install/test_selection_logic.py +++ b/tests/studio/install/test_selection_logic.py @@ -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--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