diff --git a/kernels/src/kernels/__init__.py b/kernels/src/kernels/__init__.py index ff5c36bf9..861afdc15 100644 --- a/kernels/src/kernels/__init__.py +++ b/kernels/src/kernels/__init__.py @@ -6,6 +6,7 @@ from kernels._windows import _add_additional_dll_paths from kernels.benchmark import Benchmark +from kernels.hf_hub import RepoInfo from kernels.layer import ( CUDAProperties, Device, @@ -27,7 +28,6 @@ ) from kernels.utils import ( LoadedKernel, - RepoInfo, get_kernel, get_kernel_variants, get_loaded_kernels, diff --git a/kernels/src/kernels/_versions.py b/kernels/src/kernels/_versions.py index b5b4fae85..3bcdee538 100644 --- a/kernels/src/kernels/_versions.py +++ b/kernels/src/kernels/_versions.py @@ -11,7 +11,7 @@ def _get_available_versions(repo_id: str) -> dict[int, GitRefInfo]: """Get kernel versions that are available in the repository.""" - from kernels.utils import _get_hf_api + from kernels.hf_hub import _get_hf_api if constants.HF_HUB_OFFLINE: return _get_available_versions_from_cache(repo_id) diff --git a/kernels/src/kernels/cli/benchmark.py b/kernels/src/kernels/cli/benchmark.py index b6e5bf4ea..2aa82a728 100644 --- a/kernels/src/kernels/cli/benchmark.py +++ b/kernels/src/kernels/cli/benchmark.py @@ -21,7 +21,8 @@ ) from kernels.benchmark import Benchmark -from kernels.utils import _backend, _get_hf_api +from kernels.hf_hub import _get_hf_api +from kernels.utils import _backend MISSING_DEPS: list[str] = [] diff --git a/kernels/src/kernels/cli/info.py b/kernels/src/kernels/cli/info.py index ccf545df2..36aae73d3 100644 --- a/kernels/src/kernels/cli/info.py +++ b/kernels/src/kernels/cli/info.py @@ -6,7 +6,7 @@ from kernels_data import Metadata from kernels._versions import _get_available_versions, resolve_version_spec_as_ref -from kernels.utils import CACHE_DIR, _get_hf_api +from kernels.hf_hub import CACHE_DIR, _get_hf_api from kernels.variants import ( ArchVariant, Variant, diff --git a/kernels/src/kernels/cli/versions.py b/kernels/src/kernels/cli/versions.py index b9f2cd03d..7b091848a 100644 --- a/kernels/src/kernels/cli/versions.py +++ b/kernels/src/kernels/cli/versions.py @@ -1,5 +1,5 @@ from kernels._versions import _get_available_versions -from kernels.utils import _get_hf_api +from kernels.hf_hub import _get_hf_api from kernels.variants import ( get_variants, resolve_variants, diff --git a/kernels/src/kernels/hf_hub.py b/kernels/src/kernels/hf_hub.py new file mode 100644 index 000000000..309013c40 --- /dev/null +++ b/kernels/src/kernels/hf_hub.py @@ -0,0 +1,148 @@ +import os +import platform +import warnings +from dataclasses import dataclass + +from huggingface_hub import HfApi, constants + +from kernels._system import glibc_version +from kernels.backends import _select_backend +from kernels.compat import has_torch, has_tvm_ffi + + +def _platform() -> str: + cpu = platform.machine() + os = platform.system().lower() + + if os == "darwin": + cpu = "aarch64" if cpu == "arm64" else cpu + elif os == "windows": + cpu = "x86_64" if cpu == "AMD64" else cpu + + return f"{cpu}-{os}" + + +def _get_hf_api(user_agent: str | dict | None = None) -> HfApi: + """Returns an instance of HfApi with proper settings.""" + + from . import __version__ + + user_agent_str = "" + if not constants.HF_HUB_DISABLE_TELEMETRY: + parts: list[str] = [] + + # User-defined info + if isinstance(user_agent, dict): + parts.extend(f"{k}/{v}" for k, v in user_agent.items()) + elif isinstance(user_agent, str) and user_agent: + parts.append(user_agent) + + # System info + python = ".".join(platform.python_version_tuple()[:2]) + backend = _select_backend(None).variant_str + parts.extend( + [ + f"kernels/{__version__}", + f"python/{python}", + f"backend/{backend}", + f"platform/{_platform()}", + "file_type/kernel", + ] + ) + + if has_torch: + import torch + + parts.append(f"torch/{torch.__version__}") + if has_tvm_ffi: + import tvm_ffi + + parts.append(f"tvm-ffi/{tvm_ffi.__version__}") + + # Add glibc version if available + glibc = glibc_version() + if glibc is not None: + parts.append(f"glibc/{glibc}") + + user_agent_str = "; ".join(parts) + + return HfApi(library_name="kernels", library_version=__version__, user_agent=user_agent_str) + + +def _get_cache_dir() -> str | None: + """Returns the kernels cache directory.""" + return os.environ.get("KERNELS_CACHE", None) + + +CACHE_DIR: str | None = _get_cache_dir() + + +@dataclass(frozen=True) +class RepoInfo: + """ + This dataclass stores the origin of the kernel. + + The following fields are available: + + - `repo_id` (`str`): the Hub repository containing the kernel. + - `revision` (`str`): the specific revision of the kernel. + """ + + repo_id: str + revision: str + + +def _check_trust_remote_code(repo_id: str, trust_remote_code: bool | list[str]) -> None: + """Check whether a kernel repository is trusted. + + When ``trust_remote_code`` is ``False`` (the default), only repositories + whose publisher organization has ``trustedKernelPublisher`` enabled on the + Hub are allowed. Repositories from untrusted publishers will raise a + ``ValueError``. + + When ``trust_remote_code`` is ``True``, all repositories are allowed. + + When ``trust_remote_code`` is a list of strings, it is treated as a list + of signing identities to verify against. Signing verification is not yet + implemented, so passing a list currently emits a warning and falls back + to the default trust check (i.e. only trusted publishers are allowed). + """ + if trust_remote_code is True: + return + + if isinstance(trust_remote_code, list): + warnings.warn( + "Signing identity verification is not yet implemented. " + "The provided signing identities will be ignored and the " + "kernel will be treated as untrusted. Use trust_remote_code=True " + "to bypass trust checks.", + stacklevel=3, + ) + + if constants.HF_HUB_OFFLINE: + # Publisher trust cannot be verified offline. The user opted into + # offline mode and the kernel must already be in the local cache, + # so trust was established when it was originally downloaded. + warnings.warn( + f"Skipping publisher trust check for '{repo_id}' because Hugging Face Hub is in offline mode.", + stacklevel=3, + ) + return + + publisher = repo_id.split("/", 1)[0] + + try: + info = _get_hf_api().get_organization_overview(publisher) + except Exception: + raise ValueError( + f"Kernel repository '{repo_id}' could not verify publisher trust status. " + "Set trust_remote_code=True to allow loading kernels from untrusted sources." + ) + + if getattr(info, "trustedKernelPublisher", False): + return + + raise ValueError( + f"Kernel repository '{repo_id}' is not from a trusted publisher. " + "Set trust_remote_code=True to allow loading kernels from untrusted sources." + ) diff --git a/kernels/src/kernels/locking.py b/kernels/src/kernels/locking.py index 1cafa4e7b..cb185eb2d 100644 --- a/kernels/src/kernels/locking.py +++ b/kernels/src/kernels/locking.py @@ -39,7 +39,7 @@ def get_kernel_locks(repo_id: str, version_spec: int) -> KernelLock: """ Get the locks for a kernel with the given version. """ - from kernels.utils import _get_hf_api + from kernels.hf_hub import _get_hf_api api = _get_hf_api() diff --git a/kernels/src/kernels/utils.py b/kernels/src/kernels/utils.py index e0a6d9845..e99174a6f 100644 --- a/kernels/src/kernels/utils.py +++ b/kernels/src/kernels/utils.py @@ -1,7 +1,6 @@ import functools import importlib import os -import platform import sys import warnings from dataclasses import dataclass @@ -12,11 +11,10 @@ from huggingface_hub.errors import LocalEntryNotFoundError from kernels_data import Metadata -from kernels._system import glibc_version from kernels._versions import select_revision_or_version -from kernels.backends import _backend, _select_backend -from kernels.compat import has_torch, has_tvm_ffi +from kernels.backends import _backend from kernels.deps import validate_dependencies +from kernels.hf_hub import CACHE_DIR, RepoInfo, _check_trust_remote_code, _get_hf_api from kernels.locking import ( get_caller_locked_kernel_revision, get_locked_kernel_revision, @@ -41,77 +39,6 @@ _BYTECODE_IGNORE_PATTERNS = ["*.pyc", "**/__pycache__/**"] -def _check_trust_remote_code(repo_id: str, trust_remote_code: bool | list[str]) -> None: - """Check whether a kernel repository is trusted. - - When ``trust_remote_code`` is ``False`` (the default), only repositories - whose publisher organization has ``trustedKernelPublisher`` enabled on the - Hub are allowed. Repositories from untrusted publishers will raise a - ``ValueError``. - - When ``trust_remote_code`` is ``True``, all repositories are allowed. - - When ``trust_remote_code`` is a list of strings, it is treated as a list - of signing identities to verify against. Signing verification is not yet - implemented, so passing a list currently emits a warning and falls back - to the default trust check (i.e. only trusted publishers are allowed). - """ - if trust_remote_code is True: - return - - if isinstance(trust_remote_code, list): - warnings.warn( - "Signing identity verification is not yet implemented. " - "The provided signing identities will be ignored and the " - "kernel will be treated as untrusted. Use trust_remote_code=True " - "to bypass trust checks.", - stacklevel=3, - ) - - if constants.HF_HUB_OFFLINE: - # Publisher trust cannot be verified offline. The user opted into - # offline mode and the kernel must already be in the local cache, - # so trust was established when it was originally downloaded. - warnings.warn( - f"Skipping publisher trust check for '{repo_id}' because Hugging Face Hub is in offline mode.", - stacklevel=3, - ) - return - - publisher = repo_id.split("/", 1)[0] - - try: - info = _get_hf_api().get_organization_overview(publisher) - except Exception: - raise ValueError( - f"Kernel repository '{repo_id}' could not verify publisher trust status. " - "Set trust_remote_code=True to allow loading kernels from untrusted sources." - ) - - if getattr(info, "trustedKernelPublisher", False): - return - - raise ValueError( - f"Kernel repository '{repo_id}' is not from a trusted publisher. " - "Set trust_remote_code=True to allow loading kernels from untrusted sources." - ) - - -@dataclass(frozen=True) -class RepoInfo: - """ - This dataclass stores the origin of the kernel. - - The following fields are available: - - - `repo_id` (`str`): the Hub repository containing the kernel. - - `revision` (`str`): the specific revision of the kernel. - """ - - repo_id: str - revision: str - - @dataclass(frozen=True) class LoadedKernel: """ @@ -166,11 +93,6 @@ def get_loaded_kernels() -> list[LoadedKernel]: return list(_loaded_kernels.values()) -def _get_cache_dir() -> str | None: - """Returns the kernels cache directory.""" - return os.environ.get("KERNELS_CACHE", None) - - def _get_local_kernel_overrides() -> dict[str, Path]: """Returns list local overrides for kernels.""" local_kerels = os.environ.get("LOCAL_KERNELS", None) @@ -194,9 +116,6 @@ def _parse_local_kernel_overrides(local_kernels: str) -> dict[str, Path]: return overrides -CACHE_DIR: str | None = _get_cache_dir() - - def _validate_variant_dependencies(variant_path: Path) -> None: metadata = Metadata.read_from_file(variant_path / "metadata.json") validate_dependencies(metadata.name.python_name, metadata.python_depends, _backend()) @@ -284,8 +203,6 @@ def install_kernel( backend (`str`, *optional*): The backend to load the kernel for. Can only be `cpu` or the backend that Torch is compiled for. The backend will be detected automatically if not provided. - variant_locks (`dict[str, VariantLock]`, *optional*): - Optional dictionary of variant locks for validation. user_agent (`Union[str, dict]`, *optional*): The `user_agent` info to pass to `snapshot_download()` for internal telemetry. validate_dependencies (`bool`, defaults to False): @@ -296,7 +213,7 @@ def install_kernel( `Path`: The path to the variant directory. """ api = _get_hf_api(user_agent=user_agent) - if local_files_only or constants.HF_HUB_OFFLINE: + if local_files_only: # Same local-cache resolution path used by `load_kernel`, which is # always offline. Sharing the helper avoids the network dependency # that `get_variants` would otherwise introduce. @@ -504,6 +421,7 @@ def get_kernel( revision=revision, user_agent=user_agent, validate_dependencies=True, + local_files_only=constants.HF_HUB_OFFLINE, ) return _import_from_path(variant_path, repo_info=repo_info) @@ -668,15 +586,8 @@ def load_kernel( f"Kernel `{repo_id}` is not locked. Please lock it with `kernels lock ` and then reinstall the project." ) - api = _get_hf_api() - try: - variant_path = _resolve_local_variant_path( - api, - repo_id, - revision=locked_sha, - backend=backend, - ) + variant_path = install_kernel(repo_id, revision=locked_sha, backend=backend, local_files_only=True) except FileNotFoundError as e: raise FileNotFoundError( f"Locked kernel `{repo_id}` was not downloaded or does not have an " @@ -686,7 +597,7 @@ def load_kernel( return _import_from_path(variant_path) -def get_locked_kernel(repo_id: str, local_files_only: bool = False) -> ModuleType: +def get_locked_kernel(repo_id: str) -> ModuleType: """ Get a kernel using a lock file. @@ -707,67 +618,8 @@ def get_locked_kernel(repo_id: str, local_files_only: bool = False) -> ModuleTyp variant_path = install_kernel( repo_id, revision=locked_sha, - local_files_only=local_files_only, + local_files_only=constants.HF_HUB_OFFLINE, validate_dependencies=True, ) return _import_from_path(variant_path) - - -def _platform() -> str: - cpu = platform.machine() - os = platform.system().lower() - - if os == "darwin": - cpu = "aarch64" if cpu == "arm64" else cpu - elif os == "windows": - cpu = "x86_64" if cpu == "AMD64" else cpu - - return f"{cpu}-{os}" - - -def _get_hf_api(user_agent: str | dict | None = None) -> HfApi: - """Returns an instance of HfApi with proper settings.""" - - from . import __version__ - - user_agent_str = "" - if not constants.HF_HUB_DISABLE_TELEMETRY: - parts: list[str] = [] - - # User-defined info - if isinstance(user_agent, dict): - parts.extend(f"{k}/{v}" for k, v in user_agent.items()) - elif isinstance(user_agent, str) and user_agent: - parts.append(user_agent) - - # System info - python = ".".join(platform.python_version_tuple()[:2]) - backend = _select_backend(None).variant_str - parts.extend( - [ - f"kernels/{__version__}", - f"python/{python}", - f"backend/{backend}", - f"platform/{_platform()}", - "file_type/kernel", - ] - ) - - if has_torch: - import torch - - parts.append(f"torch/{torch.__version__}") - if has_tvm_ffi: - import tvm_ffi - - parts.append(f"tvm-ffi/{tvm_ffi.__version__}") - - # Add glibc version if available - glibc = glibc_version() - if glibc is not None: - parts.append(f"glibc/{glibc}") - - user_agent_str = "; ".join(parts) - - return HfApi(library_name="kernels", library_version=__version__, user_agent=user_agent_str) diff --git a/kernels/tests/test_basic.py b/kernels/tests/test_basic.py index 5c9ce8dbd..93d66ea28 100644 --- a/kernels/tests/test_basic.py +++ b/kernels/tests/test_basic.py @@ -1,4 +1,5 @@ import logging +from pathlib import Path import pytest import torch @@ -7,7 +8,7 @@ from huggingface_hub.errors import HfHubHTTPError from kernels import get_kernel, get_local_kernel, has_kernel, install_kernel -from kernels._versions import resolve_version_spec_as_ref, select_revision_or_version +from kernels._versions import resolve_version_spec_as_ref @pytest.fixture @@ -252,17 +253,16 @@ def test_trust_remote_code_flag_allows_untrusted(): get_kernel("kernels-test-untrusted/ci-test-kernel", version=1, trust_remote_code=True) -def test_install_kernel_offline_with_revision(monkeypatch, local_kernel_path): +def test_install_kernel_offline_with_revision(local_kernel_path): """install_kernel should resolve a cached snapshot when HF_HUB_OFFLINE=1.""" expected_path = local_kernel_path - monkeypatch.setattr(constants, "HF_HUB_OFFLINE", True) - path = install_kernel("kernels-community/relu", revision="v1") + path = install_kernel("kernels-community/relu", revision="v1", local_files_only=True) assert path == expected_path def test_install_kernel_offline_avoids_network(monkeypatch, local_kernel_path): - """When HF_HUB_OFFLINE=1, install_kernel must not make any Hub requests.""" + """When offline, install_kernel must not make any Hub requests.""" expected_path = local_kernel_path class _NoNetwork(RuntimeError): @@ -279,8 +279,7 @@ def _fail(*_args, **_kwargs): # Offline mode resolves entirely from the local cache, so get_session is # never called. - monkeypatch.setattr(constants, "HF_HUB_OFFLINE", True) - path = install_kernel("kernels-community/relu", revision="v1") + path = install_kernel("kernels-community/relu", revision="v1", local_files_only=True) assert path == expected_path @@ -288,20 +287,17 @@ def test_install_kernel_offline_with_version(monkeypatch, local_kernel_path): """get_kernel(version=) should resolve via local refs when HF_HUB_OFFLINE=1.""" expected_path = local_kernel_path monkeypatch.setattr(constants, "HF_HUB_OFFLINE", True) - - commit = select_revision_or_version("kernels-community/relu", revision=None, version=1) - path = install_kernel("kernels-community/relu", revision=commit) + path = Path(get_kernel("kernels-community/relu", version=1).__file__).parent assert path == expected_path -def test_install_kernel_offline_uncached_revision(monkeypatch): +def test_install_kernel_offline_uncached_revision(): """install_kernel should fail with a helpful error when offline and uncached.""" - monkeypatch.setattr(constants, "HF_HUB_OFFLINE", True) - with pytest.raises(FileNotFoundError, match=r"local snapshot"): install_kernel( "kernels-test/this-repo-should-not-exist", revision="0000000000000000000000000000000000000000", + local_files_only=True, ) diff --git a/kernels/tests/test_loaded_kernels.py b/kernels/tests/test_loaded_kernels.py index ac22363e6..5530e747f 100644 --- a/kernels/tests/test_loaded_kernels.py +++ b/kernels/tests/test_loaded_kernels.py @@ -3,7 +3,8 @@ import pytest from kernels import get_kernel, get_loaded_kernels, get_local_kernel, install_kernel -from kernels.utils import LoadedKernel, RepoInfo, _loaded_kernels +from kernels.hf_hub import RepoInfo +from kernels.utils import LoadedKernel, _loaded_kernels _REPO_ID = "kernels-community/relu" _PACKAGE_NAME = "relu" diff --git a/kernels/tests/test_user_agent.py b/kernels/tests/test_user_agent.py index c87e62870..ab8d0bacf 100644 --- a/kernels/tests/test_user_agent.py +++ b/kernels/tests/test_user_agent.py @@ -1,6 +1,6 @@ import platform -from kernels.utils import _get_hf_api, _platform +from kernels.hf_hub import _get_hf_api, _platform def test_user_agent_contains_core_fields():