From 2b695a1b8a717e30a8f798db237938e4d9e37187 Mon Sep 17 00:00:00 2001 From: sayakpaul Date: Fri, 4 Sep 2026 15:50:37 +0530 Subject: [PATCH 1/3] feat: suggest compatible variants when available in newere versions --- kernels/src/kernels/cli/download.py | 11 ++- kernels/src/kernels/resolver.py | 72 +++++++++++++++++-- kernels/tests/test_resolver.py | 104 ++++++++++++++++++++++++++++ 3 files changed, 180 insertions(+), 7 deletions(-) diff --git a/kernels/src/kernels/cli/download.py b/kernels/src/kernels/cli/download.py index 7860a6ee..08a8acbb 100644 --- a/kernels/src/kernels/cli/download.py +++ b/kernels/src/kernels/cli/download.py @@ -1,6 +1,6 @@ import sys -from kernels_data import KernelLocks +from kernels_data import KernelLocks, KernelVersion from kernels.hf_hub import CACHE_DIR, _get_hf_api from kernels.resolver import _BYTECODE_IGNORE_PATTERNS, resolve_hub_kernel @@ -36,7 +36,14 @@ def download_kernels(args): revision=lock.commit, ) else: - location = resolve_hub_kernel(dep.repo_id, api=api, backend=None, revision=lock.commit) + version = dep.version.version if isinstance(dep.version, KernelVersion.Version) else None + location = resolve_hub_kernel( + dep.repo_id, + api=api, + backend=None, + revision=lock.commit, + version=version, + ) location.install(api=api) except FileNotFoundError as e: print(e, file=sys.stderr) diff --git a/kernels/src/kernels/resolver.py b/kernels/src/kernels/resolver.py index 2d8d994e..01e72e80 100644 --- a/kernels/src/kernels/resolver.py +++ b/kernels/src/kernels/resolver.py @@ -4,9 +4,9 @@ from huggingface_hub.errors import LocalEntryNotFoundError from huggingface_hub.hf_api import HfApi -from kernels_data import KernelDependency, KernelLocks, KernelPaths, Metadata +from kernels_data import KernelDependency, KernelLocks, KernelPaths, KernelVersion, 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, @@ -115,6 +115,7 @@ def resolve_hub_kernel( api: HfApi, backend: str | None, revision: str, + version: int | None = None, ) -> RemoteKernel: variants = get_variants( api, @@ -123,8 +124,16 @@ def resolve_hub_kernel( ) variant, trace = resolve_variant(variants, backend) if variant is None: + suggestion = _newer_compatible_version_suggestion( + api=api, + repo_id=repo_id, + revision=revision, + version=version, + 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 +153,57 @@ def resolve_hub_kernel( return location +def _newer_compatible_version_suggestion( + *, + api: HfApi, + repo_id: str, + revision: str, + version: int | None, + 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 version is None: + version = next( + ( + candidate + for candidate, ref in versions.items() + if revision in (ref.name, ref.ref, ref.target_commit) + ), + None, + ) + + if version is None: + return "" + + newer_versions = [candidate for candidate in versions if candidate > version] + newer_versions.sort(reverse=True) + + for newer_version in newer_versions: + ref = versions[newer_version] + try: + variants = get_variants(api, repo_id=repo_id, revision=ref.ref) + compatible_variant, _ = resolve_variant(variants, backend) + except Exception: + continue + if compatible_variant is not None: + return ( + f"\n\nHowever, version v{newer_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, @@ -243,7 +303,8 @@ def resolve(self, *, api: HfApi, backend: str | None, kernel: KernelDependency) revision = resolve_kernel_version(kernel, local_files_only=False) # Get the kernel metadata for the revision. - return resolve_hub_kernel(kernel.repo_id, api=api, revision=revision, backend=backend) + version = kernel.version.version if isinstance(kernel.version, KernelVersion.Version) else None + return resolve_hub_kernel(kernel.repo_id, api=api, revision=revision, version=version, backend=backend) @dataclass @@ -303,7 +364,8 @@ def resolve(self, *, api: HfApi, backend: str | None, kernel: KernelDependency) revision = _locked_revision(self.kernel_locks, kernel) # Get the kernel metadata for the revision. - return resolve_hub_kernel(kernel.repo_id, api=api, revision=revision, backend=backend) + version = kernel.version.version if isinstance(kernel.version, KernelVersion.Version) else None + return resolve_hub_kernel(kernel.repo_id, api=api, revision=revision, version=version, backend=backend) @dataclass diff --git a/kernels/tests/test_resolver.py b/kernels/tests/test_resolver.py index f599e313..4c78f9a7 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,107 @@ def test_hub_resolver_no_matching_variant(api): ) +def test_resolve_hub_kernel_suggests_newest_compatible_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=versions[1].target_commit, + version=1, + ) + + message = str(exc_info.value) + assert "Cannot find a build variant for this system" in message + assert ( + "However, version v2 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 == [versions[1].target_commit, "refs/heads/v3", "refs/heads/v2"] + + +def test_resolve_hub_kernel_continues_when_newer_version_lookup_fails(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) + } + 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/v3": + raise OSError("branch is temporarily unavailable") + 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, match="However, version v2"): + resolve_hub_kernel("test/kernel", api=object(), backend="cpu", revision="v1", version=1) + + +def test_resolve_hub_kernel_infers_version_from_revision(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, + ) + monkeypatch.setattr( + resolver_module, + "get_variants", + lambda api, *, repo_id, revision: [ + parse_variant("torch-cpu" if revision == "refs/heads/v2" else "torch-cuda") + ], + ) + + with pytest.raises(FileNotFoundError, match="However, version v2"): + resolve_hub_kernel("test/kernel", api=object(), backend="cpu", revision="v1") + + +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="v1", version=1) + + 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) From 033b6fb27bdd38b2e002549a0fc9bea75e0a2cac Mon Sep 17 00:00:00 2001 From: sayakpaul Date: Fri, 4 Sep 2026 17:47:05 +0530 Subject: [PATCH 2/3] generalize. --- kernels/src/kernels/cli/download.py | 11 ++---- kernels/src/kernels/resolver.py | 56 +++++++++-------------------- kernels/tests/test_resolver.py | 50 ++++++++++++++------------ 3 files changed, 46 insertions(+), 71 deletions(-) diff --git a/kernels/src/kernels/cli/download.py b/kernels/src/kernels/cli/download.py index 08a8acbb..7860a6ee 100644 --- a/kernels/src/kernels/cli/download.py +++ b/kernels/src/kernels/cli/download.py @@ -1,6 +1,6 @@ import sys -from kernels_data import KernelLocks, KernelVersion +from kernels_data import KernelLocks from kernels.hf_hub import CACHE_DIR, _get_hf_api from kernels.resolver import _BYTECODE_IGNORE_PATTERNS, resolve_hub_kernel @@ -36,14 +36,7 @@ def download_kernels(args): revision=lock.commit, ) else: - version = dep.version.version if isinstance(dep.version, KernelVersion.Version) else None - location = resolve_hub_kernel( - dep.repo_id, - api=api, - backend=None, - revision=lock.commit, - version=version, - ) + location = resolve_hub_kernel(dep.repo_id, api=api, backend=None, revision=lock.commit) location.install(api=api) except FileNotFoundError as e: print(e, file=sys.stderr) diff --git a/kernels/src/kernels/resolver.py b/kernels/src/kernels/resolver.py index 01e72e80..8221f79c 100644 --- a/kernels/src/kernels/resolver.py +++ b/kernels/src/kernels/resolver.py @@ -4,7 +4,7 @@ from huggingface_hub.errors import LocalEntryNotFoundError from huggingface_hub.hf_api import HfApi -from kernels_data import KernelDependency, KernelLocks, KernelPaths, KernelVersion, Metadata +from kernels_data import KernelDependency, KernelLocks, KernelPaths, Metadata from kernels._versions import _get_available_versions, resolve_kernel_version from kernels.hf_hub import CACHE_DIR, _check_trust_remote_code @@ -115,7 +115,6 @@ def resolve_hub_kernel( api: HfApi, backend: str | None, revision: str, - version: int | None = None, ) -> RemoteKernel: variants = get_variants( api, @@ -124,11 +123,9 @@ def resolve_hub_kernel( ) variant, trace = resolve_variant(variants, backend) if variant is None: - suggestion = _newer_compatible_version_suggestion( + suggestion = _latest_compatible_version_suggestion( api=api, repo_id=repo_id, - revision=revision, - version=version, backend=backend, ) raise FileNotFoundError( @@ -153,12 +150,10 @@ def resolve_hub_kernel( return location -def _newer_compatible_version_suggestion( +def _latest_compatible_version_suggestion( *, api: HfApi, repo_id: str, - revision: str, - version: int | None, backend: str | None, ) -> str: """ @@ -168,36 +163,19 @@ def _newer_compatible_version_suggestion( """ try: versions = _get_available_versions(repo_id, local_files_only=False) - - if version is None: - version = next( - ( - candidate - for candidate, ref in versions.items() - if revision in (ref.name, ref.ref, ref.target_commit) - ), - None, - ) - - if version is None: + if not versions: return "" - newer_versions = [candidate for candidate in versions if candidate > version] - newer_versions.sort(reverse=True) - - for newer_version in newer_versions: - ref = versions[newer_version] - try: - variants = get_variants(api, repo_id=repo_id, revision=ref.ref) - compatible_variant, _ = resolve_variant(variants, backend) - except Exception: - continue - if compatible_variant is not None: - return ( - f"\n\nHowever, version v{newer_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." - ) + 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 "" @@ -303,8 +281,7 @@ def resolve(self, *, api: HfApi, backend: str | None, kernel: KernelDependency) revision = resolve_kernel_version(kernel, local_files_only=False) # Get the kernel metadata for the revision. - version = kernel.version.version if isinstance(kernel.version, KernelVersion.Version) else None - return resolve_hub_kernel(kernel.repo_id, api=api, revision=revision, version=version, backend=backend) + return resolve_hub_kernel(kernel.repo_id, api=api, revision=revision, backend=backend) @dataclass @@ -364,8 +341,7 @@ def resolve(self, *, api: HfApi, backend: str | None, kernel: KernelDependency) revision = _locked_revision(self.kernel_locks, kernel) # Get the kernel metadata for the revision. - version = kernel.version.version if isinstance(kernel.version, KernelVersion.Version) else None - return resolve_hub_kernel(kernel.repo_id, api=api, revision=revision, version=version, backend=backend) + return resolve_hub_kernel(kernel.repo_id, api=api, revision=revision, backend=backend) @dataclass diff --git a/kernels/tests/test_resolver.py b/kernels/tests/test_resolver.py index 4c78f9a7..78e256a7 100644 --- a/kernels/tests/test_resolver.py +++ b/kernels/tests/test_resolver.py @@ -280,7 +280,7 @@ def test_hub_resolver_no_matching_variant(api): ) -def test_resolve_hub_kernel_suggests_newest_compatible_version(monkeypatch): +def test_resolve_hub_kernel_suggests_latest_compatible_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) @@ -295,7 +295,7 @@ def test_resolve_hub_kernel_suggests_newest_compatible_version(monkeypatch): 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")] + return [parse_variant("torch-cpu" if revision == "refs/heads/v3" else "torch-cuda")] monkeypatch.setattr(resolver_module, "get_variants", fake_get_variants) @@ -304,24 +304,24 @@ def fake_get_variants(api, *, repo_id, revision): "test/kernel", api=object(), backend="cpu", - revision=versions[1].target_commit, - version=1, + revision="locked-commit", ) message = str(exc_info.value) assert "Cannot find a build variant for this system" in message assert ( - "However, version v2 of 'test/kernel' has a build compatible with your system (torch-cpu). " + "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 == [versions[1].target_commit, "refs/heads/v3", "refs/heads/v2"] + assert checked_revisions == ["locked-commit", "refs/heads/v3"] -def test_resolve_hub_kernel_continues_when_newer_version_lookup_fails(monkeypatch): +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", @@ -329,17 +329,19 @@ def test_resolve_hub_kernel_continues_when_newer_version_lookup_fails(monkeypatc ) def fake_get_variants(api, *, repo_id, revision): - if revision == "refs/heads/v3": - raise OSError("branch is temporarily unavailable") + 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, match="However, version v2"): - resolve_hub_kernel("test/kernel", api=object(), backend="cpu", revision="v1", version=1) + 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_infers_version_from_revision(monkeypatch): +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), @@ -349,16 +351,20 @@ def test_resolve_hub_kernel_infers_version_from_revision(monkeypatch): "_get_available_versions", lambda repo_id, *, local_files_only: versions, ) - monkeypatch.setattr( - resolver_module, - "get_variants", - lambda api, *, repo_id, revision: [ - parse_variant("torch-cpu" if revision == "refs/heads/v2" else "torch-cuda") - ], - ) - with pytest.raises(FileNotFoundError, match="However, version v2"): - resolve_hub_kernel("test/kernel", api=object(), backend="cpu", revision="v1") + 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): @@ -374,7 +380,7 @@ def test_resolve_hub_kernel_preserves_original_error_when_version_lookup_fails(m ) with pytest.raises(FileNotFoundError) as exc_info: - resolve_hub_kernel("test/kernel", api=object(), backend="cpu", revision="v1", version=1) + 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 From 14d36ffaeeb355b61fada8147d38b135dde4e169 Mon Sep 17 00:00:00 2001 From: sayakpaul Date: Fri, 4 Sep 2026 17:51:59 +0530 Subject: [PATCH 3/3] fix test even when the current revision is unknown --- kernels/tests/test_resolver.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/kernels/tests/test_resolver.py b/kernels/tests/test_resolver.py index 78e256a7..9cadc4e9 100644 --- a/kernels/tests/test_resolver.py +++ b/kernels/tests/test_resolver.py @@ -280,7 +280,7 @@ def test_hub_resolver_no_matching_variant(api): ) -def test_resolve_hub_kernel_suggests_latest_compatible_version(monkeypatch): +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) @@ -304,7 +304,7 @@ def fake_get_variants(api, *, repo_id, revision): "test/kernel", api=object(), backend="cpu", - revision="locked-commit", + revision="unknown-revision", ) message = str(exc_info.value) @@ -313,7 +313,7 @@ def fake_get_variants(api, *, repo_id, revision): "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 == ["locked-commit", "refs/heads/v3"] + assert checked_revisions == ["unknown-revision", "refs/heads/v3"] def test_resolve_hub_kernel_only_checks_latest_version(monkeypatch):