diff --git a/kernels/src/kernels/resolver.py b/kernels/src/kernels/resolver.py index 2d8d994e..8221f79c 100644 --- a/kernels/src/kernels/resolver.py +++ b/kernels/src/kernels/resolver.py @@ -6,7 +6,7 @@ from huggingface_hub.hf_api import HfApi from kernels_data import KernelDependency, KernelLocks, KernelPaths, Metadata -from kernels._versions import resolve_kernel_version +from kernels._versions import _get_available_versions, resolve_kernel_version from kernels.hf_hub import CACHE_DIR, _check_trust_remote_code from kernels.variants import ( Variant, @@ -123,8 +123,14 @@ def resolve_hub_kernel( ) variant, trace = resolve_variant(variants, backend) if variant is None: + suggestion = _latest_compatible_version_suggestion( + api=api, + repo_id=repo_id, + backend=backend, + ) raise FileNotFoundError( - f"Cannot find a build variant for this system in {repo_id} (revision: {revision}):\n\n{variants_trace_str(trace)}" + f"Cannot find a build variant for this system in {repo_id} (revision: {revision}):\n\n" + f"{variants_trace_str(trace)}{suggestion}" ) metadata_path = Path( @@ -144,6 +150,38 @@ def resolve_hub_kernel( return location +def _latest_compatible_version_suggestion( + *, + api: HfApi, + repo_id: str, + backend: str | None, +) -> str: + """ + This runs only after variant resolution has failed. Version discovery and + variant inspection involve additional Hub requests, so the lookup is + best-effort and hence, we never mask the original resolution error. + """ + try: + versions = _get_available_versions(repo_id, local_files_only=False) + if not versions: + return "" + + latest_version = max(versions) + ref = versions[latest_version] + variants = get_variants(api, repo_id=repo_id, revision=ref.ref) + compatible_variant, _ = resolve_variant(variants, backend) + if compatible_variant is not None: + return ( + f"\n\nHowever, version v{latest_version} of '{repo_id}' has a build compatible with your " + f"system ({compatible_variant.variant_str}). Consider upgrading to that version by specifying " + "the `version` argument." + ) + except Exception: + return "" + + return "" + + def resolve_hub_cache_kernel( api: HfApi, repo_id: str, diff --git a/kernels/tests/test_resolver.py b/kernels/tests/test_resolver.py index f599e313..9cadc4e9 100644 --- a/kernels/tests/test_resolver.py +++ b/kernels/tests/test_resolver.py @@ -4,6 +4,7 @@ from pathlib import Path import pytest +from huggingface_hub.hf_api import GitRefInfo from kernels_data import ( KernelDependency, KernelLock, @@ -14,6 +15,7 @@ Metadata, ) +import kernels.resolver as resolver_module from kernels._versions import resolve_version_spec_as_ref from kernels.hf_hub import _get_hf_api from kernels.install import install_kernel @@ -30,6 +32,7 @@ Resolver, SequentialResolver, _locked_revision, + resolve_hub_kernel, ) from kernels.variants import parse_variant @@ -277,6 +280,113 @@ def test_hub_resolver_no_matching_variant(api): ) +def test_resolve_hub_kernel_suggests_latest_compatible_version_for_unknown_revision(monkeypatch): + versions = { + version: GitRefInfo(name=f"v{version}", ref=f"refs/heads/v{version}", target_commit=str(version) * 40) + for version in (1, 2, 3) + } + checked_revisions = [] + + monkeypatch.setattr( + resolver_module, + "_get_available_versions", + lambda repo_id, *, local_files_only: versions, + ) + + def fake_get_variants(api, *, repo_id, revision): + checked_revisions.append(revision) + return [parse_variant("torch-cpu" if revision == "refs/heads/v3" else "torch-cuda")] + + monkeypatch.setattr(resolver_module, "get_variants", fake_get_variants) + + with pytest.raises(FileNotFoundError) as exc_info: + resolve_hub_kernel( + "test/kernel", + api=object(), + backend="cpu", + revision="unknown-revision", + ) + + message = str(exc_info.value) + assert "Cannot find a build variant for this system" in message + assert ( + "However, version v3 of 'test/kernel' has a build compatible with your system (torch-cpu). " + "Consider upgrading to that version by specifying the `version` argument." + ) in message + assert checked_revisions == ["unknown-revision", "refs/heads/v3"] + + +def test_resolve_hub_kernel_only_checks_latest_version(monkeypatch): + versions = { + version: GitRefInfo(name=f"v{version}", ref=f"refs/heads/v{version}", target_commit=str(version) * 40) + for version in (1, 2, 3) + } + checked_revisions = [] + monkeypatch.setattr( + resolver_module, + "_get_available_versions", + lambda repo_id, *, local_files_only: versions, + ) + + def fake_get_variants(api, *, repo_id, revision): + checked_revisions.append(revision) + return [parse_variant("torch-cpu" if revision == "refs/heads/v2" else "torch-cuda")] + + monkeypatch.setattr(resolver_module, "get_variants", fake_get_variants) + + with pytest.raises(FileNotFoundError) as exc_info: + resolve_hub_kernel("test/kernel", api=object(), backend="cpu", revision="locked-commit") + + assert "However, version" not in str(exc_info.value) + assert checked_revisions == ["locked-commit", "refs/heads/v3"] + + +def test_resolve_hub_kernel_preserves_original_error_when_latest_version_lookup_fails(monkeypatch): + versions = { + 1: GitRefInfo(name="v1", ref="refs/heads/v1", target_commit="1" * 40), + 2: GitRefInfo(name="v2", ref="refs/heads/v2", target_commit="2" * 40), + } + monkeypatch.setattr( + resolver_module, + "_get_available_versions", + lambda repo_id, *, local_files_only: versions, + ) + + def fake_get_variants(api, *, repo_id, revision): + if revision == "refs/heads/v2": + raise OSError("latest branch is temporarily unavailable") + return [parse_variant("torch-cuda")] + + monkeypatch.setattr(resolver_module, "get_variants", fake_get_variants) + + with pytest.raises(FileNotFoundError) as exc_info: + resolve_hub_kernel("test/kernel", api=object(), backend="cpu", revision="locked-commit") + + message = str(exc_info.value) + assert "Cannot find a build variant for this system" in message + assert "However, version" not in message + + +def test_resolve_hub_kernel_preserves_original_error_when_version_lookup_fails(monkeypatch): + monkeypatch.setattr( + resolver_module, + "_get_available_versions", + lambda repo_id, *, local_files_only: (_ for _ in ()).throw(OSError("offline")), + ) + monkeypatch.setattr( + resolver_module, + "get_variants", + lambda api, *, repo_id, revision: [parse_variant("torch-cuda")], + ) + + with pytest.raises(FileNotFoundError) as exc_info: + resolve_hub_kernel("test/kernel", api=object(), backend="cpu", revision="locked-commit") + + message = str(exc_info.value) + assert "Cannot find a build variant for this system" in message + assert "However, version" not in message + + def test_hub_cache_resolver_resolves_cached_kernel(api, installed_relu_cpu): location = HubCacheResolver(trust_remote_code=False).resolve( api=api, backend="cpu", kernel=_dep("kernels-community/relu", version=1)