From f938ba65fe3575e85d62bc762ccc2d34d3a4ff2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20de=20Kok?= Date: Wed, 2 Sep 2026 15:43:42 +0000 Subject: [PATCH 1/5] Move `Validation` protocol and impls to `validate` module --- kernels/src/kernels/deps.py | 102 +------------ kernels/src/kernels/layer/func.py | 2 +- kernels/src/kernels/layer/layer.py | 2 +- kernels/src/kernels/load.py | 14 +- kernels/src/kernels/validate.py | 102 +++++++++++++ kernels/tests/conftest.py | 54 +++++++ kernels/tests/test_archs.py | 126 ---------------- kernels/tests/test_minver.py | 77 ---------- kernels/tests/test_validate.py | 141 ++++++++++++++++++ .../get-kernel-check/get-kernel-check-hook.py | 2 +- 10 files changed, 312 insertions(+), 310 deletions(-) create mode 100644 kernels/src/kernels/validate.py delete mode 100644 kernels/tests/test_minver.py create mode 100644 kernels/tests/test_validate.py diff --git a/kernels/src/kernels/deps.py b/kernels/src/kernels/deps.py index 9486c3ede..e5d57b789 100644 --- a/kernels/src/kernels/deps.py +++ b/kernels/src/kernels/deps.py @@ -1,23 +1,22 @@ from contextvars import ContextVar from dataclasses import dataclass from types import ModuleType -from typing import Generic, Protocol, TypeVar +from typing import TYPE_CHECKING, Generic, TypeVar from huggingface_hub.hf_api import HfApi -from kernels_data import KernelDependency, KernelVersion, Version -from packaging.version import InvalidVersion, parse +from kernels_data import KernelDependency, KernelVersion -from kernels.archs import _check_arch_incompatibility -from kernels.backends import _backend from kernels.hf_hub import RepoInfo from kernels.importer import _import_from_path -from kernels.python_deps import validate_dependencies from kernels.resolver import ( LocalKernel, RemoteKernel, Resolver, ) +if TYPE_CHECKING: + from kernels.validate import Validator + # Default state is `None`, to signal that we are not in a kernel # loading context. _KERNEL_DEPS: ContextVar[dict[str, ModuleType] | None] = ContextVar("_KERNEL_DEPS", default=None) @@ -83,97 +82,6 @@ def validate( node.validate(validator) -class Validator(Protocol): - """Validator for a node in a resolved kernel dependency tree.""" - - def validate(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> None: ... - - -class DependencyValidator: - """Validate the Python dependencies of a kernel tree node.""" - - def validate(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> None: - validate_dependencies( - tree.location.metadata.name.python_name, - tree.location.metadata.python_depends, - _backend(), - ) - - -class ArchValidator: - """Validate architecture compatibility for a kernel tree node.""" - - def validate(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> None: - _check_arch_incompatibility(tree.location.metadata, tree.location.variant_str) - - -def _installed_version() -> Version | None: - """The installed `kernels` version as a numeric version. - - Pre-release and development suffixes are stripped, since kernel metadata - only records release versions. Without stripping, a development version - like `0.17.0.dev0` would compare as older than the `0.17.0` it implements. - - Returns `None` if the installed version is not a PEP 440 version, which - can happen for versions derived from a VCS checkout. Such a version cannot - be compared, and this check must never make a kernel fail to load. - """ - # Avoid an import cycle. - from kernels import __version__ - - try: - release = parse(__version__).release - except InvalidVersion: - return None - - # packaging < 22 returns a LegacyVersion with `release = None` for - # non-PEP 440 versions instead of raising `InvalidVersion`. - if release is None: - return None - - return Version.from_str(".".join(str(part) for part in release)) - - -class MinverValidator: - """Validate that the installed `kernels` library meets the minimum version - required by a kernel.""" - - def validate(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> None: - metadata = tree.location.metadata - minver = metadata.kernels_minver - if minver is None: - return - - installed = _installed_version() - if installed is not None and installed < minver: - # Report the verbatim installed version, not the normalized one used - # for comparison, so the message matches what `pip show` reports. - # Avoid an import cycle. - from kernels import __version__ - - raise RuntimeError( - f"Kernel '{metadata.name}' variant '{tree.location.variant_str}' requires " - f"kernels>={minver}, but version {__version__} is installed. " - "Upgrade with: pip install --upgrade kernels" - ) - - -@dataclass -class AllValidator: - """Apply multiple validators to a kernel dependency tree.""" - - validators: list[Validator] - - def validate(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> None: - for validator in self.validators: - validator.validate(tree=tree) - - -def default_validators() -> list[Validator]: - """The validators that are applied to every kernel dependency tree.""" - return [DependencyValidator(), MinverValidator()] - - LoadCache = dict[KernelDependency, DepTreeNode] diff --git a/kernels/src/kernels/layer/func.py b/kernels/src/kernels/layer/func.py index dfd47c726..ac84ab817 100644 --- a/kernels/src/kernels/layer/func.py +++ b/kernels/src/kernels/layer/func.py @@ -10,7 +10,6 @@ from kernels.resolver import LockedHubCacheResolver, LockedHubResolver from .._versions import select_revision_or_version -from ..deps import AllValidator, default_validators from ..hf_hub import _get_hf_api from ..load import ( get_kernel, @@ -21,6 +20,7 @@ get_caller_locked_kernel_revision, get_locked_kernel_revision, ) +from ..validate import AllValidator, default_validators from .layer import _create_func_module, use_kernel_forward_from_hub from .repos import RepositoryProtocol diff --git a/kernels/src/kernels/layer/layer.py b/kernels/src/kernels/layer/layer.py index 86d13e8f5..3eb1012d8 100644 --- a/kernels/src/kernels/layer/layer.py +++ b/kernels/src/kernels/layer/layer.py @@ -15,7 +15,6 @@ from kernels.resolver import LockedHubCacheResolver, LockedHubResolver from .._versions import select_revision_or_version -from ..deps import AllValidator, default_validators from ..hf_hub import _get_hf_api from ..load import ( get_kernel, @@ -26,6 +25,7 @@ get_caller_locked_kernel_revision, get_locked_kernel_revision, ) +from ..validate import AllValidator, default_validators from .device import Device from .globals import _DISABLE_KERNEL_MAPPING, _KERNEL_MAPPING from .mode import Mode diff --git a/kernels/src/kernels/load.py b/kernels/src/kernels/load.py index a6ed7193c..e67c06699 100644 --- a/kernels/src/kernels/load.py +++ b/kernels/src/kernels/load.py @@ -7,13 +7,7 @@ from kernels_data import KernelDependency, KernelVersion from kernels._versions import revision_or_version -from kernels.deps import ( - AllValidator, - ArchValidator, - Validator, - default_validators, - resolve_kernel_tree, -) +from kernels.deps import resolve_kernel_tree from kernels.hf_hub import _get_hf_api from kernels.locking import ( get_caller_locked_kernel_revision, @@ -29,6 +23,12 @@ Resolver, SequentialResolver, ) +from kernels.validate import ( + AllValidator, + ArchValidator, + Validator, + default_validators, +) def _get_local_kernel_overrides() -> Resolver: diff --git a/kernels/src/kernels/validate.py b/kernels/src/kernels/validate.py new file mode 100644 index 000000000..db1d59ce7 --- /dev/null +++ b/kernels/src/kernels/validate.py @@ -0,0 +1,102 @@ +from dataclasses import dataclass +from typing import Protocol + +from kernels_data import Version +from packaging.version import InvalidVersion, parse + +from kernels.archs import _check_arch_incompatibility +from kernels.backends import _backend +from kernels.deps import DepTreeNode +from kernels.python_deps import validate_dependencies +from kernels.resolver import LocalKernel, RemoteKernel + + +class Validator(Protocol): + """Validator for a node in a resolved kernel dependency tree.""" + + def validate(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> None: ... + + +class DependencyValidator: + """Validate the Python dependencies of a kernel tree node.""" + + def validate(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> None: + validate_dependencies( + tree.location.metadata.name.python_name, + tree.location.metadata.python_depends, + _backend(), + ) + + +class ArchValidator: + """Validate architecture compatibility for a kernel tree node.""" + + def validate(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> None: + _check_arch_incompatibility(tree.location.metadata, tree.location.variant_str) + + +def _installed_version() -> Version | None: + """The installed `kernels` version as a numeric version. + + Pre-release and development suffixes are stripped, since kernel metadata + only records release versions. Without stripping, a development version + like `0.17.0.dev0` would compare as older than the `0.17.0` it implements. + + Returns `None` if the installed version is not a PEP 440 version, which + can happen for versions derived from a VCS checkout. Such a version cannot + be compared, and this check must never make a kernel fail to load. + """ + # Avoid an import cycle. + from kernels import __version__ + + try: + release = parse(__version__).release + except InvalidVersion: + return None + + # packaging < 22 returns a LegacyVersion with `release = None` for + # non-PEP 440 versions instead of raising `InvalidVersion`. + if release is None: + return None + + return Version.from_str(".".join(str(part) for part in release)) + + +class MinverValidator: + """Validate that the installed `kernels` library meets the minimum version + required by a kernel.""" + + def validate(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> None: + metadata = tree.location.metadata + minver = metadata.kernels_minver + if minver is None: + return + + installed = _installed_version() + if installed is not None and installed < minver: + # Report the verbatim installed version, not the normalized one used + # for comparison, so the message matches what `pip show` reports. + # Avoid an import cycle. + from kernels import __version__ + + raise RuntimeError( + f"Kernel '{metadata.name}' variant '{tree.location.variant_str}' requires " + f"kernels>={minver}, but version {__version__} is installed. " + "Upgrade with: pip install --upgrade kernels" + ) + + +@dataclass +class AllValidator: + """Apply multiple validators to a kernel dependency tree.""" + + validators: list[Validator] + + def validate(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> None: + for validator in self.validators: + validator.validate(tree=tree) + + +def default_validators() -> list[Validator]: + """The validators that are applied to every kernel dependency tree.""" + return [DependencyValidator(), MinverValidator()] diff --git a/kernels/tests/conftest.py b/kernels/tests/conftest.py index 513261ffd..8605147c9 100644 --- a/kernels/tests/conftest.py +++ b/kernels/tests/conftest.py @@ -1,9 +1,14 @@ import importlib.util +import json import sys +from pathlib import Path import pytest +from kernels_data import Metadata from kernels.backends import _get_torch_privateuse_backend_name +from kernels.deps import DepTreeNode +from kernels.resolver import LocalKernel try: import torch @@ -61,6 +66,55 @@ def device(): return "cpu" +@pytest.fixture +def make_metadata(): + def _make_metadata(backend_type: str, archs: list[str] | None, kernels_minver: str | None = None) -> Metadata: + metadata = { + "name": "test-kernel", + "id": "test_id", + "version": 1, + "license": "mit", + "python-depends": [], + "backend": {"type": backend_type, "archs": archs}, + } + if kernels_minver is not None: + metadata["kernels-minver"] = kernels_minver + return Metadata.from_bytes(json.dumps(metadata).encode("utf-8")) + + return _make_metadata + + +@pytest.fixture +def make_tree(): + def _make_tree(metadata: Metadata, variant: str = "test-variant") -> DepTreeNode: + return DepTreeNode( + location=LocalKernel(Path(variant), metadata), + deps={}, + ) + + return _make_tree + + +@pytest.fixture +def fake_cuda_device(monkeypatch): + monkeypatch.setattr(torch.version, "cuda", "12.8", raising=False) + monkeypatch.setattr(torch.version, "hip", None, raising=False) + monkeypatch.setattr(torch.cuda, "is_available", lambda: True) + monkeypatch.setattr(torch.cuda, "get_device_capability", lambda device=None: (10, 0)) + + +@pytest.fixture +def fake_rocm_device(monkeypatch): + class FakeProperties: + gcnArchName = "gfx90a:sramecc+:xnack-" + + monkeypatch.setattr(torch.version, "cuda", None, raising=False) + monkeypatch.setattr(torch.version, "hip", "6.4.0", raising=False) + monkeypatch.setattr(torch.cuda, "is_available", lambda: True) + monkeypatch.setattr(torch.cuda, "current_device", lambda: 0) + monkeypatch.setattr(torch.cuda, "get_device_properties", lambda device: FakeProperties()) + + def pytest_runtest_setup(item): if "torch_only" in item.keywords and torch is None: pytest.skip("skipping CUDA Torch-only test on host without Torch") diff --git a/kernels/tests/test_archs.py b/kernels/tests/test_archs.py index 6d8a04c91..2c7cde65f 100644 --- a/kernels/tests/test_archs.py +++ b/kernels/tests/test_archs.py @@ -1,37 +1,9 @@ -import json -from pathlib import Path - import pytest import torch from kernels_data import Metadata -import kernels.deps as deps_module from kernels import get_kernel, has_kernel, install_kernel from kernels.archs import _supports_cuda_capability -from kernels.deps import ArchValidator, DepTreeNode -from kernels.resolver import LocalKernel - - -def make_metadata(backend_type: str, archs: list[str] | None) -> Metadata: - return Metadata.from_bytes( - json.dumps( - { - "name": "test-kernel", - "id": "test_id", - "version": 1, - "license": "mit", - "python-depends": [], - "backend": {"type": backend_type, "archs": archs}, - } - ).encode("utf-8") - ) - - -def make_tree(metadata: Metadata, variant: str = "test-variant") -> DepTreeNode: - return DepTreeNode( - location=LocalKernel(Path(variant), metadata), - deps={}, - ) @pytest.mark.parametrize( @@ -77,104 +49,6 @@ def test_supports_cuda_capability(archs, capability, supported): assert _supports_cuda_capability(archs, capability) == supported -@pytest.fixture -def fake_cuda_device(monkeypatch): - monkeypatch.setattr(torch.version, "cuda", "12.8", raising=False) - monkeypatch.setattr(torch.version, "hip", None, raising=False) - monkeypatch.setattr(torch.cuda, "is_available", lambda: True) - monkeypatch.setattr(torch.cuda, "get_device_capability", lambda device=None: (10, 0)) - - -@pytest.fixture -def fake_rocm_device(monkeypatch): - class FakeProperties: - gcnArchName = "gfx90a:sramecc+:xnack-" - - monkeypatch.setattr(torch.version, "cuda", None, raising=False) - monkeypatch.setattr(torch.version, "hip", "6.4.0", raising=False) - monkeypatch.setattr(torch.cuda, "is_available", lambda: True) - monkeypatch.setattr(torch.cuda, "current_device", lambda: 0) - monkeypatch.setattr(torch.cuda, "get_device_properties", lambda device: FakeProperties()) - - -def test_cuda_incompatible_arch_is_rejected(fake_cuda_device): - with pytest.raises(RuntimeError) as exc_info: - ArchValidator().validate(tree=make_tree(make_metadata("cuda", ["8.0", "9.0a"]))) - assert "test-variant" in str(exc_info.value) - assert "CUDA capability 10.0" in str(exc_info.value) - assert "8.0, 9.0a" in str(exc_info.value) - - -def test_cuda_compatible_arch_is_accepted(fake_cuda_device): - for archs in (["8.0", "10.0"], ["10.0a"], ["10.0f"]): - ArchValidator().validate(tree=make_tree(make_metadata("cuda", archs))) - - -def test_noarch_build_is_accepted(fake_cuda_device): - # Backends that support archs (e.g. CUDA) can have builds that do not - # declare any (noarch kernels, e.g. pure Triton builds). Such builds are - # never rejected. - ArchValidator().validate(tree=make_tree(make_metadata("cuda", None))) - ArchValidator().validate(tree=make_tree(make_metadata("cuda", []))) - - -def test_rocm_arch_check(fake_rocm_device): - ArchValidator().validate(tree=make_tree(make_metadata("rocm", ["gfx90a", "gfx942"]))) - ArchValidator().validate(tree=make_tree(make_metadata("rocm", None))) - with pytest.raises(RuntimeError) as exc_info: - ArchValidator().validate(tree=make_tree(make_metadata("rocm", ["gfx942"]))) - assert "ROCm arch gfx90a" in str(exc_info.value) - assert "gfx942" in str(exc_info.value) - - -def test_check_skipped_without_device(monkeypatch): - monkeypatch.setattr(torch.version, "cuda", "12.8", raising=False) - monkeypatch.setattr(torch.cuda, "is_available", lambda: False) - ArchValidator().validate(tree=make_tree(make_metadata("cuda", ["8.0"]))) - - -def test_check_skipped_for_backends_without_archs(fake_cuda_device): - # Archs of other backends cannot be checked against the current device. - ArchValidator().validate(tree=make_tree(make_metadata("cpu", None))) - ArchValidator().validate(tree=make_tree(make_metadata("metal", ["applegpu_g13"]))) - - -def test_arch_validator_checks_entire_dependency_tree(monkeypatch): - tree = DepTreeNode( - location=LocalKernel(Path("root-variant"), make_metadata("cuda", ["8.0"])), - deps={ - "test/dependency": DepTreeNode( - location=LocalKernel(Path("dependency-variant"), make_metadata("cuda", ["9.0"])), - deps={}, - ) - }, - ) - validated = [] - - monkeypatch.setattr( - deps_module, - "_check_arch_incompatibility", - lambda metadata, variant: validated.append((metadata.backend.archs, variant)), - ) - - tree.validate(ArchValidator()) - - assert validated == [ - (["8.0"], "root-variant"), - (["9.0"], "dependency-variant"), - ] - - -def test_issue_707_fa3_on_b200(fake_cuda_device): - # https://github.com/huggingface/kernels/issues/707: flash-attn3 only - # declares sm_80/sm_90a archs, but loading it on a B200 (capability 10.0) - # succeeded and the first launch exited the process. The declared archs - # must be rejected for this device. - metadata = make_metadata("cuda", ["8.0", "9.0a"]) - with pytest.raises(RuntimeError, match="does not support the current device"): - ArchValidator().validate(tree=make_tree(metadata)) - - @pytest.mark.cuda_only def test_get_kernel_rejects_unsupported_capability(monkeypatch): variant_path = install_kernel("kernels-community/relu", version=1) diff --git a/kernels/tests/test_minver.py b/kernels/tests/test_minver.py deleted file mode 100644 index 7b1ff995b..000000000 --- a/kernels/tests/test_minver.py +++ /dev/null @@ -1,77 +0,0 @@ -import json -import re -from pathlib import Path - -import pytest -from kernels_data import Metadata, Version - -import kernels -from kernels.deps import DepTreeNode, MinverValidator, _installed_version -from kernels.resolver import LocalKernel - - -def _make_tree(minver): - metadata_dict = { - "id": "activation_1_cuda", - "name": "activation", - "version": 1, - "license": "Apache-2.0", - "python-depends": ["torch"], - "backend": {"type": "cuda"}, - } - if minver is not None: - metadata_dict["kernels-minver"] = minver - metadata = Metadata.from_bytes(json.dumps(metadata_dict).encode("utf-8")) - variant_path = Path("build") / "torch28-cxx11-cu128-x86_64-linux" - return DepTreeNode(location=LocalKernel(variant_path, metadata), deps={}) - - -@pytest.mark.parametrize("minver", [None, "0.0.1"]) -def test_no_error_when_minver_met(minver): - MinverValidator().validate(tree=_make_tree(minver)) - - -def test_no_error_for_dev_version_of_required_release(monkeypatch): - # A development version implements the release it leads up to, so - # `0.17.0.dev0` must satisfy a `0.17.0` requirement. - monkeypatch.setattr(kernels, "__version__", "0.17.0.dev0") - MinverValidator().validate(tree=_make_tree("0.17.0")) - - -@pytest.mark.parametrize( - "installed", - ["0.17.0.dev0", "0.17.0rc1", "0.17.0.post1", "0.17.0+cu121", "0.17"], -) -def test_installed_version_uses_release_segment(monkeypatch, installed): - monkeypatch.setattr(kernels, "__version__", installed) - assert _installed_version() == Version.from_str("0.17.0") - - -def test_installed_version_is_none_for_non_pep440_version(monkeypatch): - monkeypatch.setattr(kernels, "__version__", "0.17.0-dirty") - assert _installed_version() is None - - -def test_unparseable_installed_version_does_not_fail_validation(monkeypatch): - # A version that cannot be compared must not turn this check into a - # failure. - monkeypatch.setattr(kernels, "__version__", "0.17.0-dirty") - MinverValidator().validate(tree=_make_tree("999.1.0")) - - -def test_version_ordering_is_numeric_not_lexicographic(): - # `0.9 < 0.10` only holds for numeric comparison; string comparison would - # get this backwards. - assert Version.from_str("0.9") < Version.from_str("0.10") - assert Version.from_str("0.14") == Version.from_str("0.14.0") - assert Version.from_str("0.14.0") < Version.from_str("0.14.1") - - -def test_raises_when_minver_not_met(): - with pytest.raises(RuntimeError, match="requires kernels>=999.1"): - MinverValidator().validate(tree=_make_tree("999.1.0")) - - -def test_error_mentions_installed_version(): - with pytest.raises(RuntimeError, match=f"version {re.escape(kernels.__version__)} is installed"): - MinverValidator().validate(tree=_make_tree("999.1.0")) diff --git a/kernels/tests/test_validate.py b/kernels/tests/test_validate.py new file mode 100644 index 000000000..ac3c5368a --- /dev/null +++ b/kernels/tests/test_validate.py @@ -0,0 +1,141 @@ +import re +from pathlib import Path + +import pytest +import torch +from kernels_data import Version + +import kernels +import kernels.validate as validate_module +from kernels.deps import DepTreeNode +from kernels.resolver import LocalKernel +from kernels.validate import ArchValidator, MinverValidator, _installed_version + + +@pytest.mark.parametrize("minver", [None, "0.0.1"]) +def test_no_error_when_minver_met(minver, make_metadata, make_tree): + MinverValidator().validate(tree=make_tree(make_metadata("cuda", None, kernels_minver=minver))) + + +def test_no_error_for_dev_version_of_required_release(monkeypatch, make_metadata, make_tree): + # A development version implements the release it leads up to, so + # `0.17.0.dev0` must satisfy a `0.17.0` requirement. + monkeypatch.setattr(kernels, "__version__", "0.17.0.dev0") + MinverValidator().validate(tree=make_tree(make_metadata("cuda", None, kernels_minver="0.17.0"))) + + +@pytest.mark.parametrize( + "installed", + ["0.17.0.dev0", "0.17.0rc1", "0.17.0.post1", "0.17.0+cu121", "0.17"], +) +def test_installed_version_uses_release_segment(monkeypatch, installed): + monkeypatch.setattr(kernels, "__version__", installed) + assert _installed_version() == Version.from_str("0.17.0") + + +def test_installed_version_is_none_for_non_pep440_version(monkeypatch): + monkeypatch.setattr(kernels, "__version__", "0.17.0-dirty") + assert _installed_version() is None + + +def test_unparseable_installed_version_does_not_fail_validation(monkeypatch, make_metadata, make_tree): + # A version that cannot be compared must not turn this check into a + # failure. + monkeypatch.setattr(kernels, "__version__", "0.17.0-dirty") + MinverValidator().validate(tree=make_tree(make_metadata("cuda", None, kernels_minver="999.1.0"))) + + +def test_version_ordering_is_numeric_not_lexicographic(): + # `0.9 < 0.10` only holds for numeric comparison; string comparison would + # get this backwards. + assert Version.from_str("0.9") < Version.from_str("0.10") + assert Version.from_str("0.14") == Version.from_str("0.14.0") + assert Version.from_str("0.14.0") < Version.from_str("0.14.1") + + +def test_raises_when_minver_not_met(make_metadata, make_tree): + with pytest.raises(RuntimeError, match="requires kernels>=999.1"): + MinverValidator().validate(tree=make_tree(make_metadata("cuda", None, kernels_minver="999.1.0"))) + + +def test_error_mentions_installed_version(make_metadata, make_tree): + with pytest.raises(RuntimeError, match=f"version {re.escape(kernels.__version__)} is installed"): + MinverValidator().validate(tree=make_tree(make_metadata("cuda", None, kernels_minver="999.1.0"))) + + +def test_cuda_incompatible_arch_is_rejected(fake_cuda_device, make_metadata, make_tree): + with pytest.raises(RuntimeError) as exc_info: + ArchValidator().validate(tree=make_tree(make_metadata("cuda", ["8.0", "9.0a"]))) + assert "test-variant" in str(exc_info.value) + assert "CUDA capability 10.0" in str(exc_info.value) + assert "8.0, 9.0a" in str(exc_info.value) + + +def test_cuda_compatible_arch_is_accepted(fake_cuda_device, make_metadata, make_tree): + for archs in (["8.0", "10.0"], ["10.0a"], ["10.0f"]): + ArchValidator().validate(tree=make_tree(make_metadata("cuda", archs))) + + +def test_noarch_build_is_accepted(fake_cuda_device, make_metadata, make_tree): + # Backends that support archs (e.g. CUDA) can have builds that do not + # declare any (noarch kernels, e.g. pure Triton builds). Such builds are + # never rejected. + ArchValidator().validate(tree=make_tree(make_metadata("cuda", None))) + ArchValidator().validate(tree=make_tree(make_metadata("cuda", []))) + + +def test_rocm_arch_check(fake_rocm_device, make_metadata, make_tree): + ArchValidator().validate(tree=make_tree(make_metadata("rocm", ["gfx90a", "gfx942"]))) + ArchValidator().validate(tree=make_tree(make_metadata("rocm", None))) + with pytest.raises(RuntimeError) as exc_info: + ArchValidator().validate(tree=make_tree(make_metadata("rocm", ["gfx942"]))) + assert "ROCm arch gfx90a" in str(exc_info.value) + assert "gfx942" in str(exc_info.value) + + +def test_check_skipped_without_device(monkeypatch, make_metadata, make_tree): + monkeypatch.setattr(torch.version, "cuda", "12.8", raising=False) + monkeypatch.setattr(torch.cuda, "is_available", lambda: False) + ArchValidator().validate(tree=make_tree(make_metadata("cuda", ["8.0"]))) + + +def test_check_skipped_for_backends_without_archs(fake_cuda_device, make_metadata, make_tree): + # Archs of other backends cannot be checked against the current device. + ArchValidator().validate(tree=make_tree(make_metadata("cpu", None))) + ArchValidator().validate(tree=make_tree(make_metadata("metal", ["applegpu_g13"]))) + + +def test_arch_validator_checks_entire_dependency_tree(monkeypatch, make_metadata): + tree = DepTreeNode( + location=LocalKernel(Path("root-variant"), make_metadata("cuda", ["8.0"])), + deps={ + "test/dependency": DepTreeNode( + location=LocalKernel(Path("dependency-variant"), make_metadata("cuda", ["9.0"])), + deps={}, + ) + }, + ) + validated = [] + + monkeypatch.setattr( + validate_module, + "_check_arch_incompatibility", + lambda metadata, variant: validated.append((metadata.backend.archs, variant)), + ) + + tree.validate(ArchValidator()) + + assert validated == [ + (["8.0"], "root-variant"), + (["9.0"], "dependency-variant"), + ] + + +def test_issue_707_fa3_on_b200(fake_cuda_device, make_metadata, make_tree): + # https://github.com/huggingface/kernels/issues/707: flash-attn3 only + # declares sm_80/sm_90a archs, but loading it on a B200 (capability 10.0) + # succeeded and the first launch exited the process. The declared archs + # must be rejected for this device. + metadata = make_metadata("cuda", ["8.0", "9.0a"]) + with pytest.raises(RuntimeError, match="does not support the current device"): + ArchValidator().validate(tree=make_tree(metadata)) diff --git a/nix-builder/pkgs/get-kernel-check/get-kernel-check-hook.py b/nix-builder/pkgs/get-kernel-check/get-kernel-check-hook.py index 2b2b720bd..1c9671117 100755 --- a/nix-builder/pkgs/get-kernel-check/get-kernel-check-hook.py +++ b/nix-builder/pkgs/get-kernel-check/get-kernel-check-hook.py @@ -1,10 +1,10 @@ import os from pathlib import Path -from kernels.deps import AllValidator, default_validators from kernels.hf_hub import _get_hf_api from kernels.load import get_kernel_with_resolver from kernels.resolver import KernelPathsResolver, RepoPathsResolver, SequentialResolver +from kernels.validate import AllValidator, default_validators from kernels_data import KernelDependency, KernelPaths, KernelVersion out = os.getenv("out") From 3ff926800ed26a98bc2e4d92b71b306f7f744cc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20de=20Kok?= Date: Wed, 2 Sep 2026 17:36:19 +0000 Subject: [PATCH 2/5] Rename `Validator` to `MetadataValidator` --- kernels/src/kernels/deps.py | 10 +++--- kernels/src/kernels/layer/func.py | 4 +-- kernels/src/kernels/layer/layer.py | 4 +-- kernels/src/kernels/load.py | 20 +++++------ kernels/src/kernels/validate.py | 22 ++++++------ kernels/tests/test_validate.py | 34 +++++++++---------- .../get-kernel-check/get-kernel-check-hook.py | 4 +-- 7 files changed, 49 insertions(+), 49 deletions(-) diff --git a/kernels/src/kernels/deps.py b/kernels/src/kernels/deps.py index e5d57b789..9df7afdb8 100644 --- a/kernels/src/kernels/deps.py +++ b/kernels/src/kernels/deps.py @@ -15,7 +15,7 @@ ) if TYPE_CHECKING: - from kernels.validate import Validator + from kernels.validate import MetadataValidator # Default state is `None`, to signal that we are not in a kernel # loading context. @@ -70,16 +70,16 @@ def load(self) -> ModuleType: return _import_from_path(self.location.variant_path, repo_info=repo_info, deps=deps) - def validate( + def validate_metadata( self: "DepTreeNode[LocalKernel | RemoteKernel]", - validator: "Validator", + validator: "MetadataValidator", ) -> None: """Validate this kernel and its dependencies with the given validator.""" - validator.validate(tree=self) + validator.validate_metadata(tree=self) for node in self.deps.values(): - node.validate(validator) + node.validate_metadata(validator) LoadCache = dict[KernelDependency, DepTreeNode] diff --git a/kernels/src/kernels/layer/func.py b/kernels/src/kernels/layer/func.py index ac84ab817..ebe6dd5b7 100644 --- a/kernels/src/kernels/layer/func.py +++ b/kernels/src/kernels/layer/func.py @@ -20,7 +20,7 @@ get_caller_locked_kernel_revision, get_locked_kernel_revision, ) -from ..validate import AllValidator, default_validators +from ..validate import AllValidator, default_metadata_validators from .layer import _create_func_module, use_kernel_forward_from_hub from .repos import RepositoryProtocol @@ -332,7 +332,7 @@ def load(self) -> Type["nn.Module"]: backend=None, kernel=self.kernel_dep, resolver=resolver, - validator=AllValidator(validators=default_validators()), + validator=AllValidator(validators=default_metadata_validators()), ) return _get_kernel_func(self, kernel) diff --git a/kernels/src/kernels/layer/layer.py b/kernels/src/kernels/layer/layer.py index 3eb1012d8..e63b3683d 100644 --- a/kernels/src/kernels/layer/layer.py +++ b/kernels/src/kernels/layer/layer.py @@ -25,7 +25,7 @@ get_caller_locked_kernel_revision, get_locked_kernel_revision, ) -from ..validate import AllValidator, default_validators +from ..validate import AllValidator, default_metadata_validators from .device import Device from .globals import _DISABLE_KERNEL_MAPPING, _KERNEL_MAPPING from .mode import Mode @@ -241,7 +241,7 @@ def load(self) -> Type["nn.Module"]: backend=None, kernel=self.kernel_dep, resolver=resolver, - validator=AllValidator(validators=default_validators()), + validator=AllValidator(validators=default_metadata_validators()), ) return _get_kernel_layer(self, kernel) diff --git a/kernels/src/kernels/load.py b/kernels/src/kernels/load.py index e67c06699..828672402 100644 --- a/kernels/src/kernels/load.py +++ b/kernels/src/kernels/load.py @@ -26,8 +26,8 @@ from kernels.validate import ( AllValidator, ArchValidator, - Validator, - default_validators, + MetadataValidator, + default_metadata_validators, ) @@ -61,7 +61,7 @@ def get_kernel_with_resolver( backend: str | None, kernel: KernelDependency, resolver: Resolver | None, - validator: Validator, + validator: MetadataValidator, ) -> ModuleType: """ Load a kernel and its (transitive) dependencies using the given resolver. @@ -76,7 +76,7 @@ def get_kernel_with_resolver( The kernel to load. resolver (`Resolver`, *optional*): The resolver used to resolve the kernel and its (transitive) dependencies. - validator (`Validator`): + validator (`MetadataValidator`): The validator to apply to the resolved kernel dependency tree. Returns: @@ -88,7 +88,7 @@ def get_kernel_with_resolver( kernel=kernel, resolver=resolver, ) - tree.validate(validator) + tree.validate_metadata(validator) tree_only_local = tree.install(api=api) return tree_only_local.load() @@ -162,7 +162,7 @@ def get_kernel( ), ] - validators: list[Validator] = default_validators() + validators: list[MetadataValidator] = default_metadata_validators() if check_arch: validators.append(ArchValidator()) @@ -225,7 +225,7 @@ def get_local_kernel( # We don't have a name for the kernel, so let's just use the path. kernel=KernelDependency(repo_id=str(repo_path), version=KernelVersion.Version(0)), resolver=SequentialResolver(resolvers), - validator=AllValidator(validators=default_validators()), + validator=AllValidator(validators=default_metadata_validators()), ) @@ -295,7 +295,7 @@ def has_kernel( if check_arch: try: - tree.validate(ArchValidator()) + tree.validate_metadata(ArchValidator()) except RuntimeError: return False @@ -342,7 +342,7 @@ def load_kernel( backend=backend, kernel=kernel_dep, resolver=resolver, - validator=AllValidator(validators=default_validators()), + validator=AllValidator(validators=default_metadata_validators()), ) @@ -383,5 +383,5 @@ def get_locked_kernel( backend=None, kernel=kernel_dep, resolver=resolver, - validator=AllValidator(validators=default_validators()), + validator=AllValidator(validators=default_metadata_validators()), ) diff --git a/kernels/src/kernels/validate.py b/kernels/src/kernels/validate.py index db1d59ce7..e1618774c 100644 --- a/kernels/src/kernels/validate.py +++ b/kernels/src/kernels/validate.py @@ -11,16 +11,16 @@ from kernels.resolver import LocalKernel, RemoteKernel -class Validator(Protocol): - """Validator for a node in a resolved kernel dependency tree.""" +class MetadataValidator(Protocol): + """Metadata validator for a node in a resolved kernel dependency tree.""" - def validate(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> None: ... + def validate_metadata(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> None: ... class DependencyValidator: """Validate the Python dependencies of a kernel tree node.""" - def validate(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> None: + def validate_metadata(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> None: validate_dependencies( tree.location.metadata.name.python_name, tree.location.metadata.python_depends, @@ -31,7 +31,7 @@ def validate(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> None: class ArchValidator: """Validate architecture compatibility for a kernel tree node.""" - def validate(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> None: + def validate_metadata(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> None: _check_arch_incompatibility(tree.location.metadata, tree.location.variant_str) @@ -66,7 +66,7 @@ class MinverValidator: """Validate that the installed `kernels` library meets the minimum version required by a kernel.""" - def validate(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> None: + def validate_metadata(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> None: metadata = tree.location.metadata minver = metadata.kernels_minver if minver is None: @@ -90,13 +90,13 @@ def validate(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> None: class AllValidator: """Apply multiple validators to a kernel dependency tree.""" - validators: list[Validator] + validators: list[MetadataValidator] - def validate(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> None: + def validate_metadata(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> None: for validator in self.validators: - validator.validate(tree=tree) + validator.validate_metadata(tree=tree) -def default_validators() -> list[Validator]: - """The validators that are applied to every kernel dependency tree.""" +def default_metadata_validators() -> list[MetadataValidator]: + """The metadata validators that are applied to every kernel dependency tree.""" return [DependencyValidator(), MinverValidator()] diff --git a/kernels/tests/test_validate.py b/kernels/tests/test_validate.py index ac3c5368a..e9a5bf30c 100644 --- a/kernels/tests/test_validate.py +++ b/kernels/tests/test_validate.py @@ -14,14 +14,14 @@ @pytest.mark.parametrize("minver", [None, "0.0.1"]) def test_no_error_when_minver_met(minver, make_metadata, make_tree): - MinverValidator().validate(tree=make_tree(make_metadata("cuda", None, kernels_minver=minver))) + MinverValidator().validate_metadata(tree=make_tree(make_metadata("cuda", None, kernels_minver=minver))) def test_no_error_for_dev_version_of_required_release(monkeypatch, make_metadata, make_tree): # A development version implements the release it leads up to, so # `0.17.0.dev0` must satisfy a `0.17.0` requirement. monkeypatch.setattr(kernels, "__version__", "0.17.0.dev0") - MinverValidator().validate(tree=make_tree(make_metadata("cuda", None, kernels_minver="0.17.0"))) + MinverValidator().validate_metadata(tree=make_tree(make_metadata("cuda", None, kernels_minver="0.17.0"))) @pytest.mark.parametrize( @@ -42,7 +42,7 @@ def test_unparseable_installed_version_does_not_fail_validation(monkeypatch, mak # A version that cannot be compared must not turn this check into a # failure. monkeypatch.setattr(kernels, "__version__", "0.17.0-dirty") - MinverValidator().validate(tree=make_tree(make_metadata("cuda", None, kernels_minver="999.1.0"))) + MinverValidator().validate_metadata(tree=make_tree(make_metadata("cuda", None, kernels_minver="999.1.0"))) def test_version_ordering_is_numeric_not_lexicographic(): @@ -55,17 +55,17 @@ def test_version_ordering_is_numeric_not_lexicographic(): def test_raises_when_minver_not_met(make_metadata, make_tree): with pytest.raises(RuntimeError, match="requires kernels>=999.1"): - MinverValidator().validate(tree=make_tree(make_metadata("cuda", None, kernels_minver="999.1.0"))) + MinverValidator().validate_metadata(tree=make_tree(make_metadata("cuda", None, kernels_minver="999.1.0"))) def test_error_mentions_installed_version(make_metadata, make_tree): with pytest.raises(RuntimeError, match=f"version {re.escape(kernels.__version__)} is installed"): - MinverValidator().validate(tree=make_tree(make_metadata("cuda", None, kernels_minver="999.1.0"))) + MinverValidator().validate_metadata(tree=make_tree(make_metadata("cuda", None, kernels_minver="999.1.0"))) def test_cuda_incompatible_arch_is_rejected(fake_cuda_device, make_metadata, make_tree): with pytest.raises(RuntimeError) as exc_info: - ArchValidator().validate(tree=make_tree(make_metadata("cuda", ["8.0", "9.0a"]))) + ArchValidator().validate_metadata(tree=make_tree(make_metadata("cuda", ["8.0", "9.0a"]))) assert "test-variant" in str(exc_info.value) assert "CUDA capability 10.0" in str(exc_info.value) assert "8.0, 9.0a" in str(exc_info.value) @@ -73,22 +73,22 @@ def test_cuda_incompatible_arch_is_rejected(fake_cuda_device, make_metadata, mak def test_cuda_compatible_arch_is_accepted(fake_cuda_device, make_metadata, make_tree): for archs in (["8.0", "10.0"], ["10.0a"], ["10.0f"]): - ArchValidator().validate(tree=make_tree(make_metadata("cuda", archs))) + ArchValidator().validate_metadata(tree=make_tree(make_metadata("cuda", archs))) def test_noarch_build_is_accepted(fake_cuda_device, make_metadata, make_tree): # Backends that support archs (e.g. CUDA) can have builds that do not # declare any (noarch kernels, e.g. pure Triton builds). Such builds are # never rejected. - ArchValidator().validate(tree=make_tree(make_metadata("cuda", None))) - ArchValidator().validate(tree=make_tree(make_metadata("cuda", []))) + ArchValidator().validate_metadata(tree=make_tree(make_metadata("cuda", None))) + ArchValidator().validate_metadata(tree=make_tree(make_metadata("cuda", []))) def test_rocm_arch_check(fake_rocm_device, make_metadata, make_tree): - ArchValidator().validate(tree=make_tree(make_metadata("rocm", ["gfx90a", "gfx942"]))) - ArchValidator().validate(tree=make_tree(make_metadata("rocm", None))) + ArchValidator().validate_metadata(tree=make_tree(make_metadata("rocm", ["gfx90a", "gfx942"]))) + ArchValidator().validate_metadata(tree=make_tree(make_metadata("rocm", None))) with pytest.raises(RuntimeError) as exc_info: - ArchValidator().validate(tree=make_tree(make_metadata("rocm", ["gfx942"]))) + ArchValidator().validate_metadata(tree=make_tree(make_metadata("rocm", ["gfx942"]))) assert "ROCm arch gfx90a" in str(exc_info.value) assert "gfx942" in str(exc_info.value) @@ -96,13 +96,13 @@ def test_rocm_arch_check(fake_rocm_device, make_metadata, make_tree): def test_check_skipped_without_device(monkeypatch, make_metadata, make_tree): monkeypatch.setattr(torch.version, "cuda", "12.8", raising=False) monkeypatch.setattr(torch.cuda, "is_available", lambda: False) - ArchValidator().validate(tree=make_tree(make_metadata("cuda", ["8.0"]))) + ArchValidator().validate_metadata(tree=make_tree(make_metadata("cuda", ["8.0"]))) def test_check_skipped_for_backends_without_archs(fake_cuda_device, make_metadata, make_tree): # Archs of other backends cannot be checked against the current device. - ArchValidator().validate(tree=make_tree(make_metadata("cpu", None))) - ArchValidator().validate(tree=make_tree(make_metadata("metal", ["applegpu_g13"]))) + ArchValidator().validate_metadata(tree=make_tree(make_metadata("cpu", None))) + ArchValidator().validate_metadata(tree=make_tree(make_metadata("metal", ["applegpu_g13"]))) def test_arch_validator_checks_entire_dependency_tree(monkeypatch, make_metadata): @@ -123,7 +123,7 @@ def test_arch_validator_checks_entire_dependency_tree(monkeypatch, make_metadata lambda metadata, variant: validated.append((metadata.backend.archs, variant)), ) - tree.validate(ArchValidator()) + tree.validate_metadata(ArchValidator()) assert validated == [ (["8.0"], "root-variant"), @@ -138,4 +138,4 @@ def test_issue_707_fa3_on_b200(fake_cuda_device, make_metadata, make_tree): # must be rejected for this device. metadata = make_metadata("cuda", ["8.0", "9.0a"]) with pytest.raises(RuntimeError, match="does not support the current device"): - ArchValidator().validate(tree=make_tree(metadata)) + ArchValidator().validate_metadata(tree=make_tree(metadata)) diff --git a/nix-builder/pkgs/get-kernel-check/get-kernel-check-hook.py b/nix-builder/pkgs/get-kernel-check/get-kernel-check-hook.py index 1c9671117..929890472 100755 --- a/nix-builder/pkgs/get-kernel-check/get-kernel-check-hook.py +++ b/nix-builder/pkgs/get-kernel-check/get-kernel-check-hook.py @@ -4,7 +4,7 @@ from kernels.hf_hub import _get_hf_api from kernels.load import get_kernel_with_resolver from kernels.resolver import KernelPathsResolver, RepoPathsResolver, SequentialResolver -from kernels.validate import AllValidator, default_validators +from kernels.validate import AllValidator, default_metadata_validators from kernels_data import KernelDependency, KernelPaths, KernelVersion out = os.getenv("out") @@ -30,5 +30,5 @@ backend=None, kernel=kernel, resolver=SequentialResolver(resolvers=resolvers), - validator=AllValidator(validators=default_validators()), + validator=AllValidator(validators=default_metadata_validators()), ) From 2c9894a8551b70813b8be3641b0d0c78628dfd3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20de=20Kok?= Date: Wed, 2 Sep 2026 17:40:38 +0000 Subject: [PATCH 3/5] Rename `validator` argument to `metadata_validator` --- kernels/src/kernels/layer/func.py | 2 +- kernels/src/kernels/layer/layer.py | 2 +- kernels/src/kernels/load.py | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/kernels/src/kernels/layer/func.py b/kernels/src/kernels/layer/func.py index ebe6dd5b7..1d8d5333c 100644 --- a/kernels/src/kernels/layer/func.py +++ b/kernels/src/kernels/layer/func.py @@ -332,7 +332,7 @@ def load(self) -> Type["nn.Module"]: backend=None, kernel=self.kernel_dep, resolver=resolver, - validator=AllValidator(validators=default_metadata_validators()), + metadata_validator=AllValidator(validators=default_metadata_validators()), ) return _get_kernel_func(self, kernel) diff --git a/kernels/src/kernels/layer/layer.py b/kernels/src/kernels/layer/layer.py index e63b3683d..ee7633769 100644 --- a/kernels/src/kernels/layer/layer.py +++ b/kernels/src/kernels/layer/layer.py @@ -241,7 +241,7 @@ def load(self) -> Type["nn.Module"]: backend=None, kernel=self.kernel_dep, resolver=resolver, - validator=AllValidator(validators=default_metadata_validators()), + metadata_validator=AllValidator(validators=default_metadata_validators()), ) return _get_kernel_layer(self, kernel) diff --git a/kernels/src/kernels/load.py b/kernels/src/kernels/load.py index 828672402..0f3e9376d 100644 --- a/kernels/src/kernels/load.py +++ b/kernels/src/kernels/load.py @@ -61,7 +61,7 @@ def get_kernel_with_resolver( backend: str | None, kernel: KernelDependency, resolver: Resolver | None, - validator: MetadataValidator, + metadata_validator: MetadataValidator, ) -> ModuleType: """ Load a kernel and its (transitive) dependencies using the given resolver. @@ -76,7 +76,7 @@ def get_kernel_with_resolver( The kernel to load. resolver (`Resolver`, *optional*): The resolver used to resolve the kernel and its (transitive) dependencies. - validator (`MetadataValidator`): + metadata_validator (`MetadataValidator`): The validator to apply to the resolved kernel dependency tree. Returns: @@ -88,7 +88,7 @@ def get_kernel_with_resolver( kernel=kernel, resolver=resolver, ) - tree.validate_metadata(validator) + tree.validate_metadata(metadata_validator) tree_only_local = tree.install(api=api) return tree_only_local.load() @@ -171,7 +171,7 @@ def get_kernel( backend=backend, kernel=KernelDependency(repo_id=repo_id, version=kernel_version), resolver=SequentialResolver(resolvers=resolvers), - validator=AllValidator(validators=validators), + metadata_validator=AllValidator(validators=validators), ) @@ -225,7 +225,7 @@ def get_local_kernel( # We don't have a name for the kernel, so let's just use the path. kernel=KernelDependency(repo_id=str(repo_path), version=KernelVersion.Version(0)), resolver=SequentialResolver(resolvers), - validator=AllValidator(validators=default_metadata_validators()), + metadata_validator=AllValidator(validators=default_metadata_validators()), ) @@ -342,7 +342,7 @@ def load_kernel( backend=backend, kernel=kernel_dep, resolver=resolver, - validator=AllValidator(validators=default_metadata_validators()), + metadata_validator=AllValidator(validators=default_metadata_validators()), ) @@ -383,5 +383,5 @@ def get_locked_kernel( backend=None, kernel=kernel_dep, resolver=resolver, - validator=AllValidator(validators=default_metadata_validators()), + metadata_validator=AllValidator(validators=default_metadata_validators()), ) From 29b8cfc8091ca6b951324c2752be69811bb4b689 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20de=20Kok?= Date: Wed, 2 Sep 2026 17:57:26 +0000 Subject: [PATCH 4/5] Make `MetadataValidator`s take `Metadata` and the variant `str` We should be able to change the tree node representation without updating validators, so stop passing tree nodes. --- kernels/src/kernels/deps.py | 2 +- kernels/src/kernels/validate.py | 31 +++++++--------- kernels/tests/conftest.py | 14 ------- kernels/tests/test_validate.py | 66 +++++++++++++++++++-------------- 4 files changed, 53 insertions(+), 60 deletions(-) diff --git a/kernels/src/kernels/deps.py b/kernels/src/kernels/deps.py index 9df7afdb8..5fd480638 100644 --- a/kernels/src/kernels/deps.py +++ b/kernels/src/kernels/deps.py @@ -76,7 +76,7 @@ def validate_metadata( ) -> None: """Validate this kernel and its dependencies with the given validator.""" - validator.validate_metadata(tree=self) + validator.validate_metadata(metadata=self.location.metadata, variant=self.location.variant_str) for node in self.deps.values(): node.validate_metadata(validator) diff --git a/kernels/src/kernels/validate.py b/kernels/src/kernels/validate.py index e1618774c..4d2f4f059 100644 --- a/kernels/src/kernels/validate.py +++ b/kernels/src/kernels/validate.py @@ -1,38 +1,36 @@ from dataclasses import dataclass from typing import Protocol -from kernels_data import Version +from kernels_data import Metadata, Version from packaging.version import InvalidVersion, parse from kernels.archs import _check_arch_incompatibility from kernels.backends import _backend -from kernels.deps import DepTreeNode from kernels.python_deps import validate_dependencies -from kernels.resolver import LocalKernel, RemoteKernel class MetadataValidator(Protocol): - """Metadata validator for a node in a resolved kernel dependency tree.""" + """Metadata validator for a kernel build variant.""" - def validate_metadata(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> None: ... + def validate_metadata(self, *, metadata: Metadata, variant: str) -> None: ... class DependencyValidator: - """Validate the Python dependencies of a kernel tree node.""" + """Validate the Python dependencies of a kernel build variant.""" - def validate_metadata(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> None: + def validate_metadata(self, *, metadata: Metadata, variant: str) -> None: validate_dependencies( - tree.location.metadata.name.python_name, - tree.location.metadata.python_depends, + metadata.name.python_name, + metadata.python_depends, _backend(), ) class ArchValidator: - """Validate architecture compatibility for a kernel tree node.""" + """Validate architecture compatibility for a kernel build variant.""" - def validate_metadata(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> None: - _check_arch_incompatibility(tree.location.metadata, tree.location.variant_str) + def validate_metadata(self, *, metadata: Metadata, variant: str) -> None: + _check_arch_incompatibility(metadata, variant) def _installed_version() -> Version | None: @@ -66,8 +64,7 @@ class MinverValidator: """Validate that the installed `kernels` library meets the minimum version required by a kernel.""" - def validate_metadata(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> None: - metadata = tree.location.metadata + def validate_metadata(self, *, metadata: Metadata, variant: str) -> None: minver = metadata.kernels_minver if minver is None: return @@ -80,7 +77,7 @@ def validate_metadata(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> from kernels import __version__ raise RuntimeError( - f"Kernel '{metadata.name}' variant '{tree.location.variant_str}' requires " + f"Kernel '{metadata.name}' variant '{variant}' requires " f"kernels>={minver}, but version {__version__} is installed. " "Upgrade with: pip install --upgrade kernels" ) @@ -92,9 +89,9 @@ class AllValidator: validators: list[MetadataValidator] - def validate_metadata(self, *, tree: DepTreeNode[LocalKernel | RemoteKernel]) -> None: + def validate_metadata(self, *, metadata: Metadata, variant: str) -> None: for validator in self.validators: - validator.validate_metadata(tree=tree) + validator.validate_metadata(metadata=metadata, variant=variant) def default_metadata_validators() -> list[MetadataValidator]: diff --git a/kernels/tests/conftest.py b/kernels/tests/conftest.py index 8605147c9..36807676d 100644 --- a/kernels/tests/conftest.py +++ b/kernels/tests/conftest.py @@ -1,14 +1,11 @@ import importlib.util import json import sys -from pathlib import Path import pytest from kernels_data import Metadata from kernels.backends import _get_torch_privateuse_backend_name -from kernels.deps import DepTreeNode -from kernels.resolver import LocalKernel try: import torch @@ -84,17 +81,6 @@ def _make_metadata(backend_type: str, archs: list[str] | None, kernels_minver: s return _make_metadata -@pytest.fixture -def make_tree(): - def _make_tree(metadata: Metadata, variant: str = "test-variant") -> DepTreeNode: - return DepTreeNode( - location=LocalKernel(Path(variant), metadata), - deps={}, - ) - - return _make_tree - - @pytest.fixture def fake_cuda_device(monkeypatch): monkeypatch.setattr(torch.version, "cuda", "12.8", raising=False) diff --git a/kernels/tests/test_validate.py b/kernels/tests/test_validate.py index e9a5bf30c..1f2583860 100644 --- a/kernels/tests/test_validate.py +++ b/kernels/tests/test_validate.py @@ -13,15 +13,19 @@ @pytest.mark.parametrize("minver", [None, "0.0.1"]) -def test_no_error_when_minver_met(minver, make_metadata, make_tree): - MinverValidator().validate_metadata(tree=make_tree(make_metadata("cuda", None, kernels_minver=minver))) +def test_no_error_when_minver_met(minver, make_metadata): + MinverValidator().validate_metadata( + metadata=make_metadata("cuda", None, kernels_minver=minver), variant="test-variant" + ) -def test_no_error_for_dev_version_of_required_release(monkeypatch, make_metadata, make_tree): +def test_no_error_for_dev_version_of_required_release(monkeypatch, make_metadata): # A development version implements the release it leads up to, so # `0.17.0.dev0` must satisfy a `0.17.0` requirement. monkeypatch.setattr(kernels, "__version__", "0.17.0.dev0") - MinverValidator().validate_metadata(tree=make_tree(make_metadata("cuda", None, kernels_minver="0.17.0"))) + MinverValidator().validate_metadata( + metadata=make_metadata("cuda", None, kernels_minver="0.17.0"), variant="test-variant" + ) @pytest.mark.parametrize( @@ -38,11 +42,13 @@ def test_installed_version_is_none_for_non_pep440_version(monkeypatch): assert _installed_version() is None -def test_unparseable_installed_version_does_not_fail_validation(monkeypatch, make_metadata, make_tree): +def test_unparseable_installed_version_does_not_fail_validation(monkeypatch, make_metadata): # A version that cannot be compared must not turn this check into a # failure. monkeypatch.setattr(kernels, "__version__", "0.17.0-dirty") - MinverValidator().validate_metadata(tree=make_tree(make_metadata("cuda", None, kernels_minver="999.1.0"))) + MinverValidator().validate_metadata( + metadata=make_metadata("cuda", None, kernels_minver="999.1.0"), variant="test-variant" + ) def test_version_ordering_is_numeric_not_lexicographic(): @@ -53,56 +59,60 @@ def test_version_ordering_is_numeric_not_lexicographic(): assert Version.from_str("0.14.0") < Version.from_str("0.14.1") -def test_raises_when_minver_not_met(make_metadata, make_tree): +def test_raises_when_minver_not_met(make_metadata): with pytest.raises(RuntimeError, match="requires kernels>=999.1"): - MinverValidator().validate_metadata(tree=make_tree(make_metadata("cuda", None, kernels_minver="999.1.0"))) + MinverValidator().validate_metadata( + metadata=make_metadata("cuda", None, kernels_minver="999.1.0"), variant="test-variant" + ) -def test_error_mentions_installed_version(make_metadata, make_tree): +def test_error_mentions_installed_version(make_metadata): with pytest.raises(RuntimeError, match=f"version {re.escape(kernels.__version__)} is installed"): - MinverValidator().validate_metadata(tree=make_tree(make_metadata("cuda", None, kernels_minver="999.1.0"))) + MinverValidator().validate_metadata( + metadata=make_metadata("cuda", None, kernels_minver="999.1.0"), variant="test-variant" + ) -def test_cuda_incompatible_arch_is_rejected(fake_cuda_device, make_metadata, make_tree): +def test_cuda_incompatible_arch_is_rejected(fake_cuda_device, make_metadata): with pytest.raises(RuntimeError) as exc_info: - ArchValidator().validate_metadata(tree=make_tree(make_metadata("cuda", ["8.0", "9.0a"]))) + ArchValidator().validate_metadata(metadata=make_metadata("cuda", ["8.0", "9.0a"]), variant="test-variant") assert "test-variant" in str(exc_info.value) assert "CUDA capability 10.0" in str(exc_info.value) assert "8.0, 9.0a" in str(exc_info.value) -def test_cuda_compatible_arch_is_accepted(fake_cuda_device, make_metadata, make_tree): +def test_cuda_compatible_arch_is_accepted(fake_cuda_device, make_metadata): for archs in (["8.0", "10.0"], ["10.0a"], ["10.0f"]): - ArchValidator().validate_metadata(tree=make_tree(make_metadata("cuda", archs))) + ArchValidator().validate_metadata(metadata=make_metadata("cuda", archs), variant="test-variant") -def test_noarch_build_is_accepted(fake_cuda_device, make_metadata, make_tree): +def test_noarch_build_is_accepted(fake_cuda_device, make_metadata): # Backends that support archs (e.g. CUDA) can have builds that do not # declare any (noarch kernels, e.g. pure Triton builds). Such builds are # never rejected. - ArchValidator().validate_metadata(tree=make_tree(make_metadata("cuda", None))) - ArchValidator().validate_metadata(tree=make_tree(make_metadata("cuda", []))) + ArchValidator().validate_metadata(metadata=make_metadata("cuda", None), variant="test-variant") + ArchValidator().validate_metadata(metadata=make_metadata("cuda", []), variant="test-variant") -def test_rocm_arch_check(fake_rocm_device, make_metadata, make_tree): - ArchValidator().validate_metadata(tree=make_tree(make_metadata("rocm", ["gfx90a", "gfx942"]))) - ArchValidator().validate_metadata(tree=make_tree(make_metadata("rocm", None))) +def test_rocm_arch_check(fake_rocm_device, make_metadata): + ArchValidator().validate_metadata(metadata=make_metadata("rocm", ["gfx90a", "gfx942"]), variant="test-variant") + ArchValidator().validate_metadata(metadata=make_metadata("rocm", None), variant="test-variant") with pytest.raises(RuntimeError) as exc_info: - ArchValidator().validate_metadata(tree=make_tree(make_metadata("rocm", ["gfx942"]))) + ArchValidator().validate_metadata(metadata=make_metadata("rocm", ["gfx942"]), variant="test-variant") assert "ROCm arch gfx90a" in str(exc_info.value) assert "gfx942" in str(exc_info.value) -def test_check_skipped_without_device(monkeypatch, make_metadata, make_tree): +def test_check_skipped_without_device(monkeypatch, make_metadata): monkeypatch.setattr(torch.version, "cuda", "12.8", raising=False) monkeypatch.setattr(torch.cuda, "is_available", lambda: False) - ArchValidator().validate_metadata(tree=make_tree(make_metadata("cuda", ["8.0"]))) + ArchValidator().validate_metadata(metadata=make_metadata("cuda", ["8.0"]), variant="test-variant") -def test_check_skipped_for_backends_without_archs(fake_cuda_device, make_metadata, make_tree): +def test_check_skipped_for_backends_without_archs(fake_cuda_device, make_metadata): # Archs of other backends cannot be checked against the current device. - ArchValidator().validate_metadata(tree=make_tree(make_metadata("cpu", None))) - ArchValidator().validate_metadata(tree=make_tree(make_metadata("metal", ["applegpu_g13"]))) + ArchValidator().validate_metadata(metadata=make_metadata("cpu", None), variant="test-variant") + ArchValidator().validate_metadata(metadata=make_metadata("metal", ["applegpu_g13"]), variant="test-variant") def test_arch_validator_checks_entire_dependency_tree(monkeypatch, make_metadata): @@ -131,11 +141,11 @@ def test_arch_validator_checks_entire_dependency_tree(monkeypatch, make_metadata ] -def test_issue_707_fa3_on_b200(fake_cuda_device, make_metadata, make_tree): +def test_issue_707_fa3_on_b200(fake_cuda_device, make_metadata): # https://github.com/huggingface/kernels/issues/707: flash-attn3 only # declares sm_80/sm_90a archs, but loading it on a B200 (capability 10.0) # succeeded and the first launch exited the process. The declared archs # must be rejected for this device. metadata = make_metadata("cuda", ["8.0", "9.0a"]) with pytest.raises(RuntimeError, match="does not support the current device"): - ArchValidator().validate_metadata(tree=make_tree(metadata)) + ArchValidator().validate_metadata(metadata=metadata, variant="test-variant") From f450e3846c241f96be2c68913999306a324f29ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20de=20Kok?= Date: Wed, 2 Sep 2026 18:43:14 +0000 Subject: [PATCH 5/5] Fixup call that was not updated --- nix-builder/pkgs/get-kernel-check/get-kernel-check-hook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix-builder/pkgs/get-kernel-check/get-kernel-check-hook.py b/nix-builder/pkgs/get-kernel-check/get-kernel-check-hook.py index 929890472..29f25d6e1 100755 --- a/nix-builder/pkgs/get-kernel-check/get-kernel-check-hook.py +++ b/nix-builder/pkgs/get-kernel-check/get-kernel-check-hook.py @@ -30,5 +30,5 @@ backend=None, kernel=kernel, resolver=SequentialResolver(resolvers=resolvers), - validator=AllValidator(validators=default_metadata_validators()), + metadata_validator=AllValidator(validators=default_metadata_validators()), )