From 5300c047b65edb8dfb2712541c88a1540fb879da Mon Sep 17 00:00:00 2001 From: oobabooga Date: Fri, 12 Jun 2026 11:53:26 -0300 Subject: [PATCH] Installer: drop the lemonade ROCm fallback now the fork ships identical per-gfx prebuilts (#6225) --------- Co-authored-by: Daniel Han --- install.ps1 | 12 +- .../test_lemonade_llamacpp_rocm_bins_mock.py | 523 ------------------ studio/backend/tests/test_llama_cpp_update.py | 4 +- studio/backend/utils/llama_cpp_update.py | 5 +- studio/install_llama_prebuilt.py | 373 ++----------- studio/install_python_stack.py | 2 +- studio/setup.ps1 | 25 +- studio/setup.sh | 21 +- .../test_install_llama_prebuilt_logic.py | 2 +- .../install/test_llama_pr_force_and_source.py | 2 +- tests/studio/install/test_rocm_support.py | 172 +++++- tests/studio/install/test_selection_logic.py | 23 + 12 files changed, 284 insertions(+), 880 deletions(-) delete mode 100644 studio/backend/tests/test_lemonade_llamacpp_rocm_bins_mock.py diff --git a/install.ps1 b/install.ps1 index 189857d8b4..fa0d160a33 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1574,7 +1574,7 @@ shell.Run cmd, 0, False # popping a UAC/DiskPart prompt RunAsInvoker can't suppress (manifest is # asInvoker). So only probe when a HIP SDK is present (hipinfo found -> # un-elevated) or the user opts in; else fall through to WMI name inference - # (enough to pick ROCm wheels + lemonade llama.cpp). + # (enough to pick ROCm wheels + the ROCm llama.cpp prebuilt). # An explicit opt-out (UNSLOTH_ENABLE_AMD_SMI=0/false/no/off) wins over the # HIP-SDK heuristic: a HIP SDK binary with a broken runtime can still pop the # prompt, so $HipSdkInstalled must NOT silently re-enable it. @@ -1625,7 +1625,7 @@ shell.Run cmd, 0, False # ── Arch resolution: env-var override → name inference ────────────── # Runs even when the hipinfo/amd-smi probe could NOT confirm a runtime # ($HasROCm false): the gfx arch inferred from the WMI GPU name lets the - # studio setup forward --rocm-gfx and pull a GPU-accelerated (lemonade) + # studio setup forward --rocm-gfx and pull a GPU-accelerated ROCm # llama.cpp, which bundles its own ROCm runtime. PyTorch's ROCm wheels # still require a confirmed HIP SDK -- they stay gated on $HasROCm below. if (-not $ROCmGfxArch) { @@ -1636,7 +1636,7 @@ shell.Run cmd, 0, False substep "gfx arch from UNSLOTH_ROCM_GFX_ARCH env override: $ROCmGfxArch" "Cyan" } # 2. Best-effort name → arch lookup from marketing name (amd-smi / WMI). - # Targets only arches the lemonade-sdk ROCm prebuilts cover + # Targets only arches the ROCm prebuilts cover # (gfx120X/110X/1151/1150/103X); unknown names fall back to CPU. elseif ($ROCmGpuLabel) { $nameArchTable = @( @@ -1647,9 +1647,9 @@ shell.Run cmd, 0, False @{ P = "RX 7900|RX 7800|RX 7700(?!S)|PRO W7900|PRO W7800|PRO W7700"; A = "gfx1100" } # RDNA 3 desktop/workstation (Navi 31) @{ P = "RX 7600|RX 7700S|RX 7650|PRO W7600|PRO W7500|PRO V710"; A = "gfx1102" } # RDNA 3 (Navi 33) @{ P = "780M|760M|740M|Phoenix|Hawk Point|Z1 Extreme|Z2 Extreme"; A = "gfx1103" } # RDNA 3 iGPU (Phoenix / Hawk Point) - @{ P = "RX 6900|RX 6800|RX 6750|RX 6700|PRO W6800|PRO W6900"; A = "gfx1030" } # RDNA 2 (Navi 21) -- lemonade gfx103X - @{ P = "RX 6650|RX 6600|PRO W6600|PRO W6650"; A = "gfx1032" } # RDNA 2 (Navi 23) -- lemonade gfx103X - @{ P = "RX 6500|RX 6400|RX 6300|PRO W6400|PRO W6500"; A = "gfx1034" } # RDNA 2 (Navi 24) -- lemonade gfx103X + @{ P = "RX 6900|RX 6800|RX 6750|RX 6700|PRO W6800|PRO W6900"; A = "gfx1030" } # RDNA 2 (Navi 21) -- gfx103X family + @{ P = "RX 6650|RX 6600|PRO W6600|PRO W6650"; A = "gfx1032" } # RDNA 2 (Navi 23) -- gfx103X family + @{ P = "RX 6500|RX 6400|RX 6300|PRO W6400|PRO W6500"; A = "gfx1034" } # RDNA 2 (Navi 24) -- gfx103X family ) foreach ($row in $nameArchTable) { if ($ROCmGpuLabel -match $row.P) { diff --git a/studio/backend/tests/test_lemonade_llamacpp_rocm_bins_mock.py b/studio/backend/tests/test_lemonade_llamacpp_rocm_bins_mock.py deleted file mode 100644 index 7c6c514b8f..0000000000 --- a/studio/backend/tests/test_lemonade_llamacpp_rocm_bins_mock.py +++ /dev/null @@ -1,523 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 - -"""Validates that the installer correctly resolves lemonade ROCm prebuilt assets. - -Uses a faked HostInfo so no AMD GPU is needed. Network calls to the lemonade -GitHub API are stubbed out so the suite runs without internet access and is -not subject to rate limits. -""" - -from __future__ import annotations - -import importlib -import sys -from pathlib import Path -from unittest.mock import patch - -import pytest - -_studio = Path(__file__).resolve().parent.parent.parent -if str(_studio) not in sys.path: - sys.path.insert(0, str(_studio)) - -_mod = importlib.import_module("install_llama_prebuilt") -HostInfo = _mod.HostInfo -resolve_lemonade_rocm_choice = getattr(_mod, "resolve_lemonade_rocm_choice", None) -_LEMONADE_GFX_FAMILIES = getattr(_mod, "_LEMONADE_GFX_FAMILIES", None) - -if resolve_lemonade_rocm_choice is None or _LEMONADE_GFX_FAMILIES is None: - pytest.skip("PR symbols not present - check branch", allow_module_level = True) - - -@pytest.fixture(autouse = True) -def _clear_lemonade_release_cache(): - """Prevent cross-test pollution of the lemonade release lru_cache and - selection-log dedup set when tests vary the fetch_json mock return value.""" - _cache = getattr(_mod, "_fetch_lemonade_release_cached", None) - _logged: set | None = getattr(_mod, "_lemonade_selection_logged", None) - if _cache is not None and hasattr(_cache, "cache_clear"): - _cache.cache_clear() - if _logged is not None: - _logged.clear() - yield - if _cache is not None and hasattr(_cache, "cache_clear"): - _cache.cache_clear() - if _logged is not None: - _logged.clear() - - -_STUB_TAG = "b1262" -_STUB_OS_PREFIXES = ("ubuntu", "windows") -_STUB_FAMILIES = ("gfx1151", "gfx1150", "gfx120X", "gfx110X", "gfx103X") - - -def _stub_lemonade_release() -> dict: - """Minimal lemonade release payload covering all supported GPU/OS combinations.""" - assets = [ - { - "name": f"llama-{_STUB_TAG}-{prefix}-rocm-{family}-x64.zip", - "browser_download_url": ( - f"https://github.com/lemonade-sdk/llamacpp-rocm/releases/download/" - f"{_STUB_TAG}/llama-{_STUB_TAG}-{prefix}-rocm-{family}-x64.zip" - ), - } - for prefix in _STUB_OS_PREFIXES - for family in _STUB_FAMILIES - ] - return {"tag_name": _STUB_TAG, "assets": assets} - - -def _make_rocm_host(gfx_target: str, *, windows: bool = False) -> HostInfo: - return HostInfo( - system = "Windows" if windows else "Linux", - machine = "amd64" if windows else "x86_64", - is_windows = windows, - is_linux = not windows, - is_macos = False, - is_x86_64 = True, - 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 = True, - rocm_gfx_target = gfx_target, - ) - - -def _lookup_family(gfx: str) -> str | None: - for prefix, family in _LEMONADE_GFX_FAMILIES: - if gfx.startswith(prefix): - return family - return None - - -# --------------------------------------------------------------------------- -# GPU family mapping -# --------------------------------------------------------------------------- - - -@pytest.mark.parametrize( - "gfx,expected_family", - [ - ("gfx1151", "gfx1151"), - ("gfx1150", "gfx1150"), - ("gfx1201", "gfx120X"), - ("gfx1200", "gfx120X"), - ("gfx1100", "gfx110X"), - ("gfx1030", "gfx103X"), - ], -) -def test_gpu_family_mapping(gfx, expected_family): - assert _lookup_family(gfx) == expected_family - - -def test_unknown_gpu_not_in_families(): - assert _lookup_family("gfx999") is None - - -# --------------------------------------------------------------------------- -# Asset resolution - hits real lemonade GitHub API -# --------------------------------------------------------------------------- - - -@pytest.mark.parametrize( - "gfx,os_prefix,windows", - [ - ("gfx1151", "ubuntu", False), - ("gfx1150", "ubuntu", False), - ("gfx1201", "ubuntu", False), - ("gfx1100", "ubuntu", False), - ("gfx1030", "ubuntu", False), - ("gfx1151", "windows", True), - ("gfx1100", "windows", True), - ], -) -def test_asset_resolves_for_known_gpu(gfx, os_prefix, windows): - host = _make_rocm_host(gfx, windows = windows) - with patch.object(_mod, "fetch_json", return_value = _stub_lemonade_release()): - result = resolve_lemonade_rocm_choice(host, os_prefix, "default", llama_tag = "latest") - assert result is not None, f"Installer will NOT fetch lemonade binary for {gfx} ({os_prefix})" - assert _lookup_family(gfx) in result.name - assert result.url.startswith("https://github.com/lemonade-sdk/llamacpp-rocm") - - -def test_unknown_gpu_falls_through_to_upstream(): - host = _make_rocm_host("gfx999") - result = resolve_lemonade_rocm_choice(host, "ubuntu", "default", llama_tag = "latest") - assert result is None - - -# --------------------------------------------------------------------------- -# The Linux attempt builder must plan a lemonade ROCm attempt for AMD-only hosts. -# This is the path setup.sh actually invokes (fork hosts now select from the -# manifest), so the lemonade integration is useless if it isn't wired in here. -# --------------------------------------------------------------------------- - -_linux_published_attempts = getattr(_mod, "_linux_published_attempts", None) -direct_upstream_release_plan = getattr(_mod, "direct_upstream_release_plan", None) - -PublishedLlamaArtifact = _mod.PublishedLlamaArtifact -PublishedReleaseBundle = _mod.PublishedReleaseBundle - - -def _rocm_bundle(gfx_family: str, mapped_targets: list[str]) -> "PublishedReleaseBundle": - """A fork manifest bundle exposing a per-gfx linux-rocm artifact, so - published_rocm_choice_for_host can match the host before the lemonade - fallback is appended.""" - asset_name = f"app-b9457-linux-x64-rocm-{gfx_family}.tar.gz" - artifact = PublishedLlamaArtifact( - asset_name = asset_name, - install_kind = "linux-rocm", - runtime_line = None, - coverage_class = None, - supported_sms = [], - min_sm = None, - max_sm = None, - bundle_profile = None, - rank = 1000, - gfx_target = gfx_family, - mapped_targets = mapped_targets, - ) - return PublishedReleaseBundle( - repo = "unslothai/llama.cpp", - release_tag = "v1.0", - upstream_tag = "b9457", - assets = {asset_name: f"https://example.invalid/{asset_name}"}, - artifacts = [artifact], - ) - - -@pytest.mark.skipif( - _linux_published_attempts is None, - reason = "Linux attempt builder not present on this branch", -) -def test_linux_attempts_include_fork_rocm_and_lemonade_for_rocm_host(): - host = _make_rocm_host("gfx1151") - bundle = _rocm_bundle("gfx1151", ["gfx1151"]) - with patch.object(_mod, "fetch_json", return_value = _stub_lemonade_release()): - attempts = _linux_published_attempts(host, bundle, "latest") - kinds = [a.install_kind for a in attempts] - assert "linux-rocm" in kinds, f"builder did not include any linux-rocm attempt; got {kinds}" - sources = {a.source_label for a in attempts if a.install_kind == "linux-rocm"} - # The fork's own per-gfx bundle is preferred, with the lemonade prebuilt as - # the fallback -- both must be present for a covered ROCm host. - assert "published" in sources, f"fork ROCm bundle missing; got {sources}" - assert "lemonade" in sources, f"lemonade ROCm fallback missing; got {sources}" - lemonade_attempt = next(a for a in attempts if a.source_label == "lemonade") - assert "gfx1151" in lemonade_attempt.name - - -@pytest.mark.skipif( - direct_upstream_release_plan is None, - reason = "direct release planners not present on this branch", -) -def test_direct_upstream_plan_includes_lemonade_for_windows_hip_host(): - host = _make_rocm_host("gfx1151", windows = True) - release = { - "tag_name": "b9022", - "name": "b9022", - "assets": [], - } - with patch.object(_mod, "fetch_json", return_value = _stub_lemonade_release()): - plan = direct_upstream_release_plan(release, host, "ggml-org/llama.cpp", "latest") - assert plan is not None, "Windows ROCm host should plan a lemonade HIP attempt" - kinds = [a.install_kind for a in plan.attempts] - assert "windows-hip" in kinds, f"planner did not include a lemonade HIP attempt; got {kinds}" - - -@pytest.mark.skipif( - direct_upstream_release_plan is None, - reason = "direct release planners not present on this branch", -) -def test_windows_hip_falls_back_to_upstream_when_lemonade_unavailable(): - """If lemonade returns None (e.g. gfx999 or transient API failure), the planner - must still include the upstream HIP asset rather than silently downgrading to CPU.""" - host = _make_rocm_host("gfx999", windows = True) - hip_asset = "llama-b9022-bin-win-hip-radeon-x64.zip" - release = { - "tag_name": "b9022", - "name": "b9022", - "assets": [ - { - "name": hip_asset, - "browser_download_url": f"https://example.invalid/{hip_asset}", - }, - ], - } - plan = direct_upstream_release_plan(release, host, "ggml-org/llama.cpp", "latest") - assert plan is not None - kinds = [a.install_kind for a in plan.attempts] - assert "windows-hip" in kinds, f"upstream HIP asset not included as fallback; got {kinds}" - hip_attempt = next(a for a in plan.attempts if a.install_kind == "windows-hip") - assert hip_attempt.source_label == "upstream" - - -# ── Follow-up: pinned-tag URL helper, URL trust pinning, opt-out env, autouse cache clear ── - - -def test_lemonade_release_api_url_pinned_tag(): - """A pinned llama_tag must produce the /releases/tags/ URL.""" - assert _mod._lemonade_release_api_for("b1262").endswith("/releases/tags/b1262") - assert _mod._lemonade_release_api_for("latest").endswith("/releases/latest") - assert _mod._lemonade_release_api_for("").endswith("/releases/latest") - - -def test_lemonade_release_api_url_encodes_tag(): - """Unexpected slashes / hashes in the tag must be URL-encoded so the URL - cannot be reshaped (defence in depth -- tags should already be sanitised - upstream).""" - url = _mod._lemonade_release_api_for("b1260/../latest") - assert "/releases/tags/b1260%2F..%2Flatest" in url - assert "//latest" not in url.split("/releases/tags/", 1)[1] - - -def test_lemonade_resolver_skipped_by_opt_out_env(monkeypatch): - """UNSLOTH_DISABLE_LEMONADE_ROCM=1 must short-circuit the resolver.""" - monkeypatch.setenv("UNSLOTH_DISABLE_LEMONADE_ROCM", "1") - host = _make_rocm_host("gfx1151") - res = resolve_lemonade_rocm_choice(host, "ubuntu", "linux-rocm", llama_tag = "latest") - assert res is None - - -def test_lemonade_resolver_rejects_non_github_url(monkeypatch): - """If the GitHub API response somehow contained an off-host download URL, - the resolver must refuse to use it (lemonade assets are not in the - approved-hash manifest).""" - bad_release = { - "tag_name": _STUB_TAG, - "assets": [ - { - "name": f"llama-{_STUB_TAG}-ubuntu-rocm-gfx1151-x64.zip", - "browser_download_url": "https://attacker.invalid/llama.zip", - }, - ], - } - host = _make_rocm_host("gfx1151") - with patch.object(_mod, "fetch_json", return_value = bad_release): - res = resolve_lemonade_rocm_choice(host, "ubuntu", "linux-rocm", llama_tag = "latest") - assert res is None - - -def test_lemonade_resolver_rejects_http_scheme(): - assert not _mod._is_trusted_github_release_url( - "http://github.com/lemonade-sdk/llamacpp-rocm/releases/download/x/y.zip", - "lemonade-sdk/llamacpp-rocm", - ) - - -def test_lemonade_resolver_accepts_github_cdn(): - # Real GitHub release CDN URLs carry the /github-production-release-asset- prefix. - assert _mod._is_trusted_github_release_url( - "https://objects.githubusercontent.com/github-production-release-asset-abc123/456/789?token=x", - "lemonade-sdk/llamacpp-rocm", - ) - - -def test_lemonade_resolver_rejects_arbitrary_cdn_path(): - # A CDN URL without the release-asset path prefix must be rejected. - assert not _mod._is_trusted_github_release_url( - "https://objects.githubusercontent.com/abc/def", - "lemonade-sdk/llamacpp-rocm", - ) - - -def test_lemonade_resolver_accepts_release_path(): - url = "https://github.com/lemonade-sdk/llamacpp-rocm/releases/download/b1262/llama-b1262-ubuntu-rocm-gfx1151-x64.zip" - assert _mod._is_trusted_github_release_url(url, "lemonade-sdk/llamacpp-rocm") - - -def test_lemonade_resolver_rejects_wrong_repo(): - """A github.com release URL for a different repo must be rejected.""" - assert not _mod._is_trusted_github_release_url( - "https://github.com/attacker/llamacpp-rocm/releases/download/x/y.zip", - "lemonade-sdk/llamacpp-rocm", - ) - - -def test_lemonade_resolver_rejects_empty_browser_download_url(): - """An asset entry with an empty browser_download_url must fall through.""" - release = { - "tag_name": _STUB_TAG, - "assets": [ - { - "name": f"llama-{_STUB_TAG}-ubuntu-rocm-gfx1151-x64.zip", - "browser_download_url": "", - }, - ], - } - host = _make_rocm_host("gfx1151") - with patch.object(_mod, "fetch_json", return_value = release): - res = resolve_lemonade_rocm_choice(host, "ubuntu", "linux-rocm", llama_tag = "latest") - assert res is None - - -def test_lemonade_runtime_patterns_include_hip_runtime(): - """linux-rocm overlay must use a broad lib glob to catch all bundled .so files. - - Lemonade ZIPs carry transitive deps (libamd_comgr, libLLVM, libclang-cpp, - ...) whose names change across ROCm releases. A broad ``lib*.so*`` glob - avoids having to enumerate every transitive dependency by name. - """ - from install_llama_prebuilt import runtime_patterns_for_choice, AssetChoice - - choice = AssetChoice( - repo = "lemonade-sdk/llamacpp-rocm", - tag = "b1262", - name = "llama-b1262-ubuntu-rocm-gfx1151-x64.zip", - url = "https://github.com/lemonade-sdk/llamacpp-rocm/releases/download/b1262/x.zip", - source_label = "lemonade", - install_kind = "linux-rocm", - ) - pats = runtime_patterns_for_choice(choice) - # The broad glob must be present so every .so in the lemonade bundle - # (including transitive deps added in future ROCm releases) gets overlaid. - assert "lib*.so*" in pats, f"'lib*.so*' missing from linux-rocm patterns: {pats}" - - -_pick_rocm_gfx_target = getattr(_mod, "_pick_rocm_gfx_target", None) - - -@pytest.mark.skipif( - _pick_rocm_gfx_target is None, - reason = "_pick_rocm_gfx_target not present on this branch", -) -def test_pick_rocm_gfx_target_honors_cuda_visible_devices(monkeypatch): - """AMD HIP honours CUDA_VISIBLE_DEVICES identically to HIP_VISIBLE_DEVICES; - on a gfx1151 + gfx1100 mixed host, CUDA_VISIBLE_DEVICES=1 must select gfx1100.""" - # Two GPUs; rocminfo reports each token twice (as in the real tool output). - probe_out = "gfx1151\ngfx1151\ngfx1100\ngfx1100" - monkeypatch.delenv("HIP_VISIBLE_DEVICES", raising = False) - monkeypatch.delenv("ROCR_VISIBLE_DEVICES", raising = False) - monkeypatch.setenv("CUDA_VISIBLE_DEVICES", "1") - assert _pick_rocm_gfx_target(probe_out) == "gfx1100" - - -@pytest.mark.skipif( - _pick_rocm_gfx_target is None, - reason = "_pick_rocm_gfx_target not present on this branch", -) -def test_pick_rocm_gfx_target_cuda_visible_devices_minus_one_returns_none(monkeypatch): - """CUDA_VISIBLE_DEVICES=-1 means no GPU visible; resolver must return None.""" - probe_out = "gfx1151\ngfx1100" - monkeypatch.delenv("HIP_VISIBLE_DEVICES", raising = False) - monkeypatch.delenv("ROCR_VISIBLE_DEVICES", raising = False) - monkeypatch.setenv("CUDA_VISIBLE_DEVICES", "-1") - assert _pick_rocm_gfx_target(probe_out) is None - - -@pytest.mark.skipif( - _pick_rocm_gfx_target is None, - reason = "_pick_rocm_gfx_target not present on this branch", -) -def test_pick_rocm_gfx_target_same_arch_multi_gpu(monkeypatch): - """Regression: [gfx1100, gfx1100, gfx1151] with HIP_VISIBLE_DEVICES=2 must - return gfx1151, not fall back to GPU 0 due to dict.fromkeys collapsing the - two gfx1100 entries into one and making index 2 out of range.""" - # Simulate rocminfo output for 3 GPUs (2x gfx1100 dGPU + 1x gfx1151 APU). - # Each GPU gets its own Agent section with a few token mentions. - probe_out = ( - "***\nAgent 1\n***\n gfx1100 some info\n gfx1100\n" - "***\nAgent 2\n***\n gfx1100 some info\n gfx1100\n" - "***\nAgent 3\n***\n gfx1151 some info\n gfx1151\n" - ) - monkeypatch.delenv("ROCR_VISIBLE_DEVICES", raising = False) - monkeypatch.delenv("CUDA_VISIBLE_DEVICES", raising = False) - monkeypatch.setenv("HIP_VISIBLE_DEVICES", "2") - assert _pick_rocm_gfx_target(probe_out) == "gfx1151" - - -# --------------------------------------------------------------------------- -# Fork release scan: Windows ROCm resolves lemonade by the requested tag -# --------------------------------------------------------------------------- - -_resolve_release_asset_choice = getattr(_mod, "resolve_release_asset_choice", None) -_ApprovedReleaseChecksums = getattr(_mod, "ApprovedReleaseChecksums", None) - - -@pytest.mark.skipif( - _resolve_release_asset_choice is None or _ApprovedReleaseChecksums is None, - reason = "fork release planner not present on this branch", -) -def test_fork_scan_windows_rocm_resolves_lemonade_by_requested_tag(): - """The fork release scan pins llama_tag to per-release upstream tags - (b9457, ...) that lemonade's own tag series never contains, so the - lemonade lookup must use the requested tag ("latest") instead. Pinning - lemonade to the per-release tag 404s on every scanned release and a - Windows ROCm host ends in a rate-limited fatal instead of the lemonade - prebuilt.""" - host = _make_rocm_host("gfx1151", windows = True) - # No windows-rocm artifact in the bundle, matching current fork releases. - bundle = _rocm_bundle("gfx1151", ["gfx1151"]) - checksums = _ApprovedReleaseChecksums( - repo = "unslothai/llama.cpp", - release_tag = "v1.0", - upstream_tag = "b9457", - artifacts = {}, - ) - seen_urls: list[str] = [] - - def _fake_fetch(api_url, *args, **kwargs): - seen_urls.append(api_url) - if "lemonade-sdk" in api_url: - if api_url.endswith("/releases/latest"): - return _stub_lemonade_release() - raise RuntimeError(f"unexpected pinned lemonade fetch: {api_url}") - # ggml-org asset listing for the upstream HIP/CPU filename fallbacks. - return {"tag_name": "b9457", "assets": []} - - with patch.object(_mod, "fetch_json", side_effect = _fake_fetch): - attempts = _resolve_release_asset_choice( - host, - "b9457", # concrete per-release upstream tag from the scan loop - bundle, - checksums, - requested_tag = "latest", - ) - - lemonade = [a for a in attempts if a.source_label == "lemonade"] - assert lemonade, f"lemonade attempt missing for Windows ROCm host; got {attempts}" - assert "gfx1151" in lemonade[0].name - assert any( - u.endswith("/releases/latest") for u in seen_urls - ), f"lemonade was never resolved via /releases/latest; fetches: {seen_urls}" - assert not any( - "lemonade-sdk" in u and "/releases/tags/" in u for u in seen_urls - ), f"lemonade lookup was pinned to the fork release tag: {seen_urls}" - - -@pytest.mark.skipif( - direct_upstream_release_plan is None, - reason = "direct release planners not present on this branch", -) -def test_direct_upstream_plan_includes_lemonade_for_linux_rocm_host(): - """A Linux ROCm host on the ggml-org direct path (e.g. a --published-repo - override) must plan lemonade before the CPU tarball, mirroring the Windows - branch. The lemonade planning previously lived in the removed - --simple-policy dispatcher, so without this leg such hosts silently - install the CPU build.""" - host = _make_rocm_host("gfx1151") - release = { - "tag_name": "b9022", - "name": "b9022", - "assets": [ - { - "name": "llama-b9022-bin-ubuntu-x64.tar.gz", - "browser_download_url": ( - "https://github.com/ggml-org/llama.cpp/releases/download/" - "b9022/llama-b9022-bin-ubuntu-x64.tar.gz" - ), - } - ], - } - with patch.object(_mod, "fetch_json", return_value = _stub_lemonade_release()): - plan = direct_upstream_release_plan(release, host, "ggml-org/llama.cpp", "latest") - assert plan is not None, "Linux ROCm host should produce a direct plan" - kinds = [a.install_kind for a in plan.attempts] - sources = [a.source_label for a in plan.attempts] - assert "linux-rocm" in kinds, f"lemonade ROCm attempt missing; got {kinds}" - assert sources[0] == "lemonade", f"lemonade must be the first attempt; got {sources}" - assert "gfx1151" in plan.attempts[0].name diff --git a/studio/backend/tests/test_llama_cpp_update.py b/studio/backend/tests/test_llama_cpp_update.py index 3b8f511a7c..e207306c91 100644 --- a/studio/backend/tests/test_llama_cpp_update.py +++ b/studio/backend/tests/test_llama_cpp_update.py @@ -420,8 +420,8 @@ def test_start_update_installer_failure_reports_error(monkeypatch, tmp_path): # --- installer-argument construction (mirrors the post-#5963 setup scripts) --- -def test_rocm_install_args_lemonade_gfx(): - # Lemonade HIP app bundle: gfx family lives in the asset name. +def test_rocm_install_args_gfx_family(): + # Per-gfx ROCm bundle: gfx family lives in the asset name. assert upd._rocm_install_args("app-b9585-linux-x64-rocm-gfx110X.tar.gz") == [ "--rocm-gfx", "gfx110x", diff --git a/studio/backend/utils/llama_cpp_update.py b/studio/backend/utils/llama_cpp_update.py index 6eb34ffe34..8b90d36bc5 100644 --- a/studio/backend/utils/llama_cpp_update.py +++ b/studio/backend/utils/llama_cpp_update.py @@ -310,8 +310,9 @@ def get_update_status(*, force_refresh: bool = False) -> dict: def _rocm_install_args(asset: Optional[str]) -> list[str]: """Forward --rocm-gfx/--has-rocm from the marker asset, mirroring setup.sh. - The installer probe can miss the gfx arch on amd-smi-only hosts; lemonade - bundles carry the family in the name (rocm-gfx110X), fork bundles only rocm/hip.""" + The installer probe can miss the gfx arch on amd-smi-only hosts; per-gfx + ROCm bundles carry the family in the name (rocm-gfx110X), version-tagged + bundles only rocm/hip.""" if not asset: return [] low = asset.lower() diff --git a/studio/install_llama_prebuilt.py b/studio/install_llama_prebuilt.py index 5473491bde..219211034d 100644 --- a/studio/install_llama_prebuilt.py +++ b/studio/install_llama_prebuilt.py @@ -9,7 +9,6 @@ from __future__ import annotations import argparse import errno import fnmatch -import functools import hashlib import json import os @@ -140,35 +139,6 @@ DEFAULT_PUBLISHED_SHA256_ASSET = os.environ.get( UPSTREAM_REPO = "ggml-org/llama.cpp" UPSTREAM_RELEASES_API = f"https://api.github.com/repos/{UPSTREAM_REPO}/releases/latest" -LEMONADE_ROCM_REPO = "lemonade-sdk/llamacpp-rocm" -LEMONADE_ROCM_RELEASES_API = f"https://api.github.com/repos/{LEMONADE_ROCM_REPO}/releases/latest" - - -def _lemonade_release_api_for(llama_tag: str) -> str: - """Return the GitHub API URL for the lemonade release that matches a - requested llama.cpp tag. - - When llama_tag is unset or "latest", point at /releases/latest. When the - caller has pinned a specific tag (e.g. "b1260"), point at the same tag in - lemonade. Lemonade tracks `ggml-org/llama.cpp` build tags (e.g. "b1260") - but is NOT guaranteed to publish every upstream build -- lemonade may be - several builds behind ggml-org. Pinning to a specific tag that lemonade - skipped will produce a 404 and the caller falls through to the upstream - tarball; that is intentional so pinned installs stay reproducible. - Do NOT pass a `unslothai/llama.cpp` fork tag -- the fork uses its own - namespace and will always 404 against lemonade. - - The tag is URL-encoded with `safe=""` so an unexpected slash / hash / query - character cannot reshape the URL. - """ - normalized = (llama_tag or "").strip() - if not normalized or normalized.lower() == "latest": - return LEMONADE_ROCM_RELEASES_API - return ( - f"https://api.github.com/repos/{LEMONADE_ROCM_REPO}/releases/tags/" - f"{urllib.parse.quote(normalized, safe = '')}" - ) - TEST_MODEL_URL = "https://huggingface.co/ggml-org/models/resolve/main/tinyllamas/stories260K.gguf" TEST_MODEL_SHA256 = "270cba1bd5109f42d03350f60406024560464db173c0e387d91f0426d3bd256d" @@ -1395,22 +1365,10 @@ def direct_linux_release_plan( ) if selection is not None: attempts.extend(selection.attempts) - if host.has_rocm and not host.has_usable_nvidia: - # Per-GPU lemonade prebuilts ship the ROCm runtime libs alongside - # llama.cpp, so they install cleanly even on hosts (e.g. gfx1151 - # Strix Halo) the upstream combined-ROCm tarball doesn't cover. - # "ubuntu" is lemonade's asset naming convention only -- the binary - # is a manylinux-style glibc build that runs on Arch, Fedora, - # openSUSE, etc. with a recent-enough glibc. Do NOT append the CPU - # asset for ROCm-only hosts: if lemonade fails validation we want - # validate_prebuilt_attempts to raise PrebuiltFallback so the caller - # triggers the HIP source build, not silently install a CPU binary. - lemonade_choice = resolve_lemonade_rocm_choice( - host, "ubuntu", "linux-rocm", llama_tag = requested_tag - ) - if lemonade_choice is not None: - attempts.append(lemonade_choice) - elif not host.has_usable_nvidia: + elif not host.has_rocm: + # A ROCm-only host gets no CPU asset: leaving attempts empty lets the + # raise below trigger a HIP source build instead of shipping a CPU + # binary on a GPU host (this ggml-org path has no per-gfx ROCm asset). cpu_choice = published_asset_choice_for_kind(bundle, "linux-cpu") if cpu_choice is not None: attempts.append(cpu_choice) @@ -1489,11 +1447,6 @@ def direct_upstream_release_plan( if pinned is not None: attempts.insert(0, pinned) elif host.has_rocm: - lemonade_choice = resolve_lemonade_rocm_choice( - host, "windows", "windows-hip", llama_tag = requested_tag - ) - if lemonade_choice is not None: - attempts.append(lemonade_choice) hip_asset = f"llama-{release_tag}-bin-win-hip-radeon-x64.zip" hip_url = assets.get(hip_asset) if hip_url: @@ -1566,15 +1519,10 @@ def direct_upstream_release_plan( install_kind = "macos-x64", ) ) - elif host.is_linux and host.is_x86_64 and not host.has_usable_nvidia: - if host.has_rocm: - # Lemonade first, mirroring the Windows ROCm branch above, so a - # ROCm host routed to ggml-org does not silently get the CPU build. - lemonade_choice = resolve_lemonade_rocm_choice( - host, "ubuntu", "linux-rocm", llama_tag = requested_tag - ) - if lemonade_choice is not None: - attempts.append(lemonade_choice) + elif host.is_linux and host.is_x86_64 and not host.has_usable_nvidia and not host.has_rocm: + # ROCm hosts are excluded: this ggml-org path ships no per-gfx ROCm + # asset, so they fall through to the empty-attempts raise (HIP source + # build) rather than silently getting a CPU binary on a GPU host. asset_name = f"llama-{release_tag}-bin-ubuntu-x64.tar.gz" asset_url = assets.get(asset_name) if asset_url: @@ -3115,7 +3063,7 @@ def _apply_host_overrides( A forwarded gfx (--rocm-gfx or UNSLOTH_ROCM_GFX_ARCH) is authoritative and implies ROCm: the installer's own hipinfo/amd-smi probe can miss the arch on amd-smi-only hosts or when setup inferred it from the GPU name, leaving - rocm_gfx_target None and no lemonade prebuilt selected. force_cpu is the + rocm_gfx_target None and no per-gfx ROCm prebuilt selected. force_cpu is the opposite explicit signal (arm64 Linux GPU host whose source build failed): drop GPU attributes so the CPU prebuilt for this OS/arch is selected.""" if force_cpu: @@ -3473,8 +3421,7 @@ def _pinned_windows_cuda_fallback( once upstream ships a driver-runnable build again. The b9360 binary reuses the current release's source tree and convert scripts - and is recorded via binary_release_tag, the same binary/source split used for - the lemonade prebuilt.""" + and is recorded via binary_release_tag.""" if not (host.is_windows and host.is_x86_64 and host.has_usable_nvidia): return None driver = host.driver_cuda_version @@ -3850,46 +3797,30 @@ def _detect_host_rocm_version() -> tuple[int, int] | None: return None -# Map detected gfx IDs to lemonade-sdk asset family suffixes. -# More-specific prefixes must come before shorter ones (e.g. gfx1151 before gfx110). -_LEMONADE_GFX_FAMILIES: list[tuple[str, str]] = [ - ("gfx1151", "gfx1151"), - ("gfx1150", "gfx1150"), - ("gfx120", "gfx120X"), - ("gfx110", "gfx110X"), - ("gfx103", "gfx103X"), -] - - -def _lemonade_gfx_family(gfx_id: str) -> str | None: - gfx_id = gfx_id.lower().strip() - for prefix, family in _LEMONADE_GFX_FAMILIES: - if gfx_id.startswith(prefix): - return family - return None - - def published_rocm_choice_for_host( release: PublishedReleaseBundle, host: HostInfo, install_kind: str ) -> AssetChoice | None: """Select the published ROCm bundle whose gfx target covers the host GPU. - The manifest's gfx_target uses the same umbrella family labels that - _lemonade_gfx_family produces (gfx110X, gfx120X, ...), so the host's detected - gfx is matched either to that family or to the bundle's concrete - mapped_targets list. Returns None when no published bundle covers the GPU, so - the caller can fall back (lemonade / upstream HIP).""" + The manifest's gfx_target uses umbrella family labels (gfx110X, gfx120X, + ...). A host's detected gfx is matched against the bundle's concrete + mapped_targets list, or against the family label itself. Returns None when no + published bundle covers the GPU, so the caller falls back to a HIP source + build.""" if not host.rocm_gfx_target: return None gfx = host.rocm_gfx_target.lower().strip() for artifact in release.artifacts: if artifact.install_kind != install_kind: continue - # Match on the concrete built-arch list, not the family prefix: an - # in-generation-but-unbuilt arch (e.g. gfx1033 in the gfx103 prefix) must - # NOT be served the family bundle. None makes the caller fall back to a - # source build for that GPU. - if gfx not in {target.lower() for target in artifact.mapped_targets}: + # Match the concrete built-arch list so an in-generation-but-unbuilt arch + # (e.g. gfx1033 in the gfx103 family) is NOT served the family bundle and + # falls back to a source build. Also accept the family label itself: the + # llama.cpp update path re-derives --rocm-gfx from the family-named marker + # asset, so an update forwards the family token (gfx110X), not a concrete + # arch. + mapped = {target.lower() for target in artifact.mapped_targets} + if gfx not in mapped and gfx != (artifact.gfx_target or "").lower(): continue asset_url = release.assets.get(artifact.asset_name) if not asset_url: @@ -3910,184 +3841,7 @@ def published_rocm_choice_for_host( return None -def _is_trusted_github_release_url(url: str, expected_repo: str) -> bool: - """Validate a release asset URL points at GitHub's expected hosts. - - Accepts: - https://github.com/{expected_repo}/releases/download/... - https://objects.githubusercontent.com/... (GitHub's release CDN) - Anything else (including http://, raw.githubusercontent.com, gist, etc.) - is rejected so a malicious API response cannot redirect downloads to an - attacker-chosen host. - """ - if not isinstance(url, str) or not url: - return False - try: - parsed = urllib.parse.urlparse(url) - except Exception: - return False - if parsed.scheme != "https": - return False - host = (parsed.netloc or "").lower() - if host == "objects.githubusercontent.com": - # GitHub's release CDN. Restrict to release-asset paths so a tampered - # API response pointing at an arbitrary CDN object is still rejected. - # Real release asset URLs carry the "/github-production-release-asset-" - # prefix; gist / raw / avatar CDN paths do not. - return parsed.path.startswith("/github-production-release-asset-") - if host == "github.com": - return parsed.path.startswith(f"/{expected_repo}/releases/download/") - return False - - -# (gfx_target, asset_name) pairs already logged. resolve_lemonade_rocm_choice() -# runs twice per install (direct planner + resolve_upstream_asset_choice), so -# this stops its selection banner and hash-manifest NOTE printing twice. -_lemonade_selection_logged: "set[tuple[str, str]]" = set() - - -@functools.lru_cache(maxsize = 8) -def _fetch_lemonade_release_cached(api_url: str, llama_tag: str) -> "dict | None": - """Cached wrapper around fetch_json for lemonade release lookups. - - resolve_lemonade_rocm_choice() is called twice per install (once from the - direct planner, once from resolve_upstream_asset_choice) with identical - arguments. Without memoisation, each install hits api.github.com twice, - doubling the rate-limit failure surface on busy CI runners. Cache is - process-scoped; tests that need to vary fetch_json's return value across - invocations should call cache_clear(). - """ - try: - return fetch_json(api_url) - except Exception as exc: - normalized = (llama_tag or "").strip().lower() - if normalized and normalized != "latest": - log( - f"Could not fetch {LEMONADE_ROCM_REPO} release for " - f"llama_tag={llama_tag!r} ({exc}); skipping lemonade prebuilt" - ) - else: - log(f"Could not fetch {LEMONADE_ROCM_REPO} latest release: {exc}") - return None - - -def resolve_lemonade_rocm_choice( - host: HostInfo, - os_prefix: str, - install_kind: str, - llama_tag: str = "latest", -) -> "AssetChoice | None": - """Return an AssetChoice from lemonade-sdk/llamacpp-rocm for the detected GPU, or None. - - os_prefix: lemonade's asset filename label, NOT a host-distro filter. - Pass "ubuntu" for any Linux host (Arch, Fedora, openSUSE, - Debian, ...) -- lemonade only publishes one Linux variant - and it is a manylinux-style glibc build that runs on any - distro with a recent-enough glibc. Pass "windows" for - Windows hosts. - install_kind: "linux-rocm" or "windows-hip" - llama_tag: the requested upstream llama.cpp tag ("latest" or a pinned - release like "b1260"). When pinned, the resolver fetches - the matching lemonade release. When the pinned tag is not - published by lemonade we skip silently (and the caller - falls through to upstream) rather than drift to whatever - lemonade ships as latest. - """ - if not host.rocm_gfx_target: - return None - # Opt-out for users who want the upstream HIP build path only -- lemonade - # binaries are downloaded without entries in the approved-hash manifest, so - # the integrity gate is functional validation only. - if os.environ.get("UNSLOTH_DISABLE_LEMONADE_ROCM", "").strip().lower() in ( - "1", - "true", - "yes", - ): - log("UNSLOTH_DISABLE_LEMONADE_ROCM is set; skipping lemonade-sdk prebuilt") - return None - gfx_family = _lemonade_gfx_family(host.rocm_gfx_target) - if gfx_family is None: - log( - f"AMD GPU {host.rocm_gfx_target!r} is not covered by lemonade-sdk ROCm prebuilts; " - "skipping lemonade prebuilt" - ) - return None - api_url = _lemonade_release_api_for(llama_tag) - release = _fetch_lemonade_release_cached(api_url, llama_tag) - if release is None: - return None - release_tag = release.get("tag_name") if isinstance(release, dict) else None - if not isinstance(release_tag, str) or not release_tag: - log(f"Unexpected {LEMONADE_ROCM_REPO} release payload; skipping lemonade prebuilt") - return None - assets = release_asset_map(release) - asset_name = f"llama-{release_tag}-{os_prefix}-rocm-{gfx_family}-x64.zip" - if asset_name not in assets: - log( - f"{LEMONADE_ROCM_REPO}@{release_tag} has no asset {asset_name!r}; " - "skipping lemonade prebuilt" - ) - return None - asset_url = assets[asset_name] - if not asset_url: - # release_asset_map defaults to "" when an asset row is missing - # browser_download_url; skip cleanly instead of letting - # download_file("") raise a less obvious error downstream. - log( - f"{LEMONADE_ROCM_REPO}@{release_tag} asset {asset_name!r} has no " - "browser_download_url; skipping lemonade prebuilt" - ) - return None - # Defence in depth: lemonade browser_download_url should be on github.com - # or githubusercontent.com. A compromised GitHub API response that - # redirects to an attacker-chosen host would otherwise be honoured - # silently (lemonade assets are not in the approved-hash manifest). - if not _is_trusted_github_release_url(asset_url, LEMONADE_ROCM_REPO): - log( - f"{LEMONADE_ROCM_REPO}@{release_tag} asset {asset_name!r} points " - f"to an unexpected host ({asset_url!r}); refusing to download " - "lemonade prebuilt" - ) - return None - # Note: lemonade tags Linux assets with "ubuntu" but the binary is a - # generic glibc build that runs on any distro (Arch, Fedora, ...), so - # this attempt is selected for all Linux ROCm hosts, not just Ubuntu. - # Log once per (gfx_target, asset); see _lemonade_selection_logged. - log_key = (host.rocm_gfx_target, asset_name) - if log_key not in _lemonade_selection_logged: - _lemonade_selection_logged.add(log_key) - log( - f"AMD GPU {host.rocm_gfx_target!r} ({gfx_family}) -- " - f"trying lemonade-sdk ROCm prebuilt {asset_name} " - f"(works on any glibc Linux, not just Ubuntu)" - ) - log( - f"NOTE: lemonade-sdk/llamacpp-rocm releases are not covered by the " - f"Unsloth approved-hash manifest; download integrity relies on " - f"functional validation (llama-bench / llama-server smoke tests) " - f"after extraction. Set UNSLOTH_DISABLE_LEMONADE_ROCM=1 to skip " - f"lemonade and fall back to the upstream HIP build path." - ) - return AssetChoice( - repo = LEMONADE_ROCM_REPO, - tag = release_tag, - name = asset_name, - url = asset_url, - source_label = "lemonade", - install_kind = install_kind, - ) - - -def resolve_upstream_asset_choice( - host: HostInfo, - llama_tag: str, - lemonade_tag: "str | None" = None, -) -> AssetChoice: - # lemonade_tag: tag for the lemonade lookup only. The release scan pins - # llama_tag to per-release upstream tags (b9518, ...) that lemonade's own - # tag series (b1292, ...) never contains, so pinning lemonade to them 404s - # on every scanned release. Scan callers pass the original request - # (normally "latest") here; upstream asset names keep the pinned tag. +def resolve_upstream_asset_choice(host: HostInfo, llama_tag: str) -> AssetChoice: upstream_assets = github_release_assets(UPSTREAM_REPO, llama_tag) if host.is_linux and host.is_x86_64: # AMD ROCm: try upstream ROCm prebuilt first, then fall back to source build. @@ -4095,15 +3849,7 @@ def resolve_upstream_asset_choice( # the exact GPU target via rocminfo, which is more reliable for consumer # GPUs (e.g. gfx1151) that may not be in the prebuilt. if host.has_rocm and not host.has_usable_nvidia: - # Try lemonade-sdk per-GPU prebuilt first: these are built against - # specific gfx targets and bundle all required ROCm runtime libs. - lemonade_choice = resolve_lemonade_rocm_choice( - host, "ubuntu", "linux-rocm", llama_tag = lemonade_tag or llama_tag - ) - if lemonade_choice is not None: - return lemonade_choice - - # Fall back to upstream combined ROCm tarball. + # Upstream combined ROCm tarball. # Scan upstream assets for any rocm- prebuilt. When the # host ROCm runtime version is known, pick the newest candidate # whose major.minor is <= host version -- otherwise a ROCm 6.4 @@ -4181,14 +3927,8 @@ def resolve_upstream_asset_choice( return attempts[0] raise PrebuiltFallback("no compatible Windows CUDA asset was found") - # AMD ROCm on Windows: try lemonade per-GPU prebuilt first, then upstream HIP + # AMD ROCm on Windows: try upstream HIP prebuilt, then fall back to CPU if host.has_rocm: - lemonade_choice = resolve_lemonade_rocm_choice( - host, "windows", "windows-hip", llama_tag = lemonade_tag or llama_tag - ) - if lemonade_choice is not None: - return lemonade_choice - hip_name = f"llama-{llama_tag}-bin-win-hip-radeon-x64.zip" if hip_name in upstream_assets: log(f"AMD ROCm detected on Windows -- trying upstream HIP prebuilt {hip_name}") @@ -4243,16 +3983,12 @@ def resolve_upstream_asset_choice( raise PrebuiltFallback(f"no prebuilt policy exists for {host.system} {host.machine}") -def resolve_asset_choice( - host: HostInfo, - llama_tag: str, - lemonade_tag: "str | None" = None, -) -> AssetChoice: +def resolve_asset_choice(host: HostInfo, llama_tag: str) -> AssetChoice: if host.is_linux and host.is_x86_64 and host.has_usable_nvidia: raise PrebuiltFallback( "Linux CUDA installs require a compatible published bundle; upstream fallback is not available" ) - return resolve_upstream_asset_choice(host, llama_tag, lemonade_tag = lemonade_tag) + return resolve_upstream_asset_choice(host, llama_tag) def resolve_release_asset_choice( @@ -4260,7 +3996,6 @@ def resolve_release_asset_choice( llama_tag: str, release: PublishedReleaseBundle, checksums: ApprovedReleaseChecksums, - requested_tag: "str | None" = None, ) -> list[AssetChoice]: if host.is_windows and host.is_x86_64 and host.has_usable_nvidia: torch_preference = detect_torch_cuda_runtime_preference(host) @@ -4317,7 +4052,7 @@ def resolve_release_asset_choice( ) return apply_approved_hashes( - [resolve_asset_choice(host, llama_tag, lemonade_tag = requested_tag)], + [resolve_asset_choice(host, llama_tag)], checksums, ) @@ -6108,15 +5843,6 @@ def apply_approved_hashes( approved_attempts: list[AssetChoice] = [] missing_assets: list[str] = [] for attempt in attempts: - # External prebuilts (e.g. lemonade-sdk) are not listed in the - # approved-hash manifest; they are explicitly documented as relying - # on functional validation only (llama-bench / smoke tests). - # Passing them through here lets the caller include both a lemonade - # attempt and a hash-approved upstream fallback in the same list - # without apply_approved_hashes discarding the lemonade entry. - if attempt.source_label == "lemonade": - approved_attempts.append(attempt) - continue approved = approved_hash_for_attempt(attempt) if approved is None: missing_assets.append(attempt.name) @@ -6204,13 +5930,11 @@ def resolve_install_attempts( return requested_tag, plan.llama_tag, plan.attempts, plan.approved_checksums -def _linux_published_attempts( - host: HostInfo, bundle: PublishedReleaseBundle, requested_tag: str -) -> list[AssetChoice]: +def _linux_published_attempts(host: HostInfo, bundle: PublishedReleaseBundle) -> list[AssetChoice]: """Build the install attempts for a fork Linux host from a manifest-described - bundle: CUDA (with a CPU fallback), per-gfx ROCm (with a lemonade fallback), - or CPU. Same selection the upstream filename path used, just sourced from the - manifest instead of reconstructed from asset names.""" + bundle: CUDA (with a CPU fallback), per-gfx ROCm, or CPU. Same selection the + upstream filename path used, just sourced from the manifest instead of + reconstructed from asset names.""" attempts: list[AssetChoice] = [] if host.has_usable_nvidia: # Prefer the cudart major Studio loads at runtime (torch's bundled @@ -6226,20 +5950,14 @@ def _linux_published_attempts( if selection is not None: attempts.extend(selection.attempts) if host.has_rocm and not host.has_usable_nvidia: - # Prefer the fork's own per-gfx ROCm bundle (hash-approved, ships the - # full ROCm runtime) and fall back to the external lemonade prebuilt. - # Do NOT append the CPU asset for ROCm-only hosts: if lemonade fails - # validation we want validate_prebuilt_attempts to raise PrebuiltFallback - # so the caller triggers the HIP source build, not silently install a - # CPU-only binary. + # Use the fork's own per-gfx ROCm bundle (hash-approved, ships the full + # ROCm runtime). Do NOT append the CPU asset for ROCm-only hosts: if no + # bundle covers the GPU we want validate_prebuilt_attempts to raise + # PrebuiltFallback so the caller triggers the HIP source build, not + # silently install a CPU-only binary. published_rocm = published_rocm_choice_for_host(bundle, host, "linux-rocm") if published_rocm is not None: attempts.append(published_rocm) - lemonade_choice = resolve_lemonade_rocm_choice( - host, "ubuntu", "linux-rocm", llama_tag = requested_tag - ) - if lemonade_choice is not None: - attempts.append(lemonade_choice) else: cpu_choice = published_asset_choice_for_kind(bundle, "linux-cpu") if cpu_choice is not None: @@ -6279,7 +5997,7 @@ def _fork_manifest_release_plans( resolved_tag = bundle.upstream_tag try: if host.is_linux: - linux_attempts = _linux_published_attempts(host, bundle, requested_tag) + linux_attempts = _linux_published_attempts(host, bundle) if not linux_attempts: raise PrebuiltFallback("no compatible Linux prebuilt asset was found") attempts = apply_approved_hashes(linux_attempts, checksums) @@ -6293,7 +6011,6 @@ def _fork_manifest_release_plans( resolved_tag, bundle, checksums, - requested_tag = requested_tag, ) if not attempts: raise PrebuiltFallback("no compatible prebuilt asset was found") @@ -6364,10 +6081,10 @@ def write_prebuilt_metadata( "asset": choice.name, "asset_sha256": choice.expected_sha256, "source": choice.source_label, - # Binary-side repo/tag for non-upstream sources (e.g. lemonade). - # published_repo/release_tag always refer to the unsloth source tree; - # these capture where the actual binaries came from so the install - # summary can show both (e.g. "unslothai/llama.cpp@b9334 + lemonade@b1280"). + # Binary-side repo/tag for non-fork sources (e.g. the ggml-org upstream + # CPU/HIP prebuilts). published_repo/release_tag always refer to the + # unsloth source tree; these capture where the actual binaries came from + # so the install summary can show both. "binary_repo": choice.repo, "binary_release_tag": choice.tag, "source_asset": source_asset_name, @@ -6663,7 +6380,7 @@ def validate_prebuilt_choice( approved_checksums = approved_checksums, prebuilt_fallback_used = prebuilt_fallback_used, ) - # Hashless external prebuilts (e.g. lemonade) are not in the approved-sha256 + # Hashless external prebuilts are not in the approved-sha256 # manifest and rely on the functional smoke test as their only integrity gate, # so they are always validated. For an approved bundle the sha256 manifest # already proves integrity, so its runtime smoke test -- a cold CUDA-JIT pass @@ -6931,7 +6648,7 @@ def parse_args() -> argparse.Namespace: default = os.environ.get("UNSLOTH_ROCM_GFX_ARCH"), help = ( "Forward the AMD gfx target (e.g. gfx1151) that setup.ps1/setup.sh " - "resolved, so the lemonade HIP prebuilt is selected even when the " + "resolved, so the per-gfx ROCm prebuilt is selected even when the " "installer's own hipinfo/amd-smi probe cannot report it. Implies " "--has-rocm. Defaults to the UNSLOTH_ROCM_GFX_ARCH environment variable." ), diff --git a/studio/install_python_stack.py b/studio/install_python_stack.py index e540aac305..d404041ca7 100644 --- a/studio/install_python_stack.py +++ b/studio/install_python_stack.py @@ -433,7 +433,7 @@ def _detect_windows_gfx_arch() -> str | None: # GPU marketing-name → gfx arch table, mirroring setup.ps1's $nameArchTable. -# Most-specific first; first match wins. Covers only arches the lemonade-sdk +# Most-specific first; first match wins. Covers only arches the ROCm # prebuilts / AMD Windows torch indexes support; unknown names return None # (callers then fall back cleanly to CPU). _WIN_GPU_NAME_ARCH_TABLE: "list[tuple[str, str]]" = [ diff --git a/studio/setup.ps1 b/studio/setup.ps1 index 542846ac70..acc7e92875 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -850,7 +850,7 @@ if (-not $HasNvidiaSmi) { # popping a UAC/DiskPart prompt RunAsInvoker can't suppress (its manifest is # asInvoker; even 'amd-smi version' hangs). So only probe when a HIP SDK is present # (hipinfo found -> un-elevated) or the user opts in; else fall through to WMI name - # inference (enough to pick ROCm wheels + lemonade llama.cpp). + # inference (enough to pick ROCm wheels + the ROCm llama.cpp prebuilt). # An explicit opt-out (UNSLOTH_ENABLE_AMD_SMI=0/false/no/off) wins over the HIP-SDK # heuristic: a HIP SDK binary with a broken runtime can still pop the prompt, so # $HipSdkInstalled must NOT silently re-enable it. @@ -923,7 +923,7 @@ if (-not $HasNvidiaSmi) { } # ── Arch resolution: env-var override → name inference ────────────────── # Runs after all probes, even when none confirmed a ROCm runtime ($HasROCm false): - # the Adrenalin driver alone runs the lemonade-sdk llama.cpp prebuilt (bundles its + # the Adrenalin driver alone runs the per-gfx ROCm llama.cpp prebuilt (bundles its # own runtime), and all it needs is the gfx arch, inferable from the WMI GPU name. # Resolving it here lets setup.ps1 forward --rocm-gfx so a GPU llama.cpp is pulled # instead of CPU. (PyTorch ROCm wheels still require a HIP SDK -- gated on $HasROCm @@ -936,7 +936,7 @@ if (-not $HasNvidiaSmi) { substep "gfx arch from UNSLOTH_ROCM_GFX_ARCH env override: $script:ROCmGfxArch" "Cyan" } # 2. Best-effort name → arch lookup (amd-smi / WMI). Most-specific first, - # first match wins. Covers only arches the lemonade-sdk prebuilts support + # first match wins. Covers only arches the ROCm prebuilts support # (gfx120X/110X/1151/1150/103X); unknown names fall back cleanly to CPU. elseif ($ROCmGpuLabel) { $nameArchTable = @( @@ -947,9 +947,9 @@ if (-not $HasNvidiaSmi) { @{ P = "RX 7900|RX 7800|RX 7700(?!S)|PRO W7900|PRO W7800|PRO W7700"; A = "gfx1100" } # RDNA 3 desktop / workstation (Navi 31) @{ P = "RX 7600|RX 7700S|RX 7650|PRO W7600|PRO W7500|PRO V710"; A = "gfx1102" } # RDNA 3 (Navi 33) @{ P = "780M|760M|740M|Phoenix|Hawk Point|Z1 Extreme|Z2 Extreme"; A = "gfx1103" } # RDNA 3 iGPU (Phoenix / Hawk Point) - @{ P = "RX 6900|RX 6800|RX 6750|RX 6700|PRO W6800|PRO W6900"; A = "gfx1030" } # RDNA 2 (Navi 21) -- lemonade gfx103X - @{ P = "RX 6650|RX 6600|PRO W6600|PRO W6650"; A = "gfx1032" } # RDNA 2 (Navi 23) -- lemonade gfx103X - @{ P = "RX 6500|RX 6400|RX 6300|PRO W6400|PRO W6500"; A = "gfx1034" } # RDNA 2 (Navi 24) -- lemonade gfx103X + @{ P = "RX 6900|RX 6800|RX 6750|RX 6700|PRO W6800|PRO W6900"; A = "gfx1030" } # RDNA 2 (Navi 21) -- gfx103X family + @{ P = "RX 6650|RX 6600|PRO W6600|PRO W6650"; A = "gfx1032" } # RDNA 2 (Navi 23) -- gfx103X family + @{ P = "RX 6500|RX 6400|RX 6300|PRO W6400|PRO W6500"; A = "gfx1034" } # RDNA 2 (Navi 24) -- gfx103X family ) foreach ($row in $nameArchTable) { if ($ROCmGpuLabel -match $row.P) { @@ -2644,7 +2644,10 @@ $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. -$HelperReleaseRepo = if ($HasNvidiaSmi -or $HasROCm) { "unslothai/llama.cpp" } else { "ggml-org/llama.cpp" } +# 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" } $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 } @@ -2754,7 +2757,7 @@ if ($env:UNSLOTH_LLAMA_FORCE_COMPILE -eq "1") { # or the upstream windows-hip fallback, so accept either and never # treat a valid ROCm install as mismatched. A name-inferred gfx # arch (Adrenalin-only, no confirmed runtime) still counts as - # ROCm-capable -- the lemonade prebuilt bundles its own runtime, + # 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, @@ -2785,7 +2788,7 @@ if ($env:UNSLOTH_LLAMA_FORCE_COMPILE -eq "1") { if ($HasROCm) { $prebuiltArgs += "--has-rocm" } - # Forward the resolved gfx arch so the lemonade HIP prebuilt is picked even + # Forward the resolved gfx arch so the per-gfx ROCm prebuilt is picked even # when the installer's probe can't confirm the runtime (amd-smi-only / # Adrenalin-only, name-inferred arch). --rocm-gfx is authoritative and # implies ROCm in install_llama_prebuilt.py, so the GPU prebuilt is selected @@ -2971,10 +2974,10 @@ if (-not $NeedLlamaSourceBuild) { } elseif ($HasROCm -or $script:ROCmGfxArch) { # AMD GPU present but in the CPU-only source-build fallback: a HIP source # build needs the full HIP SDK + ROCm clang toolchain. AMD GPU acceleration - # comes from the lemonade prebuilt (bundles the runtime, no SDK) -- reaching + # comes from the per-gfx ROCm prebuilt (bundles the runtime, no SDK) -- reaching # here means it couldn't be installed. Warn loudly, don't ship a slow CPU build. $_amdArch = if ($script:ROCmGfxArch) { $script:ROCmGfxArch } else { "ROCm" } - substep "[WARN] AMD GPU ($_amdArch) detected, but the GPU-accelerated lemonade" "Yellow" + substep "[WARN] AMD GPU ($_amdArch) detected, but the GPU-accelerated ROCm" "Yellow" substep " llama.cpp prebuilt could not be installed -- falling back to a CPU build." "Yellow" substep " The prebuilt is the AMD GPU path (no HIP SDK required). To restore GPU" "Yellow" substep " acceleration: re-run the installer (check your network / proxy), or set" "Yellow" diff --git a/studio/setup.sh b/studio/setup.sh index e56fab36ef..613d311708 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -340,9 +340,9 @@ binary_tag = str(payload.get("binary_release_tag") or "").strip() if not repo or not release_tag: raise SystemExit(0) -# For non-upstream sources (e.g. lemonade) the published_repo/release_tag -# refer to the unsloth source tree while the actual binaries came from a -# different repo. Show both so the log is unambiguous. +# For non-fork sources (e.g. ggml-org upstream prebuilts) the published_repo/ +# release_tag refer to the unsloth source tree while the actual binaries came +# from a different repo. Show both so the log is unambiguous. if source and source != "upstream" and binary_repo and binary_tag and binary_repo != repo: message = f"installed release: {repo}@{release_tag} + {source}@{binary_tag}" else: @@ -1004,6 +1004,19 @@ else fi done fi +# 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 + _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" ] \ @@ -1086,7 +1099,7 @@ else if [ -n "${UNSLOTH_LLAMA_RELEASE_TAG:-}" ]; then _PREBUILT_CMD+=(--published-release-tag "$UNSLOTH_LLAMA_RELEASE_TAG") fi - # Forward the gfx arch resolved above so the lemonade HIP prebuilt is picked + # Forward the gfx arch resolved above so the per-gfx ROCm prebuilt is picked # even when the installer's own probe cannot report it (amd-smi-only hosts, # name-inferred arch). Implies --has-rocm on the installer side. if [ -n "${_setup_gfx:-}" ]; then diff --git a/tests/studio/install/test_install_llama_prebuilt_logic.py b/tests/studio/install/test_install_llama_prebuilt_logic.py index 313cfa9225..9ccfe15a88 100644 --- a/tests/studio/install/test_install_llama_prebuilt_logic.py +++ b/tests/studio/install/test_install_llama_prebuilt_logic.py @@ -2854,7 +2854,7 @@ def test_validate_prebuilt_choice_approved_validation_skipped_when_flag_off(tmp_ def test_validate_prebuilt_choice_hashless_build_always_validated(tmp_path, monkeypatch): - # A hashless external build (e.g. lemonade) has no approved sha256, so the + # A hashless external build has no approved sha256, so the # functional smoke test is its only integrity gate and must run even while the # flag is off -- otherwise a corrupted/replaced archive could be activated. calls = _run_validate_prebuilt_choice(monkeypatch, tmp_path, expected_sha256 = None) 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 6c780057ad..8643806127 100644 --- a/tests/studio/install/test_llama_pr_force_and_source.py +++ b/tests/studio/install/test_llama_pr_force_and_source.py @@ -467,7 +467,7 @@ class TestSourcePatternsPs1: # (GPU -> fork, CPU -> ggml-org), mirroring setup.sh. assert "$HelperReleaseRepo = if ($env:UNSLOTH_LLAMA_RELEASE_REPO)" not in self.content assert ( - "$HelperReleaseRepo = if ($HasNvidiaSmi -or $HasROCm) " + "$HelperReleaseRepo = if ($HasNvidiaSmi -or $HasROCm -or $script:ROCmGfxArch) " '{ "unslothai/llama.cpp" } else { "ggml-org/llama.cpp" }' in self.content ) diff --git a/tests/studio/install/test_rocm_support.py b/tests/studio/install/test_rocm_support.py index 1a7de1af6d..a3c9555d0b 100644 --- a/tests/studio/install/test_rocm_support.py +++ b/tests/studio/install/test_rocm_support.py @@ -3130,7 +3130,7 @@ class TestHipSdkInstalledButDeviceInaccessible: # TEST: --rocm-gfx forwarding -- setup.sh/setup.ps1 hand their resolved gfx arch -# to install_llama_prebuilt.py so the lemonade HIP prebuilt is selected even when +# to install_llama_prebuilt.py so the per-gfx ROCm prebuilt is selected even when # the installer's own hipinfo/amd-smi probe cannot report it. _SETUP_SH_PATH = PACKAGE_ROOT / "studio" / "setup.sh" @@ -3219,6 +3219,176 @@ class TestRocmGfxForwarding: assert "--rocm-gfx" in source assert "$script:ROCmGfxArch" in source + def test_setup_sh_routes_inferred_gfx_to_fork(self): + # A forwarded/inferred gfx arch must route to the fork even without ROCm + # tooling on PATH, so the per-gfx prebuilt is picked over ggml-org. Pin + # the routing guard specifically -- a bare "${_setup_gfx:-}" check also + # appears in the unrelated --rocm-gfx forwarding block. + source = _SETUP_SH_PATH.read_text(encoding = "utf-8") + assert '[ "$_LINUX_HAS_GPU" = false ] && [ -n "${_setup_gfx:-}" ]' in source + + def test_setup_ps1_routes_inferred_gfx_to_fork(self): + # Same on Windows: a resolved $script:ROCmGfxArch counts as a fork/GPU + # install even when $HasROCm is false (Adrenalin-only, no HIP runtime). + source = _SETUP_PS1_PATH.read_text(encoding = "utf-8") + assert "$HasNvidiaSmi -or $HasROCm -or $script:ROCmGfxArch" in source + + # The two assertions above pin the guard *text*. The tests below *execute* + # the real routing block from setup.sh / setup.ps1 and assert the resolved + # release repo, so a refactor that keeps the literal but breaks (or drops) + # the inferred-gfx -> fork decision is still caught. All inputs are faked -- + # no GPU, no ROCm tooling on PATH, no network. + + @staticmethod + def _resolve_setup_sh_repo( + host_machine, + nvidia_usable, + setup_gfx, + rocm_gfx_arch_env = "", + ): + """Run setup.sh's release-repo routing block under bash and return the + resolved _HELPER_RELEASE_REPO. PATH is emptied so the rocminfo/amd-smi/ + hipconfig/hipinfo `command -v` probes all miss (no ROCm tooling). + rocm_gfx_arch_env populates UNSLOTH_ROCM_GFX_ARCH for the env-forwarded + path that fires when no probe set _setup_gfx.""" + 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") + block = source[start:end] + assert "_HELPER_RELEASE_REPO" in block, "setup.sh routing anchors not found" + env = { + "PATH": "", # no rocminfo/amd-smi/hipconfig/hipinfo discoverable + "ROUTING_BLOCK": block, + "_HOST_SYSTEM": "Linux", + "_HOST_MACHINE": host_machine, + "_setup_nvidia_usable": "true" if nvidia_usable else "false", + "_setup_gfx": setup_gfx, + "UNSLOTH_ROCM_GFX_ARCH": rocm_gfx_arch_env, + } + result = subprocess.run( + [bash, "-c", 'eval "$ROUTING_BLOCK"; printf "%s" "$_HELPER_RELEASE_REPO"'], + capture_output = True, + text = True, + timeout = 30, + env = env, + ) + assert result.returncode == 0, result.stderr + return result.stdout.strip() + + def test_setup_sh_inferred_gfx_resolves_to_fork(self): + # No usable NVIDIA, no ROCm tooling on PATH, only a name-inferred gfx + # arch -> the host must still be treated as a GPU host and routed to the + # fork's per-gfx prebuilt, not ggml-org / a source build. Linux x64 and + # arm64 both go through the same 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): + # UNSLOTH_ROCM_GFX_ARCH set on a host where no probe fired (_setup_gfx + # empty, no usable NVIDIA, no ROCm tooling): setup.sh adopts the env arch + # and routes to the fork, same as the name-inference path. + 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): + # Guard against over-correcting the fix: a real CPU host (no usable GPU, + # no inferred gfx, no env override) must keep routing to ggml-org for the + # CPU prebuilt. + assert self._resolve_setup_sh_repo("x86_64", False, "") == "ggml-org/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.""" + import shutil + + pwsh = shutil.which("pwsh") + if pwsh is None: + 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 (") + ), + 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" + ) + result = subprocess.run( + [pwsh, "-NoProfile", "-Command", harness], + capture_output = True, + text = True, + timeout = 60, + ) + assert result.returncode == 0, result.stderr + return result.stdout.strip() + + def test_setup_ps1_inferred_gfx_resolves_to_fork(self): + # Adrenalin-only Windows host: $HasROCm is false (no HIP runtime) but a + # gfx arch was inferred -> route to the 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" + + +# TEST: _pick_rocm_gfx_target -- visible-device selection from rocminfo output. +# Honours CUDA_VISIBLE_DEVICES/HIP_VISIBLE_DEVICES so a mixed-arch host installs +# the prebuilt for the GPU actually selected, not GPU 0. + +_pick_rocm_gfx_target = prebuilt_mod._pick_rocm_gfx_target + + +def test_pick_rocm_gfx_target_honors_cuda_visible_devices(monkeypatch): + """AMD HIP honours CUDA_VISIBLE_DEVICES identically to HIP_VISIBLE_DEVICES; + on a gfx1151 + gfx1100 mixed host, CUDA_VISIBLE_DEVICES=1 must select gfx1100.""" + # Two GPUs; rocminfo reports each token twice (as in the real tool output). + probe_out = "gfx1151\ngfx1151\ngfx1100\ngfx1100" + monkeypatch.delenv("HIP_VISIBLE_DEVICES", raising = False) + monkeypatch.delenv("ROCR_VISIBLE_DEVICES", raising = False) + monkeypatch.setenv("CUDA_VISIBLE_DEVICES", "1") + assert _pick_rocm_gfx_target(probe_out) == "gfx1100" + + +def test_pick_rocm_gfx_target_cuda_visible_devices_minus_one_returns_none(monkeypatch): + """CUDA_VISIBLE_DEVICES=-1 means no GPU visible; resolver must return None.""" + probe_out = "gfx1151\ngfx1100" + monkeypatch.delenv("HIP_VISIBLE_DEVICES", raising = False) + monkeypatch.delenv("ROCR_VISIBLE_DEVICES", raising = False) + monkeypatch.setenv("CUDA_VISIBLE_DEVICES", "-1") + assert _pick_rocm_gfx_target(probe_out) is None + + +def test_pick_rocm_gfx_target_same_arch_multi_gpu(monkeypatch): + """Regression: [gfx1100, gfx1100, gfx1151] with HIP_VISIBLE_DEVICES=2 must + return gfx1151, not fall back to GPU 0 due to dict.fromkeys collapsing the + two gfx1100 entries into one and making index 2 out of range.""" + # Simulate rocminfo output for 3 GPUs (2x gfx1100 dGPU + 1x gfx1151 APU). + # Each GPU gets its own Agent section with a few token mentions. + probe_out = ( + "***\nAgent 1\n***\n gfx1100 some info\n gfx1100\n" + "***\nAgent 2\n***\n gfx1100 some info\n gfx1100\n" + "***\nAgent 3\n***\n gfx1151 some info\n gfx1151\n" + ) + monkeypatch.delenv("ROCR_VISIBLE_DEVICES", raising = False) + monkeypatch.delenv("CUDA_VISIBLE_DEVICES", raising = False) + monkeypatch.setenv("HIP_VISIBLE_DEVICES", "2") + assert _pick_rocm_gfx_target(probe_out) == "gfx1151" + # TEST: WSL ROCDXG fixes -- drop-in persistence + system-HIP-before-bundle diff --git a/tests/studio/install/test_selection_logic.py b/tests/studio/install/test_selection_logic.py index a4fc09a183..4e8beeb888 100644 --- a/tests/studio/install/test_selection_logic.py +++ b/tests/studio/install/test_selection_logic.py @@ -2963,6 +2963,29 @@ class TestPublishedRocmGfxSelection: is None ), unbuilt + def test_family_token_matches_family_bundle(self): + # The llama.cpp update path re-derives --rocm-gfx from the family-named + # marker asset, so it forwards a family token (gfx110X, lowercased to + # gfx110x by _normalize_forwarded_gfx), not a concrete arch. That must + # still select the family bundle instead of falling to a source build. + release = self._release("linux-rocm", "app-b9457-linux-x64-rocm") + for token in ("gfx110X", "gfx110x"): + choice = INSTALL_LLAMA_PREBUILT.published_rocm_choice_for_host( + release, self._host(token), "linux-rocm" + ) + assert choice is not None, token + assert choice.name == "app-b9457-linux-x64-rocm-gfx110X.tar.gz", token + + def test_windows_family_token_matches_family_bundle(self): + # The Windows update path forwards the same family token (gfx120X) for a + # windows-rocm bundle, so the family-label match must cover it too. + release = self._release("windows-rocm", "app-b9457-windows-x64-rocm") + choice = INSTALL_LLAMA_PREBUILT.published_rocm_choice_for_host( + release, self._host("gfx120x"), "windows-rocm" + ) + assert choice is not None + assert choice.name == "app-b9457-windows-x64-rocm-gfx120X.zip" + class TestPublishedMacosForkSelection: """macOS now routes to the fork (setup.sh), which ships