diff --git a/studio/install_whisper_prebuilt.py b/studio/install_whisper_prebuilt.py index 1c81ad6105..f1a7fce5e0 100644 --- a/studio/install_whisper_prebuilt.py +++ b/studio/install_whisper_prebuilt.py @@ -335,6 +335,31 @@ def artifacts_for_host( # ── Slim selection (paired with the installed llama.cpp ggml runtime) ── +def _llama_ggml_commit(tag: str) -> str | None: + """The ggml commit a llama.cpp fork tag was built against. Fork tags are + "b-mix-"; the ggml commit after "-mix-" fixes + the ggml ABI the slim whisper bundle links against, while the build number + only tracks upstream llama / fork PRs outside ggml. None when the tag has no + "-mix-" marker (then only an exact tag pairs).""" + marker = "-mix-" + idx = tag.rfind(marker) + end = idx + len(marker) + return tag[end:] if idx >= 0 and end < len(tag) else None + + +def llama_runtime_pairs(installed_tag: str, required_tag: Any) -> bool: + """Whether an installed llama tag can back a slim bundle needing required_tag. + An exact tag always pairs; so does a shared ggml commit, since a newer llama + build with the same ggml ships an ABI-identical runtime. requires_ggml_sonames + stays the real per-file ABI gate.""" + if not isinstance(required_tag, str): + return False + if installed_tag == required_tag: + return True + commit = _llama_ggml_commit(installed_tag) + return commit is not None and commit == _llama_ggml_commit(required_tag) + + def slim_pairing_for_artifact( artifact: dict[str, Any], host: HostInfo, backend: str ) -> tuple[Path, str] | None: @@ -348,10 +373,10 @@ def slim_pairing_for_artifact( return None llama_bin_dir, llama_tag, _profile = runtime requires_tag = artifact.get("requires_llama_tag") - if not isinstance(requires_tag, str) or requires_tag != llama_tag: + if not llama_runtime_pairs(llama_tag, requires_tag): log( f"slim_selection: {asset} skipped: installed llama tag {llama_tag!r} " - f"!= required {requires_tag!r}" + f"does not pair with required {requires_tag!r}" ) return None sonames = artifact.get("requires_ggml_sonames") @@ -466,11 +491,10 @@ def _slim_release_incompatibility(manifest: dict[str, Any], host: HostInfo) -> s for artifact in os_compatible if isinstance(artifact.get("requires_llama_tag"), str) } - if required_tags and installed_tag not in required_tags: + if required_tags and not any(llama_runtime_pairs(installed_tag, tag) for tag in required_tags): required_tag = sorted(required_tags)[0] return ( - f"slim bundle requires llama.cpp {required_tag}; " - f"installed llama.cpp is {installed_tag}" + f"slim bundle requires llama.cpp {required_tag}; installed llama.cpp is {installed_tag}" ) return None @@ -820,7 +844,7 @@ def selection_from_artifact( # A slim selection carries its pairing so the install wiring and marker know # which llama runtime provides the ggml libraries. runtime = installed_llama_runtime() - if runtime is None or runtime[1] != artifact.get("requires_llama_tag"): + if runtime is None or not llama_runtime_pairs(runtime[1], artifact.get("requires_llama_tag")): raise PrebuiltFallback( "the paired llama.cpp runtime changed underneath the slim whisper selection" ) diff --git a/tests/studio/install/test_install_whisper_prebuilt_logic.py b/tests/studio/install/test_install_whisper_prebuilt_logic.py index 7364ea454d..0a5541b27c 100644 --- a/tests/studio/install/test_install_whisper_prebuilt_logic.py +++ b/tests/studio/install/test_install_whisper_prebuilt_logic.py @@ -441,8 +441,9 @@ def test_main_forwards_requested_whisper_tags(tmp_path, monkeypatch): monkeypatch.setattr( M, "resolve_prebuilt", - lambda host, **kwargs: seen.update(kwargs) - or {"prebuilt_available": False, "repo": "unslothai/whisper.cpp"}, + lambda host, **kwargs: ( + seen.update(kwargs) or {"prebuilt_available": False, "repo": "unslothai/whisper.cpp"} + ), ) assert M.main(["--resolve-prebuilt", "v1.8.0", "--output-format", "json"]) == 0 assert seen["whisper_tag"] == "v1.8.0" @@ -807,6 +808,50 @@ def test_slim_release_tag_skew_has_distinct_compatibility_error(tmp_path, monkey M.select_artifact_with_fallback(manifest, _cuda_host(), "cuda") +# A newer llama build that keeps the same ggml commit as SLIM_LLAMA_TAG. +NEWER_LLAMA_TAG = "b10079-mix-fb3d4ca" + + +@pytest.mark.parametrize( + "installed,required,pairs", + [ + (SLIM_LLAMA_TAG, SLIM_LLAMA_TAG, True), # exact tag + (NEWER_LLAMA_TAG, SLIM_LLAMA_TAG, True), # newer build, same ggml commit + ("b10069-mix-0000000", SLIM_LLAMA_TAG, False), # same build, different ggml + (SLIM_LLAMA_TAG, None, False), # no requirement recorded + ("b10069", "b10069", True), # tag without -mix-, exact only + ("b10070", "b10069", False), # tag without -mix-, no shared key + ], +) +def test_llama_runtime_pairs_keys_on_ggml_commit(installed, required, pairs): + assert M.llama_runtime_pairs(installed, required) is pairs + + +def test_slim_pairs_across_llama_build_bump_with_same_ggml(tmp_path, monkeypatch): + # The live failure: the llama installer advances to a newer build that keeps + # the same ggml commit, so the slim bundle's paired runtime is ABI-identical + # and must still select rather than degrade to CPU or report unavailable. + bin_dir = _fake_llama_bin(tmp_path) + monkeypatch.setattr( + M, "installed_llama_runtime", lambda: (bin_dir, NEWER_LLAMA_TAG, "cuda13-newer") + ) + artifact, backend, used_fallback = M.select_artifact_with_fallback( + _slim_manifest(), _cuda_host(), "cuda" + ) + assert artifact["asset"] == SLIM_ASSET + assert backend == "cuda" and used_fallback is False + + +def test_slim_build_bump_same_ggml_is_not_a_compatibility_error(tmp_path, monkeypatch): + # A same-ggml build bump must not surface as a release incompatibility (the + # update path reports that as unavailable); only a real ggml skew does. + bin_dir = _fake_llama_bin(tmp_path) + monkeypatch.setattr( + M, "installed_llama_runtime", lambda: (bin_dir, NEWER_LLAMA_TAG, "cuda13-newer") + ) + assert M._slim_release_incompatibility(_slim_manifest(), _cuda_host()) is None + + def test_link_ggml_runtime_hardlinks_every_ggml_library(tmp_path): bin_dir = _fake_llama_bin(tmp_path) whisper_bin = tmp_path / "whisper.cpp" / "build" / "bin"