Skip to content

Commit 4d2725c

Browse files
committed
Add check for microkernels missing from benchmarking.
Signed-off-by: Cathal Lawlor <cathal.lawlor@arm.com> Reviewed-by: james-gross-arm <james.gross@arm.com> Approved-by: Emil Ohlsson <emil.ohlsson@arm.com>
1 parent 89c3ed4 commit 4d2725c

2 files changed

Lines changed: 189 additions & 0 deletions

File tree

.gitlab-ci.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,15 @@ test-clang-tidy:
254254
# Only test the main library with C/C++ files.
255255
- clang-tidy --header-filter ".*" --warnings-as-errors "*" -p build/${CI_JOB_NAME_SLUG} $(find kai -type f \( -name \*.c -o -name \*.cpp \))
256256

257+
# Purpose: Verify that all micro-kernels are registered for benchmarking.
258+
test-verify-benchmark-registration:
259+
extends:
260+
- .standard-rules
261+
stage: test
262+
needs: []
263+
script:
264+
- python3 tools/check_benchmark_microkernels.py
265+
257266
# Purpose: Run pre-commit hooks (formatting, linting) with cached tool downloads.
258267
pre-commit-hooks:
259268
variables:
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
#!/usr/bin/env python3
2+
#
3+
# SPDX-FileCopyrightText: Copyright 2025-2026 Arm Limited and/or its affiliates <open-source-office@arm.com>
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
#
7+
"""
8+
Validate that every matmul, imatmul, or dwconv micro-kernel header has a benchmark registration.
9+
"""
10+
from __future__ import annotations
11+
12+
import argparse
13+
import os
14+
import re
15+
import sys
16+
from collections import defaultdict
17+
from typing import Optional
18+
from typing import Sequence
19+
from typing import Set
20+
21+
INCLUDE_RE = re.compile(r'^\s*#\s*include\s*["<]([^">]+)[">]')
22+
BLOCK_COMMENT_RE = re.compile(r"/\*.*?\*/", re.DOTALL)
23+
LINE_COMMENT_RE = re.compile(r"//.*?$", re.MULTILINE)
24+
25+
SRC_EXTS = {".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx"}
26+
27+
28+
def strip_comments(text: str) -> str:
29+
text = BLOCK_COMMENT_RE.sub("", text)
30+
text = LINE_COMMENT_RE.sub("", text)
31+
return text
32+
33+
34+
def iter_files(root: str):
35+
for dirpath, _, files in os.walk(root):
36+
for filename in files:
37+
if os.path.splitext(filename)[1].lower() in SRC_EXTS:
38+
yield os.path.join(dirpath, filename)
39+
40+
41+
def classify_kernel(path: str) -> Optional[str]:
42+
parts = path.split("/")
43+
if not parts:
44+
return None
45+
head = parts[0]
46+
if head == "dwconv":
47+
return "dwconv"
48+
if head != "matmul":
49+
return None
50+
if len(parts) > 1 and parts[1].startswith("imatmul"):
51+
return "imatmul"
52+
return "matmul"
53+
54+
55+
def gather_includes(
56+
src_dir: str, include_prefix: str, kernel_types: Set[str]
57+
) -> Set[str]:
58+
used: Set[str] = set()
59+
# Normalize include prefix for comparison so that Windows and Unix paths match
60+
prefix = include_prefix.replace("\\", "/").rstrip("/")
61+
plen = len(prefix)
62+
for path in iter_files(src_dir):
63+
try:
64+
with open(path, "r", encoding="utf-8", errors="ignore") as fh:
65+
content = strip_comments(fh.read())
66+
except OSError:
67+
continue
68+
for line in content.splitlines():
69+
match = INCLUDE_RE.match(line)
70+
if not match:
71+
continue
72+
inc = match.group(1).replace("\\", "/")
73+
if not inc.startswith(prefix):
74+
continue
75+
rel = inc[plen + 1 :]
76+
kernel_type = classify_kernel(rel)
77+
if kernel_type in kernel_types:
78+
used.add(rel)
79+
return used
80+
81+
82+
def list_present(ukernels_dir: str, kernel_types: Set[str]) -> Set[str]:
83+
present: Set[str] = set()
84+
base = os.path.abspath(ukernels_dir)
85+
for dirpath, _, files in os.walk(base):
86+
for filename in files:
87+
_, ext = os.path.splitext(filename)
88+
if ext.lower() not in {".h", ".hh", ".hpp", ".hxx"}:
89+
continue
90+
rel_path = os.path.relpath(os.path.join(dirpath, filename), base).replace(
91+
"\\", "/"
92+
)
93+
if rel_path.startswith(".."): # Skip anything outside the tree
94+
continue
95+
# Skip packing kernels and interface headers, as benchmarks only cover concrete micro-kernels
96+
if "/pack/" in rel_path:
97+
continue
98+
if rel_path.endswith("_interface.h"):
99+
continue
100+
kernel_type = classify_kernel(rel_path)
101+
if kernel_type in kernel_types:
102+
present.add(rel_path)
103+
return present
104+
105+
106+
def parse_kernel_types(values: Sequence[str]) -> Set[str]:
107+
allowed = {"matmul", "imatmul", "dwconv"}
108+
requested = set(value.lower() for value in values)
109+
unknown = requested.difference(allowed)
110+
if unknown:
111+
raise SystemExit(
112+
f"Unknown kernel types requested: {', '.join(sorted(unknown))}"
113+
)
114+
return requested
115+
116+
117+
def main(argv: Optional[Sequence[str]] = None) -> int:
118+
parser = argparse.ArgumentParser(description=__doc__)
119+
parser.add_argument(
120+
"--benchmark-dir",
121+
default="benchmark",
122+
help="Directory to scan for benchmark micro-kernel registrations (default: %(default)s)",
123+
)
124+
parser.add_argument(
125+
"--ukernels-dir",
126+
default="kai/ukernels",
127+
help="Directory containing micro-kernel headers (default: %(default)s)",
128+
)
129+
parser.add_argument(
130+
"--kernel-types",
131+
nargs="+",
132+
default=["matmul", "imatmul", "dwconv"],
133+
help="Kernel families to verify (default: %(default)s)",
134+
)
135+
args = parser.parse_args(argv)
136+
137+
kernel_types = parse_kernel_types(args.kernel_types)
138+
139+
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
140+
141+
def resolve_dir(value: str, default: str) -> str:
142+
raw = os.path.expanduser(value)
143+
if value == default:
144+
raw = os.path.join(repo_root, default)
145+
return os.path.abspath(raw)
146+
147+
benchmark_dir = resolve_dir(args.benchmark_dir, parser.get_default("benchmark_dir"))
148+
ukernels_dir = resolve_dir(args.ukernels_dir, parser.get_default("ukernels_dir"))
149+
150+
if not os.path.isdir(benchmark_dir):
151+
raise SystemExit(f"Benchmark directory not found: {benchmark_dir}")
152+
if not os.path.isdir(ukernels_dir):
153+
raise SystemExit(f"Micro-kernel directory not found: {ukernels_dir}")
154+
155+
present = list_present(ukernels_dir, kernel_types)
156+
157+
include_prefix = os.path.relpath(ukernels_dir, repo_root).replace("\\", "/")
158+
if include_prefix.startswith(".."):
159+
raise SystemExit(
160+
f"Micro-kernel directory must reside within the repo: {ukernels_dir}"
161+
)
162+
163+
used = gather_includes(benchmark_dir, include_prefix, kernel_types)
164+
165+
unused = sorted(present - used)
166+
if unused:
167+
grouped = defaultdict(list)
168+
for rel in unused:
169+
grouped[classify_kernel(rel)].append(rel)
170+
print("Missing benchmark registrations for the following micro-kernel headers:")
171+
for kernel_type in sorted(grouped):
172+
print(f"{kernel_type}:")
173+
for rel in grouped[kernel_type]:
174+
print(f" - {rel}")
175+
return 1
176+
return 0
177+
178+
179+
if __name__ == "__main__":
180+
sys.exit(main())

0 commit comments

Comments
 (0)