benches: package as kernbench.benches, add @bench registry + list subcommand

Move benches/ -> src/kernbench/benches/ and src/kernbench/cli/probe.py ->
src/kernbench/probes/probe.py. Each bench self-registers via
@bench(name=..., description=...); kernbench list enumerates benches
with auto-assigned indices, --bench accepts kebab-case name or numeric
index. Audit at package-import time fails if any non-underscore module
forgets the decorator. ADR-0010 (EN + KO) updated to reflect the new
resolver path, list subcommand, and probes package separation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 14:42:10 -07:00
parent 168b0c89f0
commit 049e3d8bb3
28 changed files with 398 additions and 79 deletions
+106
View File
@@ -0,0 +1,106 @@
"""Bench registry: @bench decorator + name/index resolution.
Each bench module under ``kernbench.benches`` MUST register its callable
via ``@bench(name=..., description=...)``. Indices are assigned
alphabetically by name after eager import; they are a CLI convenience,
not a stable API.
"""
from __future__ import annotations
import re
from collections.abc import Callable
from dataclasses import dataclass
from importlib import import_module
from pkgutil import iter_modules
BenchFn = Callable[..., object]
_NAME_RE = re.compile(r"^[a-z][a-z0-9]*(-[a-z0-9]+)*$")
@dataclass(frozen=True)
class BenchSpec:
index: int
name: str
description: str
run: BenchFn
_PENDING: list[tuple[str, str, BenchFn]] = []
_REGISTERED_MODULES: set[str] = set()
_REGISTRY: dict[str, BenchSpec] = {}
def bench(*, name: str, description: str) -> Callable[[BenchFn], BenchFn]:
if not isinstance(name, str) or not _NAME_RE.match(name):
raise ValueError(
f"bench name {name!r} must be kebab-case (lowercase, digits, dashes; "
f"starts with a letter)."
)
if not isinstance(description, str) or not description.strip():
raise ValueError(f"bench {name!r}: description must be a non-empty string.")
def deco(fn: BenchFn) -> BenchFn:
_PENDING.append((name, description, fn))
_REGISTERED_MODULES.add(fn.__module__)
return fn
return deco
def _finalize() -> None:
if _REGISTRY:
return
seen: set[str] = set()
for n, _, _ in _PENDING:
if n in seen:
raise RuntimeError(f"duplicate bench name: {n!r}")
seen.add(n)
for i, (n, d, f) in enumerate(sorted(_PENDING, key=lambda t: t[0]), start=1):
_REGISTRY[n] = BenchSpec(index=i, name=n, description=d, run=f)
def list_all() -> list[BenchSpec]:
_finalize()
return sorted(_REGISTRY.values(), key=lambda s: s.index)
def resolve(identifier: str) -> BenchSpec:
_finalize()
if not isinstance(identifier, str) or not identifier.strip():
raise ValueError("bench identifier must be a non-empty string.")
ident = identifier.strip()
if ident.isdigit():
idx = int(ident)
for s in _REGISTRY.values():
if s.index == idx:
return s
raise ValueError(
f"No bench with index {idx}. Use 'kernbench list' to see options."
)
if ident in _REGISTRY:
return _REGISTRY[ident]
raise ValueError(
f"Unknown bench {ident!r}. Use 'kernbench list' to see options."
)
def _audit_modules(imported: list[str], registered: set[str]) -> None:
missing = sorted(m for m in imported if m not in registered)
if missing:
raise RuntimeError(
f"Bench module(s) missing @bench decorator: {missing}. "
f"Each file under kernbench.benches/ must register at least one bench "
f"via @bench(...), or be renamed with a leading underscore if it is a "
f"helper."
)
def _eager_import_and_audit(pkg_path: list[str], pkg_name: str) -> None:
imported: list[str] = []
for m in iter_modules(pkg_path):
if m.name == "registry" or m.name.startswith("_"):
continue
mod = import_module(f"{pkg_name}.{m.name}")
imported.append(mod.__name__)
_audit_modules(imported, _REGISTERED_MODULES)