From 84b42c92830bef8914f2bb12fe99cba208addb23 Mon Sep 17 00:00:00 2001 From: Leo Borcherding Date: Thu, 11 Jun 2026 22:14:22 -0500 Subject: [PATCH] fix: deduplicate lemonade ROCm prebuilt selection log (#6021) * fix: deduplicate lemonade ROCm prebuilt selection log resolve_lemonade_rocm_choice() is called twice per install (direct planner + resolve_upstream_asset_choice). The API fetch is already memoised via _fetch_lemonade_release_cached but the selection log lines were still emitted on both calls, printing the 'trying lemonade-sdk ROCm prebuilt' banner and hash-manifest NOTE twice. Add _lemonade_selection_logged set keyed on (gfx_target, asset_name) and guard the two log() calls behind a membership check so they print exactly once per process regardless of call count. Also extend the _clear_lemonade_release_cache test fixture to clear the new set between tests to prevent cross-test state bleed. Fixes #6020 * fix: write log() output to stdout to avoid PowerShell NativeCommandError On Windows, PowerShell treats any stderr output from a native process as an error record and prefixes it with 'python.exe :' and sets the ErrorId to NativeCommandError. Since log() wrote to sys.stderr, every [llama-prebuilt] status line triggered this, making normal progress output look like errors in the installer console. Switch log() to sys.stdout. The download progress bar (DownloadProgress) retains its stderr/tty logic unchanged -- that path is for interactive terminal rendering, not status logging. * fix: remove redundant 'or ""' in lemonade log_key host.rocm_gfx_target is already guaranteed truthy by the early return at the top of resolve_lemonade_rocm_choice. The fallback was dead code. * Keep resolver stdout machine-readable, route install logs to stdout log() sending everything to stdout breaks the resolver modes: setup.sh json.load()s the whole stdout, so one helper log line (network retry, release-tag scan) corrupts the parse and silently drops back to building "latest". Default log() to stderr and flip to stdout only on the install path, where PowerShell otherwise renders stderr as NativeCommandError noise. Also tighten the lemonade dedup comments. --------- Co-authored-by: Daniel Han --- .../test_lemonade_llamacpp_rocm_bins_mock.py | 9 +++- studio/install_llama_prebuilt.py | 45 +++++++++++++------ 2 files changed, 39 insertions(+), 15 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 57bbc84fb0..7c6c514b8f 100644 --- a/studio/backend/tests/test_lemonade_llamacpp_rocm_bins_mock.py +++ b/studio/backend/tests/test_lemonade_llamacpp_rocm_bins_mock.py @@ -32,14 +32,19 @@ if resolve_lemonade_rocm_choice is None or _LEMONADE_GFX_FAMILIES is None: @pytest.fixture(autouse = True) def _clear_lemonade_release_cache(): - """Prevent cross-test pollution of the lemonade release lru_cache when - future tests vary the fetch_json mock return value.""" + """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" diff --git a/studio/install_llama_prebuilt.py b/studio/install_llama_prebuilt.py index bd8c90dc9c..bed7a27a63 100644 --- a/studio/install_llama_prebuilt.py +++ b/studio/install_llama_prebuilt.py @@ -460,8 +460,14 @@ def is_busy_lock_error(exc: BaseException) -> bool: return False +# Status logs default to stderr so resolver modes keep stdout machine-readable +# (setup.sh json.load()s the whole stdout). main() flips this for the install +# path, where PowerShell otherwise renders stderr as NativeCommandError noise. +_LOG_TO_STDOUT = False + + def log(message: str) -> None: - print(f"[llama-prebuilt] {message}", file = sys.stderr) + print(f"[llama-prebuilt] {message}", file = sys.stdout if _LOG_TO_STDOUT else sys.stderr) def log_lines(lines: Iterable[str]) -> None: @@ -3928,6 +3934,12 @@ def _is_trusted_github_release_url(url: str, expected_repo: str) -> bool: 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. @@ -4034,18 +4046,22 @@ def resolve_lemonade_rocm_choice( # 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( - 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." - ) + # 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, @@ -6963,6 +6979,9 @@ def main() -> int: raise SystemExit( "install_llama_prebuilt.py: --install-dir is required unless --resolve-llama-tag, --resolve-install-tag, or --resolve-source-build is used" ) + # Install path only: route status logs to stdout (see _LOG_TO_STDOUT note). + global _LOG_TO_STDOUT + _LOG_TO_STDOUT = True install_prebuilt( install_dir = Path(args.install_dir).expanduser().resolve(), llama_tag = args.llama_tag,