From 152e950a94a4310af0bcaa5f387e49b9c5c14fe9 Mon Sep 17 00:00:00 2001 From: LeoBorcherding Date: Sat, 16 May 2026 15:37:45 -0500 Subject: [PATCH] fix(lemonade): pass resolved tag to lemonade resolver; add upstream HIP fallback; stub API in tests - direct_linux_release_plan: pass bundle.upstream_tag (not requested_tag) to resolve_lemonade_rocm_choice so a "latest" request doesn't mix a newer lemonade binary with an older planned unsloth release (Codex P2) - direct_upstream_release_plan: same fix on the Windows path (release_tag instead of requested_tag); also add the upstream HIP asset (llama--bin-win-hip-radeon-x64.zip) as a fallback between lemonade and CPU so unsupported GPUs or transient lemonade failures don't silently downgrade to CPU when an upstream ROCm prebuilt exists (Codex P2) - test file: stub fetch_json with a synthetic lemonade release payload so the suite is hermetic and not subject to GitHub API rate limits (Codex P1); add test_simple_policy_windows_hip_falls_back_to_upstream_when_lemonade_unavailable to cover the new HIP fallback path --- .../test_lemonade_llamacpp_rocm_bins_mock.py | 78 ++++++++++++++++--- studio/install_llama_prebuilt.py | 17 +++- 2 files changed, 82 insertions(+), 13 deletions(-) diff --git a/studio/backend/tests/test_lemonade_llamacpp_rocm_bins_mock.py b/studio/backend/tests/test_lemonade_llamacpp_rocm_bins_mock.py index 2723fcb49d..06964f4324 100644 --- a/studio/backend/tests/test_lemonade_llamacpp_rocm_bins_mock.py +++ b/studio/backend/tests/test_lemonade_llamacpp_rocm_bins_mock.py @@ -3,7 +3,9 @@ """Validates that the installer correctly resolves lemonade ROCm prebuilt assets. -Hits the real lemonade GitHub API with a faked HostInfo so no AMD GPU is needed. +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 @@ -11,6 +13,7 @@ from __future__ import annotations import importlib import sys from pathlib import Path +from unittest.mock import patch import pytest @@ -27,6 +30,27 @@ 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) +_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", @@ -97,9 +121,10 @@ def test_unknown_gpu_not_in_families(): ) def test_asset_resolves_for_known_gpu(gfx, os_prefix, windows): host = _make_rocm_host(gfx, windows = windows) - result = resolve_lemonade_rocm_choice( - host, os_prefix, "default", llama_tag = "latest" - ) + 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})" @@ -147,12 +172,13 @@ def _stub_unsloth_release(release_tag: str = "b9022") -> dict: ) def test_simple_policy_plans_lemonade_for_rocm_host(): host = _make_rocm_host("gfx1151") - plan = direct_linux_release_plan( - _stub_unsloth_release(), - host, - "unslothai/llama.cpp", - "latest", - ) + with patch.object(_mod, "fetch_json", return_value = _stub_lemonade_release()): + plan = direct_linux_release_plan( + _stub_unsloth_release(), + host, + "unslothai/llama.cpp", + "latest", + ) assert plan is not None, "ROCm host should not be skipped by simple-policy planner" kinds = [a.install_kind for a in plan.attempts] assert ( @@ -174,9 +200,39 @@ def test_simple_policy_plans_lemonade_for_windows_hip_host(): "name": "b9022", "assets": [], } - plan = direct_upstream_release_plan(release, host, "ggml-org/llama.cpp", "latest") + 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"simple-policy planner did not include a lemonade HIP attempt; got {kinds}" + + +@pytest.mark.skipif( + direct_upstream_release_plan is None, + reason = "simple-policy dispatcher not present on this branch", +) +def test_simple_policy_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" diff --git a/studio/install_llama_prebuilt.py b/studio/install_llama_prebuilt.py index f70d70dd9c..34f9c37b1b 100644 --- a/studio/install_llama_prebuilt.py +++ b/studio/install_llama_prebuilt.py @@ -1304,7 +1304,7 @@ def direct_linux_release_plan( # If the host glibc is too old, validate_prebuilt_attempts will fail # the lemonade attempt and we fall through to the source build. lemonade_choice = resolve_lemonade_rocm_choice( - host, "ubuntu", "linux-rocm", llama_tag = requested_tag + host, "ubuntu", "linux-rocm", llama_tag = bundle.upstream_tag ) if lemonade_choice is not None: attempts.append(lemonade_choice) @@ -1358,10 +1358,23 @@ def direct_upstream_release_plan( ) if host.has_rocm and not host.has_usable_nvidia: lemonade_choice = resolve_lemonade_rocm_choice( - host, "windows", "windows-hip", llama_tag = requested_tag + host, "windows", "windows-hip", llama_tag = release_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: + attempts.append( + AssetChoice( + repo = repo, + tag = release_tag, + name = hip_asset, + url = hip_url, + source_label = "upstream", + install_kind = "windows-hip", + ) + ) cpu_asset = f"llama-{release_tag}-bin-win-cpu-x64.zip" cpu_url = assets.get(cpu_asset) if cpu_url: