Installer: drop the lemonade ROCm fallback now the fork ships identical per-gfx prebuilts (#6225)
--------- Co-authored-by: Daniel Han <danielhanchen@gmail.com>
This commit is contained in:
parent
9d8daecf18
commit
5300c047b6
12 changed files with 284 additions and 880 deletions
12
install.ps1
12
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) {
|
||||
|
|
|
|||
|
|
@ -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/<tag> 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
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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-<version> 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."
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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]]" = [
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue