Skip to content

Commit b0c431f

Browse files
authored
Add the kernels check subcommand (#158)
* Add the `kernels check` subcommand This subcommand checks a given kernel. Currently it applies the same ABI checks as `kernel-abi-check` in `kernel-builder`. * Print an error when `build` contains files * Forgot to update has_issues in two places
1 parent 9a188ea commit b0c431f

File tree

7 files changed

+240
-1
lines changed

7 files changed

+240
-1
lines changed

.github/workflows/test.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ jobs:
7373
run: |
7474
uv run kernels generate-readme kernels-community/triton-layer-norm
7575
76+
- name: Check kernel check
77+
run: |
78+
uv pip install kernel-abi-check
79+
kernels check kernels-community/activation
80+
7681
- name: Import check without torch
7782
run: |
7883
uv pip uninstall torch

docs/source/cli.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,24 @@
22

33
## Main Functions
44

5+
### kernels check
6+
7+
You can use `kernels check` to test compliance of a kernel on the Hub.
8+
This currently checks that the kernel:
9+
10+
- Supports the currently-required Python ABI version.
11+
- Works on supported operating system versions.
12+
13+
For example:
14+
15+
```bash
16+
$ kernels check kernels-community/flash-attn3
17+
Checking variant: torch28-cxx11-cu128-aarch64-linux
18+
🐍 Python ABI 3.9 compatible
19+
🐧 manylinux_2_28 compatible
20+
[...]
21+
```
22+
523
### kernels to-wheel
624

725
We strongly recommend downloading kernels from the Hub using the `kernels`
@@ -38,4 +56,3 @@ your kernel builds to the Hub.
3856
- If a repo with the `repo_id` already exists and if it contains a `build` with the build variant
3957
being uploaded, it will attempt to delete the files existing under it.
4058
- Make sure to be authenticated (run `hf auth login` if not) to be able to perform uploads to the Hub.
41-

flake.nix

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
in
2525
{
2626
formatter = pkgs.nixfmt-tree;
27+
packages.kernel-abi-check = pkgs.python3.pkgs.callPackage ./nix/kernel-abi-check.nix {};
2728
devShells = with pkgs; rec {
2829
default = mkShell {
2930
nativeBuildInputs = [
@@ -40,6 +41,7 @@
4041
++ (with python3.pkgs; [
4142
docutils
4243
huggingface-hub
44+
(callPackage ./nix/kernel-abi-check.nix {})
4345
mktestdocs
4446
pytest
4547
pytest-benchmark

nix/kernel-abi-check.nix

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
buildPythonPackage,
3+
fetchPypi,
4+
rustPlatform,
5+
}:
6+
7+
buildPythonPackage rec {
8+
pname = "kernel-abi-check";
9+
version = "0.6.2";
10+
11+
src = fetchPypi {
12+
inherit version;
13+
pname = "kernel_abi_check";
14+
hash = "sha256-goWC7SK79FVNEvkp3bISBwbOqdSrmobANtrWIve9/Ys=";
15+
};
16+
17+
cargoDeps = rustPlatform.fetchCargoVendor {
18+
inherit pname version src sourceRoot;
19+
hash = "sha256-+1jdbKsDKmG+bf0NEVYMv8t7Meuge1z2cgYfbdB9q8A=";
20+
};
21+
22+
sourceRoot = "kernel_abi_check-${version}/bindings/python";
23+
24+
pyproject = true;
25+
26+
nativeBuildInputs = with rustPlatform; [ cargoSetupHook maturinBuildHook ];
27+
}

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ dev = [
3434
]
3535

3636
[project.optional-dependencies]
37+
abi-check = ["kernel-abi-check>=0.6.2,<0.7.0"]
3738
torch = ["torch"]
3839
docs = [
3940
"hf-doc-builder",

src/kernels/check.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
from pathlib import Path
2+
import sys
3+
4+
from huggingface_hub import snapshot_download
5+
from kernels.utils import CACHE_DIR
6+
from kernel_abi_check import (
7+
BinaryFormat,
8+
IncompatibleMacOSVersion,
9+
ObjectFile,
10+
IncompatibleAbi3Symbol,
11+
NonAbi3Symbol,
12+
IncompatibleManylinuxSymbol,
13+
MissingMacOSVersion,
14+
)
15+
16+
17+
def check_kernel(
18+
*, macos: str, manylinux: str, python_abi: str, repo_id: str, revision: str
19+
):
20+
variants_path = (
21+
Path(
22+
snapshot_download(
23+
repo_id,
24+
allow_patterns=["build/*"],
25+
cache_dir=CACHE_DIR,
26+
revision=revision,
27+
)
28+
)
29+
/ "build"
30+
)
31+
32+
has_issues = False
33+
for variant_path in variants_path.iterdir():
34+
if not variant_path.is_dir():
35+
print(
36+
f"⛔ `build/` must only contain directories, found: {variant_path.name}",
37+
file=sys.stderr,
38+
)
39+
has_issues = True
40+
continue
41+
42+
print(f"Checking variant: {variant_path.name}", file=sys.stderr)
43+
44+
indent = 2
45+
46+
for dylib_path in variant_path.rglob("*.so"):
47+
print_with_indent(
48+
indent,
49+
f"Dynamic library {dylib_path.relative_to(variant_path)}:",
50+
)
51+
52+
o = ObjectFile(dylib_path)
53+
has_issues |= check_abi3(o, python_abi, indent + 2)
54+
55+
# TODO: also check operating system
56+
if o.format() == BinaryFormat.ELF:
57+
has_issues |= check_manylinux(o, manylinux, indent + 2)
58+
elif o.format() == BinaryFormat.MACH_O:
59+
has_issues |= check_macos(o, macos, indent + 2)
60+
61+
if has_issues:
62+
sys.exit(1)
63+
64+
65+
def check_abi3(object_file: ObjectFile, python_abi: str, indent: int) -> bool:
66+
has_issues = False
67+
violations = object_file.check_python_abi(python_abi)
68+
if violations != []:
69+
has_issues = True
70+
print_with_indent(
71+
indent,
72+
f"⛔ Found symbols that are incompatible with Python ABI {python_abi}:",
73+
)
74+
for violation in violations:
75+
if isinstance(violation, IncompatibleAbi3Symbol):
76+
print_with_indent(
77+
indent + 3,
78+
f"{violation.name}: {violation.version_added}",
79+
)
80+
elif isinstance(violation, NonAbi3Symbol):
81+
print_with_indent(
82+
indent + 3,
83+
f"{violation.name}",
84+
)
85+
else:
86+
print_with_indent(indent, f"🐍 Python ABI {python_abi} compatible")
87+
88+
return has_issues
89+
90+
91+
def check_macos(object_file: ObjectFile, macos: str, indent: int) -> bool:
92+
has_issues = False
93+
violations = object_file.check_macos(macos)
94+
if violations != []:
95+
has_issues = True
96+
print_with_indent(
97+
indent,
98+
f"⛔ Found incompatibility with macOS {macos}:",
99+
)
100+
101+
for violation in violations:
102+
if isinstance(violation, MissingMacOSVersion):
103+
print_with_indent(
104+
indent + 3,
105+
"shared library does not contain macOS version",
106+
)
107+
elif isinstance(violation, IncompatibleMacOSVersion):
108+
print_with_indent(
109+
indent + 3,
110+
f"shared library requires macOS {violation.version}",
111+
)
112+
else:
113+
print_with_indent(indent, f"🍏 compatible with macOS {macos}")
114+
115+
return has_issues
116+
117+
118+
def check_manylinux(object_file: ObjectFile, manylinux: str, indent: int) -> bool:
119+
has_issues = False
120+
violations = object_file.check_manylinux(manylinux)
121+
if violations != []:
122+
has_issues = True
123+
print_with_indent(
124+
indent,
125+
f"⛔ Found symbols that are incompatible with {manylinux}:",
126+
)
127+
128+
for violation in violations:
129+
if isinstance(violation, IncompatibleManylinuxSymbol):
130+
print_with_indent(
131+
indent + 3,
132+
f"{violation.name}_{violation.dep}: {violation.version}",
133+
)
134+
else:
135+
print_with_indent(indent, f"🐧 {manylinux} compatible")
136+
137+
return has_issues
138+
139+
140+
def print_with_indent(indent: int, message: str):
141+
print(f"{' ' * indent}{message}", file=sys.stderr)

src/kernels/cli.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,31 @@ def main():
2020
)
2121
subparsers = parser.add_subparsers(required=True)
2222

23+
check_parser = subparsers.add_parser("check", help="Check a kernel for compliance")
24+
check_parser.add_argument("repo_id", type=str, help="The kernel repo ID")
25+
check_parser.add_argument(
26+
"--revision",
27+
type=str,
28+
default="main",
29+
help="The kernel revision (branch, tag, or commit SHA, defaults to 'main')",
30+
)
31+
check_parser.add_argument("--macos", type=str, help="macOS version", default="15.0")
32+
check_parser.add_argument(
33+
"--manylinux", type=str, help="Manylinux version", default="manylinux_2_28"
34+
)
35+
check_parser.add_argument(
36+
"--python-abi", type=str, help="Python ABI version", default="3.9"
37+
)
38+
check_parser.set_defaults(
39+
func=lambda args: check_kernel(
40+
macos=args.macos,
41+
manylinux=args.manylinux,
42+
python_abi=args.python_abi,
43+
repo_id=args.repo_id,
44+
revision=args.revision,
45+
)
46+
)
47+
2348
download_parser = subparsers.add_parser("download", help="Download locked kernels")
2449
download_parser.add_argument(
2550
"project_dir",
@@ -205,3 +230,24 @@ def default(self, o):
205230
if dataclasses.is_dataclass(o):
206231
return dataclasses.asdict(o)
207232
return super().default(o)
233+
234+
235+
def check_kernel(
236+
*, macos: str, manylinux: str, python_abi: str, repo_id: str, revision: str
237+
):
238+
try:
239+
import kernels.check
240+
except ImportError:
241+
print(
242+
"`kernels check` requires the `kernel-abi-check` package: pip install kernel-abi-check",
243+
file=sys.stderr,
244+
)
245+
sys.exit(1)
246+
247+
kernels.check.check_kernel(
248+
macos=macos,
249+
manylinux=manylinux,
250+
python_abi=python_abi,
251+
repo_id=repo_id,
252+
revision=revision,
253+
)

0 commit comments

Comments
 (0)