Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 9 additions & 101 deletions kernels/src/kernels/deps.py
Original file line number Diff line number Diff line change
@@ -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 MetadataValidator

# 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)
Expand Down Expand Up @@ -71,107 +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(metadata=self.location.metadata, variant=self.location.variant_str)

for node in self.deps.values():
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()]
node.validate_metadata(validator)


LoadCache = dict[KernelDependency, DepTreeNode]
Expand Down
4 changes: 2 additions & 2 deletions kernels/src/kernels/layer/func.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -21,6 +20,7 @@
get_caller_locked_kernel_revision,
get_locked_kernel_revision,
)
from ..validate import AllValidator, default_metadata_validators
from .layer import _create_func_module, use_kernel_forward_from_hub
from .repos import RepositoryProtocol

Expand Down Expand Up @@ -332,7 +332,7 @@ def load(self) -> Type["nn.Module"]:
backend=None,
kernel=self.kernel_dep,
resolver=resolver,
validator=AllValidator(validators=default_validators()),
metadata_validator=AllValidator(validators=default_metadata_validators()),
)
return _get_kernel_func(self, kernel)

Expand Down
4 changes: 2 additions & 2 deletions kernels/src/kernels/layer/layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -26,6 +25,7 @@
get_caller_locked_kernel_revision,
get_locked_kernel_revision,
)
from ..validate import AllValidator, default_metadata_validators
from .device import Device
from .globals import _DISABLE_KERNEL_MAPPING, _KERNEL_MAPPING
from .mode import Mode
Expand Down Expand Up @@ -241,7 +241,7 @@ def load(self) -> Type["nn.Module"]:
backend=None,
kernel=self.kernel_dep,
resolver=resolver,
validator=AllValidator(validators=default_validators()),
metadata_validator=AllValidator(validators=default_metadata_validators()),
)
return _get_kernel_layer(self, kernel)

Expand Down
32 changes: 16 additions & 16 deletions kernels/src/kernels/load.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -29,6 +23,12 @@
Resolver,
SequentialResolver,
)
from kernels.validate import (
AllValidator,
ArchValidator,
MetadataValidator,
default_metadata_validators,
)


def _get_local_kernel_overrides() -> Resolver:
Expand Down Expand Up @@ -61,7 +61,7 @@ def get_kernel_with_resolver(
backend: str | None,
kernel: KernelDependency,
resolver: Resolver | None,
validator: Validator,
metadata_validator: MetadataValidator,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be a generic validator still because we might want to add a kernel validator in the future and we will have to add another argument for that, right?

@danieldk danieldk Sep 3, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For kernel validators there would be a kernel_validator: KernelValidator argument. Otherwise we'd have MetadataValidator | KernelValidator, but you would not be able to pass both.

@danieldk danieldk Sep 3, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or, we'd need another Validator protocol, but it couldn't define any methods, since the arguments would be different between metadata and kernel validators.

) -> ModuleType:
"""
Load a kernel and its (transitive) dependencies using the given resolver.
Expand All @@ -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`):
metadata_validator (`MetadataValidator`):
The validator to apply to the resolved kernel dependency tree.

Returns:
Expand All @@ -88,7 +88,7 @@ def get_kernel_with_resolver(
kernel=kernel,
resolver=resolver,
)
tree.validate(validator)
tree.validate_metadata(metadata_validator)
tree_only_local = tree.install(api=api)
return tree_only_local.load()

Expand Down Expand Up @@ -162,7 +162,7 @@ def get_kernel(
),
]

validators: list[Validator] = default_validators()
validators: list[MetadataValidator] = default_metadata_validators()
if check_arch:
validators.append(ArchValidator())

Expand All @@ -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),
)


Expand Down Expand Up @@ -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()),
metadata_validator=AllValidator(validators=default_metadata_validators()),
)


Expand Down Expand Up @@ -295,7 +295,7 @@ def has_kernel(

if check_arch:
try:
tree.validate(ArchValidator())
tree.validate_metadata(ArchValidator())
except RuntimeError:
return False

Expand Down Expand Up @@ -342,7 +342,7 @@ def load_kernel(
backend=backend,
kernel=kernel_dep,
resolver=resolver,
validator=AllValidator(validators=default_validators()),
metadata_validator=AllValidator(validators=default_metadata_validators()),
)


Expand Down Expand Up @@ -383,5 +383,5 @@ def get_locked_kernel(
backend=None,
kernel=kernel_dep,
resolver=resolver,
validator=AllValidator(validators=default_validators()),
metadata_validator=AllValidator(validators=default_metadata_validators()),
)
99 changes: 99 additions & 0 deletions kernels/src/kernels/validate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
from dataclasses import dataclass
from typing import Protocol

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.python_deps import validate_dependencies


class MetadataValidator(Protocol):
"""Metadata validator for a kernel build variant."""

def validate_metadata(self, *, metadata: Metadata, variant: str) -> None: ...


class DependencyValidator:
"""Validate the Python dependencies of a kernel build variant."""

def validate_metadata(self, *, metadata: Metadata, variant: str) -> None:
validate_dependencies(
metadata.name.python_name,
metadata.python_depends,
_backend(),
)


class ArchValidator:
"""Validate architecture compatibility for a kernel build variant."""

def validate_metadata(self, *, metadata: Metadata, variant: str) -> None:
_check_arch_incompatibility(metadata, variant)


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_metadata(self, *, metadata: Metadata, variant: str) -> None:
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 '{variant}' 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[MetadataValidator]

def validate_metadata(self, *, metadata: Metadata, variant: str) -> None:
for validator in self.validators:
validator.validate_metadata(metadata=metadata, variant=variant)


def default_metadata_validators() -> list[MetadataValidator]:
"""The metadata validators that are applied to every kernel dependency tree."""
return [DependencyValidator(), MinverValidator()]
Loading
Loading