kernels 0.16.0__tar.gz → 0.16.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. {kernels-0.16.0 → kernels-0.16.2}/PKG-INFO +1 -1
  2. {kernels-0.16.0 → kernels-0.16.2}/pyproject.toml +1 -1
  3. kernels-0.16.2/src/kernels/archs.py +115 -0
  4. kernels-0.16.2/src/kernels/minver.py +69 -0
  5. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/utils.py +46 -7
  6. {kernels-0.16.0 → kernels-0.16.2}/src/kernels.egg-info/PKG-INFO +1 -1
  7. {kernels-0.16.0 → kernels-0.16.2}/src/kernels.egg-info/SOURCES.txt +4 -0
  8. kernels-0.16.2/tests/test_archs.py +158 -0
  9. kernels-0.16.2/tests/test_minver.py +112 -0
  10. {kernels-0.16.0 → kernels-0.16.2}/README.md +0 -0
  11. {kernels-0.16.0 → kernels-0.16.2}/setup.cfg +0 -0
  12. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/__init__.py +0 -0
  13. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/_system.py +0 -0
  14. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/_versions.py +0 -0
  15. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/_windows.py +0 -0
  16. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/backends.py +0 -0
  17. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/benchmark.py +0 -0
  18. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/benchmarks/__init__.py +0 -0
  19. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/benchmarks/activation.py +0 -0
  20. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/benchmarks/attention.py +0 -0
  21. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/benchmarks/layer_norm.py +0 -0
  22. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/cli/__init__.py +0 -0
  23. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/cli/benchmark.py +0 -0
  24. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/cli/benchmark_graphics.py +0 -0
  25. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/cli/verify_signature.py +0 -0
  26. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/cli/versions.py +0 -0
  27. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/compat.py +0 -0
  28. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/deps.py +0 -0
  29. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/layer/__init__.py +0 -0
  30. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/layer/_interval_tree.py +0 -0
  31. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/layer/device.py +0 -0
  32. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/layer/func.py +0 -0
  33. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/layer/globals.py +0 -0
  34. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/layer/kernelize.py +0 -0
  35. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/layer/layer.py +0 -0
  36. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/layer/mode.py +0 -0
  37. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/layer/repos.py +0 -0
  38. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/lockfile.py +0 -0
  39. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/python_depends.json +0 -0
  40. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/status.py +0 -0
  41. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/variants.py +0 -0
  42. {kernels-0.16.0 → kernels-0.16.2}/src/kernels/verify.py +0 -0
  43. {kernels-0.16.0 → kernels-0.16.2}/src/kernels.egg-info/dependency_links.txt +0 -0
  44. {kernels-0.16.0 → kernels-0.16.2}/src/kernels.egg-info/entry_points.txt +0 -0
  45. {kernels-0.16.0 → kernels-0.16.2}/src/kernels.egg-info/requires.txt +0 -0
  46. {kernels-0.16.0 → kernels-0.16.2}/src/kernels.egg-info/top_level.txt +0 -0
  47. {kernels-0.16.0 → kernels-0.16.2}/tests/test_basic.py +0 -0
  48. {kernels-0.16.0 → kernels-0.16.2}/tests/test_benchmarks.py +0 -0
  49. {kernels-0.16.0 → kernels-0.16.2}/tests/test_deps.py +0 -0
  50. {kernels-0.16.0 → kernels-0.16.2}/tests/test_doctest.py +0 -0
  51. {kernels-0.16.0 → kernels-0.16.2}/tests/test_func.py +0 -0
  52. {kernels-0.16.0 → kernels-0.16.2}/tests/test_interval_tree.py +0 -0
  53. {kernels-0.16.0 → kernels-0.16.2}/tests/test_kernel_locking.py +0 -0
  54. {kernels-0.16.0 → kernels-0.16.2}/tests/test_layer.py +0 -0
  55. {kernels-0.16.0 → kernels-0.16.2}/tests/test_loaded_kernels.py +0 -0
  56. {kernels-0.16.0 → kernels-0.16.2}/tests/test_status.py +0 -0
  57. {kernels-0.16.0 → kernels-0.16.2}/tests/test_tvm_ffi.py +0 -0
  58. {kernels-0.16.0 → kernels-0.16.2}/tests/test_user_agent.py +0 -0
  59. {kernels-0.16.0 → kernels-0.16.2}/tests/test_variants.py +0 -0
  60. {kernels-0.16.0 → kernels-0.16.2}/tests/test_verify.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kernels
3
- Version: 0.16.0
3
+ Version: 0.16.2
4
4
  Summary: Download compute kernels
5
5
  Author-email: Daniel de Kok <daniel@huggingface.co>, David Holtz <david@huggingface.co>
6
6
  License: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kernels"
3
- version = "0.16.0"
3
+ version = "0.16.2"
4
4
  description = "Download compute kernels"
5
5
  authors = [
6
6
  { name = "Daniel de Kok", email = "daniel@huggingface.co" },
@@ -0,0 +1,115 @@
1
+ from kernels_data import Backend, Metadata
2
+
3
+ from kernels.compat import has_torch
4
+
5
+
6
+ def _parse_cuda_arch(arch: str) -> tuple[int, int, str, bool] | None:
7
+ """Parse a CUDA arch into its capability, suffix, and PTX flag.
8
+
9
+ CUDA archs are compute capabilities like `9.0`, optionally with an
10
+ architecture-specific (`9.0a`) or family-specific (`10.0f`) suffix
11
+ and/or a `+PTX` flag (`9.0+PTX`). Returns `None` when the arch string
12
+ is not in this format.
13
+ """
14
+ ptx = False
15
+ if arch.endswith("+PTX"):
16
+ ptx = True
17
+ arch = arch[: -len("+PTX")]
18
+
19
+ suffix = ""
20
+ if arch.endswith(("a", "f")):
21
+ suffix = arch[-1]
22
+ arch = arch[:-1]
23
+
24
+ arch_major, sep, arch_minor = arch.partition(".")
25
+ if not sep or not arch_major.isdigit() or not arch_minor.isdigit():
26
+ return None
27
+
28
+ return int(arch_major), int(arch_minor), suffix, ptx
29
+
30
+
31
+ def _cuda_arch_supports(arch: str, capability: tuple[int, int]) -> bool | None:
32
+ """Check whether a single declared CUDA arch supports a compute capability.
33
+
34
+ Returns `None` when the arch string is not in a known format.
35
+ """
36
+ parsed = _parse_cuda_arch(arch)
37
+ if parsed is None:
38
+ return None
39
+
40
+ arch_major, arch_minor, suffix, ptx = parsed
41
+ major, minor = capability
42
+
43
+ if suffix == "a":
44
+ # Architecture-specific builds only run on that exact capability.
45
+ return (major, minor) == (arch_major, arch_minor)
46
+
47
+ if suffix == "f":
48
+ # Family-specific builds run on capabilities of the same generation
49
+ # with the same or a newer minor version.
50
+ return major == arch_major and minor >= arch_minor
51
+
52
+ if ptx:
53
+ # PTX is JIT-compiled for the current device, so the build also runs
54
+ # on any newer capability.
55
+ return (major, minor) >= (arch_major, arch_minor)
56
+
57
+ # Base builds run on capabilities of the same generation with the same
58
+ # or a newer minor version.
59
+ return major == arch_major and minor >= arch_minor
60
+
61
+
62
+ def _supports_cuda_capability(archs: list[str], capability: tuple[int, int]) -> bool:
63
+ supports = [_cuda_arch_supports(arch, capability) for arch in archs]
64
+ if all(support is None for support in supports):
65
+ # None of the arch strings are in a known format (e.g. produced by a
66
+ # newer kernel-builder), so compatibility cannot be determined.
67
+ return True
68
+ return any(supports)
69
+
70
+
71
+ def _check_arch_incompatibility(metadata: Metadata, variant: str) -> None:
72
+ """Check a kernel build against the architecture of the current device.
73
+
74
+ Raises `RuntimeError` when the build does not support the device.
75
+ """
76
+ archs = metadata.backend.archs
77
+ if not archs or not has_torch:
78
+ return
79
+
80
+ import torch
81
+
82
+ reason = None
83
+ backend_type = metadata.backend.backend_type
84
+ if backend_type == Backend.CUDA:
85
+ if torch.version.cuda is None or not torch.cuda.is_available():
86
+ return
87
+ major, minor = torch.cuda.get_device_capability()
88
+ if not _supports_cuda_capability(archs, (major, minor)):
89
+ reason = (
90
+ f"CUDA capability {major}.{minor} of the current device is not "
91
+ f"supported by the architectures of the build: {', '.join(archs)}"
92
+ )
93
+ elif backend_type == Backend.ROCm:
94
+ if torch.version.hip is None or not torch.cuda.is_available():
95
+ return
96
+ gcn_arch = torch.cuda.get_device_properties(torch.cuda.current_device()).gcnArchName
97
+ # Strip feature flags, e.g. `gfx90a:sramecc+:xnack-` -> `gfx90a`.
98
+ gcn_arch = gcn_arch.split(":")[0]
99
+ if gcn_arch not in archs:
100
+ reason = (
101
+ f"ROCm arch {gcn_arch} of the current device is not supported "
102
+ f"by the architectures of the build: {', '.join(archs)}"
103
+ )
104
+
105
+ if reason is None:
106
+ return
107
+
108
+ raise RuntimeError(
109
+ f"Kernel '{metadata.name}' variant '{variant}' does not support the "
110
+ f"current device: {reason}. The declared architectures can be "
111
+ "incomplete (e.g. when a kernel has a Triton fallback), pass "
112
+ "`check_arch=False` to skip this check. Warning: the kernel may "
113
+ "fail or crash the process at run time when it does not support "
114
+ "the device."
115
+ )
@@ -0,0 +1,69 @@
1
+ import json
2
+ import warnings
3
+ from pathlib import Path
4
+
5
+ from kernels_data import Metadata, Version
6
+ from packaging.version import InvalidVersion, parse
7
+
8
+
9
+ def _read_kernels_minver(variant_path: Path) -> Version | None:
10
+ """The minimum `kernels` version required by a build variant."""
11
+
12
+ # Note, for kernels >= 0.17, we read the version from the metadata,
13
+ # but to minimize the impact in the backport, we are reading from
14
+ # metadata.json here directly.
15
+
16
+ try:
17
+ with open(variant_path / "metadata.json", "rb") as f:
18
+ minver = json.load(f).get("kernels-minver")
19
+ except (OSError, ValueError):
20
+ return None
21
+
22
+ if not isinstance(minver, str):
23
+ return None
24
+
25
+ try:
26
+ return Version.from_str(minver)
27
+ except ValueError:
28
+ return None
29
+
30
+
31
+ def _installed_version() -> Version | None:
32
+ """The installed `kernels` version as a numeric version."""
33
+ # Avoid an import cycle.
34
+ from kernels import __version__
35
+
36
+ try:
37
+ release = parse(__version__).release
38
+ except InvalidVersion:
39
+ return None
40
+
41
+ # packaging < 22 returns a LegacyVersion with `release = None` for
42
+ # non-PEP 440 versions instead of raising `InvalidVersion`.
43
+ if release is None:
44
+ return None
45
+
46
+ return Version.from_str(".".join(str(part) for part in release))
47
+
48
+
49
+ def _warn_if_below_minver(metadata: Metadata, variant_path: Path) -> None:
50
+ """Warn when the installed `kernels` library is older than the minimum
51
+ version required by a kernel.
52
+ """
53
+ minver = _read_kernels_minver(variant_path)
54
+ if minver is None:
55
+ return
56
+
57
+ installed = _installed_version()
58
+ if installed is not None and installed < minver:
59
+ # Report the verbatim installed version, not the normalized one used
60
+ # for comparison, so the message matches what `pip show` reports.
61
+ from kernels import __version__
62
+
63
+ warnings.warn(
64
+ f"Kernel '{metadata.name}' variant '{variant_path.name}' requires "
65
+ f"kernels>={minver}, but version {__version__} is installed. "
66
+ "The kernel may not load or work correctly; upgrade with: "
67
+ "pip install --upgrade kernels",
68
+ stacklevel=3,
69
+ )
@@ -14,15 +14,17 @@ from pathlib import Path
14
14
  from types import ModuleType
15
15
 
16
16
  from huggingface_hub import HfApi, constants
17
- from huggingface_hub.errors import LocalEntryNotFoundError
17
+ from huggingface_hub.errors import EntryNotFoundError, LocalEntryNotFoundError
18
18
  from kernels_data import Metadata
19
19
 
20
20
  from kernels._system import glibc_version
21
21
  from kernels._versions import select_revision_or_version
22
+ from kernels.archs import _check_arch_incompatibility
22
23
  from kernels.backends import _backend, _select_backend
23
24
  from kernels.compat import has_torch, has_tvm_ffi
24
25
  from kernels.deps import validate_dependencies
25
26
  from kernels.lockfile import KernelLock, VariantLock
27
+ from kernels.minver import _warn_if_below_minver
26
28
  from kernels.status import resolve_status
27
29
  from kernels.variants import (
28
30
  Decision,
@@ -209,6 +211,7 @@ def _import_from_path(variant_path: Path, repo_info: RepoInfo | None = None) ->
209
211
  return loaded_kernel.module
210
212
 
211
213
  metadata = Metadata.read_from_file(variant_path / "metadata.json")
214
+ _warn_if_below_minver(metadata, variant_path)
212
215
  module_name = metadata.name.python_name
213
216
 
214
217
  file_path = variant_path / "__init__.py"
@@ -455,6 +458,7 @@ def get_kernel(
455
458
  backend: str | None = None,
456
459
  user_agent: str | dict | None = None,
457
460
  trust_remote_code: bool | list[str] = False,
461
+ check_arch: bool = True,
458
462
  ) -> ModuleType:
459
463
  """
460
464
  Load a kernel from the kernel hub.
@@ -481,6 +485,12 @@ def get_kernel(
481
485
  repositories are allowed. A list of strings will be used to verify signing
482
486
  identities in a future release; for now it emits a warning and falls
483
487
  back to the default trust check.
488
+ check_arch (`bool`, *optional*, defaults to `True`):
489
+ Whether to check that the kernel build supports the architecture
490
+ (e.g. CUDA compute capability) of the current device. Kernels can
491
+ support more architectures than they declare (e.g. through a
492
+ Triton fallback), `check_arch=False` skips the check for such
493
+ kernels.
484
494
 
485
495
  Returns:
486
496
  `ModuleType`: The imported kernel module.
@@ -514,6 +524,9 @@ def get_kernel(
514
524
  user_agent=user_agent,
515
525
  validate_dependencies=True,
516
526
  )
527
+ if check_arch:
528
+ metadata = Metadata.read_from_file(variant_path / "metadata.json")
529
+ _check_arch_incompatibility(metadata, variant_path.name)
517
530
  return _import_from_path(variant_path, repo_info=repo_info)
518
531
 
519
532
 
@@ -558,6 +571,7 @@ def has_kernel(
558
571
  revision: str | None = None,
559
572
  version: int | None = None,
560
573
  backend: str | None = None,
574
+ check_arch: bool = True,
561
575
  ) -> bool:
562
576
  """
563
577
  Check whether a kernel build exists for the current environment (Torch version and compute framework).
@@ -573,6 +587,12 @@ def has_kernel(
573
587
  backend (`str`, *optional*):
574
588
  The backend to load the kernel for. Can only be `cpu` or the backend that Torch is compiled for.
575
589
  The backend will be detected automatically if not provided.
590
+ check_arch (`bool`, *optional*, defaults to `True`):
591
+ Whether to check that the kernel build supports the architecture
592
+ (e.g. CUDA compute capability) of the current device. Kernels can
593
+ support more architectures than they declare (e.g. through a
594
+ Triton fallback), `check_arch=False` skips the check for such
595
+ kernels.
576
596
 
577
597
  Returns:
578
598
  `bool`: `True` if a kernel is available for the current environment.
@@ -586,12 +606,31 @@ def has_kernel(
586
606
  if variant is None:
587
607
  return False
588
608
 
589
- return api.file_exists(
590
- repo_id,
591
- repo_type="kernel",
592
- revision=revision,
593
- filename=f"build/{variant.variant_str}/metadata.json",
594
- )
609
+ if not check_arch:
610
+ return api.file_exists(
611
+ repo_id,
612
+ repo_type="kernel",
613
+ revision=revision,
614
+ filename=f"build/{variant.variant_str}/metadata.json",
615
+ )
616
+
617
+ try:
618
+ metadata_path = api.hf_hub_download(
619
+ repo_id,
620
+ repo_type="kernel",
621
+ revision=revision,
622
+ filename=f"build/{variant.variant_str}/metadata.json",
623
+ cache_dir=CACHE_DIR,
624
+ )
625
+ except EntryNotFoundError:
626
+ return False
627
+
628
+ try:
629
+ _check_arch_incompatibility(Metadata.read_from_file(metadata_path), variant.variant_str)
630
+ except RuntimeError:
631
+ return False
632
+
633
+ return True
595
634
 
596
635
 
597
636
  def get_kernel_variants(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kernels
3
- Version: 0.16.0
3
+ Version: 0.16.2
4
4
  Summary: Download compute kernels
5
5
  Author-email: Daniel de Kok <daniel@huggingface.co>, David Holtz <david@huggingface.co>
6
6
  License: Apache-2.0
@@ -4,11 +4,13 @@ src/kernels/__init__.py
4
4
  src/kernels/_system.py
5
5
  src/kernels/_versions.py
6
6
  src/kernels/_windows.py
7
+ src/kernels/archs.py
7
8
  src/kernels/backends.py
8
9
  src/kernels/benchmark.py
9
10
  src/kernels/compat.py
10
11
  src/kernels/deps.py
11
12
  src/kernels/lockfile.py
13
+ src/kernels/minver.py
12
14
  src/kernels/python_depends.json
13
15
  src/kernels/status.py
14
16
  src/kernels/utils.py
@@ -38,6 +40,7 @@ src/kernels/layer/kernelize.py
38
40
  src/kernels/layer/layer.py
39
41
  src/kernels/layer/mode.py
40
42
  src/kernels/layer/repos.py
43
+ tests/test_archs.py
41
44
  tests/test_basic.py
42
45
  tests/test_benchmarks.py
43
46
  tests/test_deps.py
@@ -47,6 +50,7 @@ tests/test_interval_tree.py
47
50
  tests/test_kernel_locking.py
48
51
  tests/test_layer.py
49
52
  tests/test_loaded_kernels.py
53
+ tests/test_minver.py
50
54
  tests/test_status.py
51
55
  tests/test_tvm_ffi.py
52
56
  tests/test_user_agent.py
@@ -0,0 +1,158 @@
1
+ import json
2
+
3
+ import pytest
4
+ import torch
5
+ from kernels_data import Metadata
6
+
7
+ from kernels import get_kernel, has_kernel, install_kernel
8
+ from kernels.archs import _check_arch_incompatibility, _supports_cuda_capability
9
+
10
+
11
+ def make_metadata(backend_type: str, archs: list[str] | None) -> Metadata:
12
+ return Metadata.from_bytes(
13
+ json.dumps(
14
+ {
15
+ "name": "test-kernel",
16
+ "id": "test_id",
17
+ "version": 1,
18
+ "license": "mit",
19
+ "python-depends": [],
20
+ "backend": {"type": backend_type, "archs": archs},
21
+ }
22
+ ).encode("utf-8")
23
+ )
24
+
25
+
26
+ @pytest.mark.parametrize(
27
+ "archs,capability,supported",
28
+ [
29
+ # Base archs run on the same generation with the same or a newer
30
+ # minor capability.
31
+ (["8.0"], (8, 0), True),
32
+ (["8.0"], (8, 6), True),
33
+ (["8.6"], (8, 0), False),
34
+ (["8.0"], (7, 5), False),
35
+ (["8.0"], (9, 0), False),
36
+ (["8.0", "9.0"], (9, 0), True),
37
+ # Architecture-specific archs only run on that exact capability.
38
+ (["9.0a"], (9, 0), True),
39
+ (["9.0a"], (9, 1), False),
40
+ (["8.0", "9.0a"], (10, 0), False),
41
+ # Family-specific archs run on the same generation with the same or
42
+ # a newer minor capability.
43
+ (["10.0f"], (10, 0), True),
44
+ (["10.0f"], (10, 3), True),
45
+ (["10.0f"], (12, 0), False),
46
+ # PTX is JIT-compiled for the current device, so builds with `+PTX`
47
+ # also run on any newer capability.
48
+ (["9.0+PTX"], (9, 0), True),
49
+ (["9.0+PTX"], (9, 1), True),
50
+ (["9.0+PTX"], (10, 0), True),
51
+ (["9.0+PTX"], (12, 1), True),
52
+ (["9.0+PTX"], (8, 6), False),
53
+ # ...but the `a` and `f` suffixes keep their own semantics.
54
+ (["9.0a+PTX"], (9, 0), True),
55
+ (["9.0a+PTX"], (10, 0), False),
56
+ (["10.0f+PTX"], (10, 3), True),
57
+ (["10.0f+PTX"], (12, 0), False),
58
+ # Arch strings in an unknown format do not count as a match...
59
+ (["garbage", "8.0"], (9, 0), False),
60
+ # ...but when no arch string can be parsed, compatibility cannot be
61
+ # determined and the build is not rejected.
62
+ (["garbage"], (9, 0), True),
63
+ ],
64
+ )
65
+ def test_supports_cuda_capability(archs, capability, supported):
66
+ assert _supports_cuda_capability(archs, capability) == supported
67
+
68
+
69
+ @pytest.fixture
70
+ def fake_cuda_device(monkeypatch):
71
+ monkeypatch.setattr(torch.version, "cuda", "12.8", raising=False)
72
+ monkeypatch.setattr(torch.version, "hip", None, raising=False)
73
+ monkeypatch.setattr(torch.cuda, "is_available", lambda: True)
74
+ monkeypatch.setattr(torch.cuda, "get_device_capability", lambda device=None: (10, 0))
75
+
76
+
77
+ @pytest.fixture
78
+ def fake_rocm_device(monkeypatch):
79
+ class FakeProperties:
80
+ gcnArchName = "gfx90a:sramecc+:xnack-"
81
+
82
+ monkeypatch.setattr(torch.version, "cuda", None, raising=False)
83
+ monkeypatch.setattr(torch.version, "hip", "6.4.0", raising=False)
84
+ monkeypatch.setattr(torch.cuda, "is_available", lambda: True)
85
+ monkeypatch.setattr(torch.cuda, "current_device", lambda: 0)
86
+ monkeypatch.setattr(torch.cuda, "get_device_properties", lambda device: FakeProperties())
87
+
88
+
89
+ def test_cuda_incompatible_arch_is_rejected(fake_cuda_device):
90
+ with pytest.raises(RuntimeError) as exc_info:
91
+ _check_arch_incompatibility(make_metadata("cuda", ["8.0", "9.0a"]), "test-variant")
92
+ assert "test-variant" in str(exc_info.value)
93
+ assert "CUDA capability 10.0" in str(exc_info.value)
94
+ assert "8.0, 9.0a" in str(exc_info.value)
95
+
96
+
97
+ def test_cuda_compatible_arch_is_accepted(fake_cuda_device):
98
+ for archs in (["8.0", "10.0"], ["10.0a"], ["10.0f"]):
99
+ _check_arch_incompatibility(make_metadata("cuda", archs), "test-variant")
100
+
101
+
102
+ def test_noarch_build_is_accepted(fake_cuda_device):
103
+ # Backends that support archs (e.g. CUDA) can have builds that do not
104
+ # declare any (noarch kernels, e.g. pure Triton builds). Such builds are
105
+ # never rejected.
106
+ _check_arch_incompatibility(make_metadata("cuda", None), "test-variant")
107
+ _check_arch_incompatibility(make_metadata("cuda", []), "test-variant")
108
+
109
+
110
+ def test_rocm_arch_check(fake_rocm_device):
111
+ _check_arch_incompatibility(make_metadata("rocm", ["gfx90a", "gfx942"]), "test-variant")
112
+ _check_arch_incompatibility(make_metadata("rocm", None), "test-variant")
113
+ with pytest.raises(RuntimeError) as exc_info:
114
+ _check_arch_incompatibility(make_metadata("rocm", ["gfx942"]), "test-variant")
115
+ assert "ROCm arch gfx90a" in str(exc_info.value)
116
+ assert "gfx942" in str(exc_info.value)
117
+
118
+
119
+ def test_check_skipped_without_device(monkeypatch):
120
+ monkeypatch.setattr(torch.version, "cuda", "12.8", raising=False)
121
+ monkeypatch.setattr(torch.cuda, "is_available", lambda: False)
122
+ _check_arch_incompatibility(make_metadata("cuda", ["8.0"]), "test-variant")
123
+
124
+
125
+ def test_check_skipped_for_backends_without_archs(fake_cuda_device):
126
+ # Archs of other backends cannot be checked against the current device.
127
+ _check_arch_incompatibility(make_metadata("cpu", None), "test-variant")
128
+ _check_arch_incompatibility(make_metadata("metal", ["applegpu_g13"]), "test-variant")
129
+
130
+
131
+ def test_issue_707_fa3_on_b200(fake_cuda_device):
132
+ # https://github.com/huggingface/kernels/issues/707: flash-attn3 only
133
+ # declares sm_80/sm_90a archs, but loading it on a B200 (capability 10.0)
134
+ # succeeded and the first launch exited the process. The declared archs
135
+ # must be rejected for this device.
136
+ metadata = make_metadata("cuda", ["8.0", "9.0a"])
137
+ with pytest.raises(RuntimeError, match="does not support the current device"):
138
+ _check_arch_incompatibility(metadata, "test-variant")
139
+
140
+
141
+ @pytest.mark.cuda_only
142
+ def test_get_kernel_rejects_unsupported_capability(monkeypatch):
143
+ variant_path = install_kernel("kernels-community/relu", revision="v1")
144
+ metadata = Metadata.read_from_file(variant_path / "metadata.json")
145
+ if not metadata.backend.archs:
146
+ pytest.skip("kernel build does not declare archs")
147
+
148
+ # ReLU kernel supports PTX, so we need an old version.
149
+ monkeypatch.setattr(torch.cuda, "get_device_capability", lambda device=None: (1, 0))
150
+
151
+ with pytest.raises(RuntimeError, match="does not support the current device"):
152
+ get_kernel("kernels-community/relu", version=1)
153
+ assert not has_kernel("kernels-community/relu", version=1)
154
+
155
+ # The opt-out only checks that a compatible build variant exists.
156
+ assert has_kernel("kernels-community/relu", version=1, check_arch=False)
157
+ kernel = get_kernel("kernels-community/relu", version=1, check_arch=False)
158
+ assert kernel is not None
@@ -0,0 +1,112 @@
1
+ import json
2
+ import re
3
+
4
+ import pytest
5
+ from kernels_data import Metadata, Version
6
+
7
+ import kernels
8
+ from kernels.minver import _installed_version, _warn_if_below_minver
9
+ from kernels.utils import _import_from_path, _loaded_kernels
10
+
11
+
12
+ def _write_variant(tmp_path, minver):
13
+ variant_dir = tmp_path / "build" / "torch28-cxx11-cu128-x86_64-linux"
14
+ variant_dir.mkdir(parents=True)
15
+ metadata = {
16
+ "id": "activation_1_cuda",
17
+ "name": "activation",
18
+ "version": 1,
19
+ "license": "Apache-2.0",
20
+ "python-depends": ["torch"],
21
+ "backend": {"type": "cuda"},
22
+ }
23
+ if minver is not None:
24
+ metadata["kernels-minver"] = minver
25
+ (variant_dir / "metadata.json").write_text(json.dumps(metadata))
26
+ return variant_dir
27
+
28
+
29
+ @pytest.mark.parametrize("minver", [None, "0.0.1"])
30
+ def test_no_warning_when_minver_met(tmp_path, recwarn, minver):
31
+ variant_dir = _write_variant(tmp_path, minver)
32
+ metadata = Metadata.read_from_file(variant_dir / "metadata.json")
33
+ _warn_if_below_minver(metadata, variant_dir)
34
+ assert len(recwarn) == 0
35
+
36
+
37
+ def test_no_warning_for_dev_version_of_required_release(tmp_path, recwarn, monkeypatch):
38
+ # A development version implements the release it leads up to, so
39
+ # `0.17.0.dev0` must satisfy a `0.17.0` requirement.
40
+ monkeypatch.setattr(kernels, "__version__", "0.17.0.dev0")
41
+ variant_dir = _write_variant(tmp_path, "0.17.0")
42
+ metadata = Metadata.read_from_file(variant_dir / "metadata.json")
43
+ _warn_if_below_minver(metadata, variant_dir)
44
+ assert len(recwarn) == 0
45
+
46
+
47
+ @pytest.mark.parametrize(
48
+ "installed",
49
+ ["0.17.0.dev0", "0.17.0rc1", "0.17.0.post1", "0.17.0+cu121", "0.17"],
50
+ )
51
+ def test_installed_version_uses_release_segment(monkeypatch, installed):
52
+ monkeypatch.setattr(kernels, "__version__", installed)
53
+ assert _installed_version() == Version.from_str("0.17.0")
54
+
55
+
56
+ def test_installed_version_is_none_for_non_pep440_version(monkeypatch):
57
+ monkeypatch.setattr(kernels, "__version__", "0.17.0-dirty")
58
+ assert _installed_version() is None
59
+
60
+
61
+ def test_unparseable_installed_version_does_not_fail_load(tmp_path, recwarn, monkeypatch):
62
+ # A version that cannot be compared must not turn this advisory check into
63
+ # a hard failure.
64
+ monkeypatch.setattr(kernels, "__version__", "0.17.0-dirty")
65
+ variant_dir = _write_variant(tmp_path, "999.1.0")
66
+ metadata = Metadata.read_from_file(variant_dir / "metadata.json")
67
+ _warn_if_below_minver(metadata, variant_dir)
68
+ assert len(recwarn) == 0
69
+
70
+
71
+ @pytest.mark.parametrize("minver", [42, "", "not-a-version"])
72
+ def test_invalid_minver_is_ignored(tmp_path, recwarn, minver):
73
+ # A `kernels-minver` that cannot be parsed (e.g. written by a future
74
+ # version that changed the format) must not fail the load.
75
+ variant_dir = _write_variant(tmp_path, minver)
76
+ metadata = Metadata.read_from_file(variant_dir / "metadata.json")
77
+ _warn_if_below_minver(metadata, variant_dir)
78
+ assert len(recwarn) == 0
79
+
80
+
81
+ def test_version_ordering_is_numeric_not_lexicographic():
82
+ # `0.9 < 0.10` only holds for numeric comparison; string comparison would
83
+ # get this backwards.
84
+ assert Version.from_str("0.9") < Version.from_str("0.10")
85
+ assert Version.from_str("0.14") == Version.from_str("0.14.0")
86
+ assert Version.from_str("0.14.0") < Version.from_str("0.14.1")
87
+
88
+
89
+ def test_warns_when_minver_not_met(tmp_path):
90
+ variant_dir = _write_variant(tmp_path, "999.1.0")
91
+ metadata = Metadata.read_from_file(variant_dir / "metadata.json")
92
+ with pytest.warns(UserWarning, match="requires kernels>=999.1"):
93
+ _warn_if_below_minver(metadata, variant_dir)
94
+
95
+
96
+ def test_warning_mentions_installed_version(tmp_path):
97
+ variant_dir = _write_variant(tmp_path, "999.1.0")
98
+ metadata = Metadata.read_from_file(variant_dir / "metadata.json")
99
+ with pytest.warns(UserWarning, match=f"version {re.escape(kernels.__version__)} is installed"):
100
+ _warn_if_below_minver(metadata, variant_dir)
101
+
102
+
103
+ def test_import_from_path_warns_on_unmet_minver(tmp_path):
104
+ variant_dir = _write_variant(tmp_path, "999.1.0")
105
+ (variant_dir / "__init__.py").write_text("value = 42\n")
106
+ _loaded_kernels.pop(variant_dir, None)
107
+ try:
108
+ with pytest.warns(UserWarning, match="requires kernels>=999.1"):
109
+ module = _import_from_path(variant_dir)
110
+ assert module.value == 42
111
+ finally:
112
+ _loaded_kernels.pop(variant_dir, None)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes