|
| 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