adr: add ADR-0045 (bench module contract — registration, dispatch, authoring)
Documents src/kernbench/benches/: how @bench registration + audit work, how the CLI dispatches via run_bench/RuntimeContext, and the contract a new bench module must satisfy. Nine decisions (D1-D9) cover: - @bench name/description rules and duplicate detection - Module-file convention (_-prefixed helpers vs bench modules) - def run(torch) signature; torch = RuntimeContext - Minimum-one-submit rule (else NO_REQUESTS) - Single-device convention + multi-SIP CCL exception (ADR-0024/0027) - resolve() name/index decision tree; indices are not a stable API - Exact RuntimeContext surface exposed to benches - Env-var parameterization (matmul_composite / gemm_sweep.py pattern) Four alternatives rejected with documented reasons (manifest YAML, decorator entry= arg, @multi_device_bench split, stable indices). Verifier (tools/verify_adr_lang_pairs.py) passes for EN/KO pair. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
# ADR-0045: Bench Module Contract — registration, dispatch, and authoring
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-05-21).
|
||||
|
||||
`src/kernbench/benches/` 패키지의 등록 메커니즘(@bench), CLI 디스패치 경로
|
||||
(`kernbench run/list`), 그리고 새 bench 모듈 작성 시 따라야 할 계약을 통합
|
||||
정의한다. ADR-0010 (CLI surface)이 `kernbench list/run` 인터페이스를 명세하나,
|
||||
**bench가 어떻게 등록되고 어떤 함수 시그너처를 따라야 하는가**는 ADR 레벨에
|
||||
없었음.
|
||||
|
||||
## First action (제일 처음에 하는 일)
|
||||
|
||||
`kernbench.benches` 패키지가 임포트되면 `__init__.py` 가 즉시
|
||||
`_eager_import_and_audit(__path__, __name__)` 를 호출한다. 이 함수의 첫 일은
|
||||
패키지 디렉터리 안의 모든 형제 모듈을 `pkgutil.iter_modules(__path__)`로 나열한
|
||||
뒤, 다음 두 조건을 만족하지 않는 모듈을 모두 `importlib.import_module(...)`로
|
||||
**즉시 로드**하는 것이다:
|
||||
|
||||
- 이름이 `registry` 인 경우 (인프라 자체)
|
||||
- 이름이 `_` 로 시작하는 경우 (helper 모듈)
|
||||
|
||||
임포트 시점에 각 모듈 안의 `@bench(name=..., description=...)` 데코레이터가
|
||||
실행되어 `_PENDING` 리스트에 `(name, description, fn)` 튜플이 append 되고,
|
||||
`_REGISTERED_MODULES` 셋에 `fn.__module__` 가 추가된다.
|
||||
|
||||
전체 임포트가 끝나면 `_audit_modules(imported, _REGISTERED_MODULES)` 가 호출되어,
|
||||
**임포트는 되었지만 @bench를 한 번도 호출하지 않은 모듈**이 있으면
|
||||
`RuntimeError("Bench module(s) missing @bench decorator: ...")` 가 즉시 발생한다.
|
||||
이 audit이 통과한 시점에 인덱스 할당은 아직 일어나지 않은 상태이며, 첫
|
||||
`list_all()` / `resolve(...)` 호출 시 `_finalize()` 가 이름 알파벳 정렬 순으로
|
||||
1-based index를 부여한다.
|
||||
|
||||
즉, **bench 인프라의 첫 일은 "패키지 디렉터리의 모든 비-helper 모듈을 임포트
|
||||
하고, 각 모듈이 최소 한 번 @bench를 호출했는지 감사하는 것"** 이다.
|
||||
|
||||
## Context
|
||||
|
||||
`src/kernbench/benches/` 는 현재 8개의 bench 모듈을 보유한다 (`ccl_allreduce`,
|
||||
`gemm_single_pe`, `gpt3_qkv`, `ipcq_allreduce`, `matmul_composite`, `qkv_gemm`,
|
||||
`qkv_gemm_multi_pe`, `va_offset_verify`). 모든 bench는 다음 통합 흐름을 따른다:
|
||||
|
||||
```
|
||||
kernbench run --topology <T> --bench <N>
|
||||
↓
|
||||
cli/main.py::cmd_run
|
||||
↓ resolve_topology(T) + resolve(N) + resolve_device(device_arg)
|
||||
↓
|
||||
runtime_api/bench_runner.py::run_bench(topology, bench_fn, device, engine_factory)
|
||||
↓ engine_factory(topology, device) → GraphEngine
|
||||
↓ RuntimeContext(engine, target_device, correlation_id, spec)
|
||||
↓
|
||||
bench_fn(ctx) ← bench가 정의한 run(torch) 가 호출됨
|
||||
↓ ctx.empty/zeros/from_numpy/launch/distributed.* 등을 통해 submit
|
||||
↓
|
||||
ctx.wait_all() ← 미완료 핸들이 있으면 drain
|
||||
↓
|
||||
BenchResult(completion, correlation_id, trace, traces, engine)
|
||||
```
|
||||
|
||||
ADR-0010 은 CLI 표면만 다루고 (`run/list/probe/web`), ADR-0007 은 runtime API ↔
|
||||
sim_engine 책임 경계만 다룬다. 정작 "새 bench 파일을 추가하려면 어떤 모양으로
|
||||
써야 하는가"는 코드 컨벤션만으로 추적해야 한다. 결과적으로:
|
||||
|
||||
- @bench 데코레이터의 호출 규약 (kebab-case 이름, non-empty description)이
|
||||
코드에만 존재.
|
||||
- bench 함수 시그너처 (`def run(torch)`) 가 사실상 컨벤션인데, CLI 디스패치 측이
|
||||
`spec.run` 을 호출한다는 사실로 강제되고 있음.
|
||||
- 신규 bench 추가자가 "helper 모듈은 `_` 접두로 분리해야 한다"는 것을 audit
|
||||
RuntimeError를 받아본 뒤에야 학습.
|
||||
- single-device 컨벤션 (CLAUDE.md Part 2 CLI Semantics)이 bench 작성자 관점에서
|
||||
어디까지 적용되는지 (CCL 멀티-SIP bench는 예외인가?) 명확하지 않음.
|
||||
|
||||
이 ADR이 이런 모호함을 한 곳에 정리한다.
|
||||
|
||||
## Decision
|
||||
|
||||
### D1. @bench 데코레이터 계약
|
||||
|
||||
```python
|
||||
from kernbench.benches.registry import bench
|
||||
|
||||
@bench(name="my-bench", description="Short, complete-sentence description.")
|
||||
def run(torch):
|
||||
...
|
||||
```
|
||||
|
||||
- `name`: kebab-case 문자열. 정규식 `^[a-z][a-z0-9]*(-[a-z0-9]+)*$` 통과 필요.
|
||||
소문자/숫자/대시만 허용; 밑줄(`_`) 금지; 알파벳으로 시작.
|
||||
- `description`: non-empty 문자열 (strip 후 길이 > 0). CLI `list` 출력에 그대로
|
||||
표시됨.
|
||||
- 데코레이터는 **fn을 변형 없이 반환**한다 — 즉 직접 호출도 가능. 부수효과로
|
||||
`_PENDING` 에 등록만 추가한다.
|
||||
|
||||
위 두 규칙 위반은 즉시 `ValueError`. duplicate name은 `_finalize()` 시점에
|
||||
`RuntimeError("duplicate bench name: ...")` 로 잡힌다.
|
||||
|
||||
### D2. 모듈 파일 컨벤션
|
||||
|
||||
`src/kernbench/benches/<slug>.py` 는 다음 중 하나여야 한다:
|
||||
|
||||
- **bench 모듈**: 최상위 임포트 경로에서 적어도 한 번 `@bench(...)` 가 실행되어
|
||||
최소 하나의 bench를 등록한다.
|
||||
- **helper 모듈**: 파일명이 `_` 로 시작 (예: `_shared_helpers.py`). `iter_modules`
|
||||
순회에서 스킵된다.
|
||||
|
||||
audit (`_audit_modules`) 는 helper가 아닌데도 @bench를 호출하지 않은 모듈을
|
||||
허용하지 않는다. 의도된 결과: 새 파일을 `benches/` 에 추가하기만 하면 자동
|
||||
등록되며, helper와의 구분은 **파일명 접두사** 하나로 명확하게 표시된다.
|
||||
|
||||
### D3. bench 함수 시그너처는 `def run(torch)` 다
|
||||
|
||||
데코레이터는 함수 이름을 강제하지 않지만, **CLI 디스패치는 `spec_entry.run`
|
||||
(즉 데코레이트된 callable) 을 호출**한다. 따라서 컨벤션은:
|
||||
|
||||
- 함수 이름: `run`. 다른 이름으로 데코레이트해도 동작은 하지만 readability /
|
||||
grep-ability 측면에서 항상 `run`.
|
||||
- 인자: 단일 위치 인자 `torch`. 실제로는 `RuntimeContext` 인스턴스이며 PyTorch
|
||||
스타일의 namespace (zeros/empty/launch/distributed/...)를 노출한다 (ADR-0024 D3).
|
||||
- 반환값: 임의 (`Any`). 현재 `run_bench` 는 반환값을 무시하고 `ctx.handles()` /
|
||||
`engine.get_completion()` 로 완료를 추적한다.
|
||||
|
||||
`torch` 이름은 PyTorch 호환 idiom을 흉내내기 위함이며, 실제로 PyTorch 모듈이
|
||||
들어오는 것은 아니다 (ADR-0024 의 "rank = SIP" launcher 컨벤션과 정렬).
|
||||
|
||||
### D4. bench는 최소 한 번의 submit을 수행해야 한다
|
||||
|
||||
`run_bench` 는 `ctx.handles()` 가 비어 있는 경우 BenchResult.completion 을
|
||||
`ok=False, error_code="NO_REQUESTS"` 로 반환한다. 따라서 의미 있는 bench는
|
||||
다음 중 하나 이상을 호출해야 한다:
|
||||
|
||||
- 텐서 생성 API: `torch.zeros(...)`, `torch.empty(...)` — 내부적으로
|
||||
`MmuMapMsg` 와 (zeros 의 경우) `MemoryWriteMsg` 가 submit 됨.
|
||||
- 커널 실행 API: `torch.launch(name, fn, *args)` — `KernelLaunchMsg` 를 SIP 별로
|
||||
submit.
|
||||
- (예외) 빈 placeholder bench: `ipcq_allreduce.py` 처럼 `print(...)` 만 하는
|
||||
스텁은 NO_REQUESTS 결과를 받게 됨. CI 측에서 placeholder임을 인지하고 별도
|
||||
처리하는 것을 가정한다.
|
||||
|
||||
### D5. 단일-디바이스 컨벤션 + 멀티-SIP 예외 (ADR-0024/0027)
|
||||
|
||||
CLAUDE.md Part 2 CLI Semantics 가 명시하는 **"benchmarks MUST remain
|
||||
single-device"** 컨벤션은 다음과 같이 해석된다:
|
||||
|
||||
- **일반 bench (single-SIP 사용)**: `dp = DPPolicy(...)` 로 텐서 placement를
|
||||
정의하고 `torch.launch(...)` 로 커널 발사. SIP 인덱스는 `--device` 가
|
||||
결정한다 (CLI 측 책임).
|
||||
- **CCL bench (멀티-SIP 사용)**: 예외적으로 `torch.distributed.init_process_group
|
||||
(backend="ahbm")` + `torch.multiprocessing.spawn(_worker, ..., nprocs=ws)` 로
|
||||
rank = SIP 패턴 (ADR-0024 D3) 을 따른다. `--device` 는 무시되며 (또는
|
||||
`all` 로 가정), 각 spawned worker가 `torch.ahbm.set_device(rank)` 로 자신의
|
||||
SIP를 바인딩한다.
|
||||
|
||||
이 두 패턴 외의 멀티-디바이스 호출 (예: 한 bench 함수가 동일 process에서 여러
|
||||
SIP을 직접 launch) 은 본 ADR이 금지한다. CLI 가 `--device all` 로 호출되어도
|
||||
bench는 한 번만 실행되며, 그 안에서 멀티-SIP을 다루려면 D5의 두 번째 패턴을
|
||||
사용한다.
|
||||
|
||||
### D6. 이름·인덱스 해석 (`resolve`)
|
||||
|
||||
`resolve(identifier: str)` 는 다음 순서로 BenchSpec을 반환한다:
|
||||
|
||||
1. `identifier.isdigit()` → 정수 변환 후 `_REGISTRY` 의 entries에서 `index ==`
|
||||
인 spec 반환. 없으면 `ValueError("No bench with index ..."`)`.
|
||||
2. `identifier in _REGISTRY` → 직접 lookup.
|
||||
3. 그 외 → `ValueError("Unknown bench ...")`.
|
||||
|
||||
빈/공백 identifier 는 `ValueError("bench identifier must be a non-empty string.")`.
|
||||
|
||||
CLI 는 `--bench` 의 인자를 그대로 `resolve` 에 넘긴다. 따라서 사용자는
|
||||
`kernbench run --bench gemm-single-pe` 또는 `kernbench run --bench 2` 형식 모두
|
||||
사용 가능.
|
||||
|
||||
### D7. 인덱스는 안정 API가 아니다
|
||||
|
||||
`_finalize()` 가 `_PENDING` 을 **이름 알파벳 정렬** 후 1-based index를 부여하므로,
|
||||
새 bench 가 추가되면 기존 bench의 index가 밀릴 수 있다. 따라서:
|
||||
|
||||
- 사람-친화적 인터랙티브 사용: 인덱스 OK.
|
||||
- 스크립트 / CI 자동화: 반드시 이름을 사용한다.
|
||||
|
||||
이 사실은 `registry.py` 모듈 docstring 에 명시되어 있다.
|
||||
|
||||
### D8. RuntimeContext 가 bench에 노출하는 표면
|
||||
|
||||
bench 함수가 `torch` 파라미터를 통해 정상적으로 사용할 수 있는 표면:
|
||||
|
||||
- **텐서 생성**: `torch.empty(shape, dtype=..., dp=DPPolicy(...), name=...)`,
|
||||
`torch.zeros(...)`, `torch.from_numpy(arr)`. 모두 host-side 메타 + 디바이스
|
||||
배포 (MmuMap + MemoryWrite) 를 submit 한다.
|
||||
- **커널 발사**: `torch.launch(kernel_name, kernel_fn, *args)` —
|
||||
`(Tensor, int, float)` 위치 인자를 `TensorArg` / `ScalarArg` 로 변환하여
|
||||
SIP 별 `KernelLaunchMsg` 발행 후 drain.
|
||||
- **동기화**: `torch.wait(handle)`, `torch.wait_all()` (run_bench 가 자동 호출).
|
||||
- **분산**: `torch.distributed.init_process_group(backend="ahbm")`,
|
||||
`torch.distributed.get_world_size()`, `torch.distributed.all_reduce(t, op=...)`
|
||||
(ADR-0024/0027).
|
||||
- **멀티-프로세스 (rank=SIP)**: `torch.multiprocessing.spawn(_worker, ..., nprocs=ws)`
|
||||
(ADR-0024 D3 / ADR-0027).
|
||||
- **디바이스 바인딩**: `torch.ahbm.set_device(rank)` 또는
|
||||
`torch.accelerator.set_device_index(rank)` (둘 다 같은 namespace를 가리킴).
|
||||
- **IPCQ 설치**: `torch.install_ipcq(algorithm=..., ccl_yaml=...)` (ADR-0023 D10).
|
||||
- **스펙 조회**: `torch.spec` — 토폴로지 빌더가 만든 dict (시스템·cube_mesh·HBM
|
||||
파라미터 등). bench가 toplogy.yaml 파라미터에 의존하지 않게 짜기 위함.
|
||||
|
||||
bench는 위에 열거되지 않은 RuntimeContext 의 private 멤버 (`_handles`, `_traces`,
|
||||
`_allocators` 등) 에 직접 접근해선 안 된다. ADR-0007 의 layer boundary 정신과
|
||||
정렬: bench → runtime API → sim_engine 한 방향만 허용.
|
||||
|
||||
### D9. 환경 변수로 파라미터화는 허용된다
|
||||
|
||||
`matmul_composite.py` 처럼 `os.environ.get("MATMUL_M", ...)` 등으로 bench
|
||||
파라미터를 외부에서 주입하는 패턴은 허용한다. 이유:
|
||||
|
||||
- bench 함수 시그너처는 D3 에 의해 `def run(torch)` 로 고정되어 있어 위치/키워드
|
||||
인자로 파라미터를 받기 곤란.
|
||||
- 환경 변수 패턴은 `MATMUL_VARIANT` 같은 운영-시 스윕을 위한 자연스러운 hook.
|
||||
- `scripts/gemm_sweep.py` 같은 외부 드라이버 (ADR-0044) 가 이 hook을 사용한다.
|
||||
|
||||
단, 환경 변수가 bench의 동작을 바꾼다면 모듈 docstring 에 모든 변수를 명시할 것
|
||||
(matmul_composite.py 가 그 예시).
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### A1. 명시적 manifest 파일 (YAML)에 bench 목록 두기
|
||||
|
||||
기각. @bench 데코레이터 + audit 패턴은 "파일 추가 = 자동 등록" 을 보장하여 신규
|
||||
bench 작성자의 인지 비용을 한 곳 (파일 작성)으로 집중시킨다. 별도 manifest는
|
||||
유지보수 측에서 drift 위험이 크고, helper 분리는 이미 `_` 접두로 명확하다.
|
||||
|
||||
### A2. bench 함수 이름을 데코레이터 인자로 받기 (`@bench(name=..., entry="run_xxx")`)
|
||||
|
||||
기각. 디스패치 측에서 `spec.run` 하나만 호출하면 되는 단순함을 깬다. `run` 컨벤션
|
||||
하나로 충분하며, 변종이 필요하면 같은 모듈에 여러 함수를 등록하면 된다 (각각
|
||||
@bench 데코레이트).
|
||||
|
||||
### A3. CCL bench를 위한 별도 `@multi_device_bench` 데코레이터
|
||||
|
||||
기각. D5에서 명시한 두 패턴 (single + ADR-0024 멀티-SIP) 만으로 현재 8개 bench가
|
||||
모두 표현 가능. 별도 데코레이터는 디스패치 측에서 분기를 강제하여 복잡도를 늘리며,
|
||||
멀티-SIP 사용 의도는 bench 함수 본문의 `init_process_group(...)` 호출로 충분히
|
||||
드러난다.
|
||||
|
||||
### A4. 인덱스를 안정 API로 만들기 (등록 순서 / explicit index= 인자)
|
||||
|
||||
기각. D7에서 명시한 trade-off — 사용자 친화성 (알파벳 정렬된 인덱스가 list 출력
|
||||
에서 자연스럽게 1, 2, 3...) 우선. 스크립트는 이름으로 지정하면 충분.
|
||||
|
||||
## Consequences
|
||||
|
||||
- "bench 추가 방법" 이 한 ADR로 정리됨 → 신규 작성자가 코드 grep 없이 D1-D3,
|
||||
D8 만 따르면 됨.
|
||||
- helper 모듈을 `_` 접두로 분리하는 패턴이 ADR-level에서 정당화되어, 향후
|
||||
`benches/_*.py` 식의 공유 helper 작성이 자유로워짐.
|
||||
- CLAUDE.md Part 2 CLI Semantics 의 single-device 컨벤션이 멀티-SIP CCL bench
|
||||
와 모순되지 않음을 D5 가 명시 — 둘은 직교한다.
|
||||
- ADR-0044 (GEMM eval harness) 의 `scripts/gemm_sweep.py` 가 환경 변수 hook을
|
||||
사용하는 근거 (D9) 가 본 ADR에 굳어짐.
|
||||
- 인덱스가 불안정함 (D7) 이 명시되어, CI 측 `kernbench run --bench 3` 같은
|
||||
코드는 본 ADR 수락 직후 점검 대상.
|
||||
@@ -0,0 +1,291 @@
|
||||
# ADR-0045: Bench Module Contract — registration, dispatch, and authoring
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-05-21).
|
||||
|
||||
Unifies the `src/kernbench/benches/` registration mechanism (@bench), the
|
||||
CLI dispatch path (`kernbench run/list`), and the contract a new bench
|
||||
module must follow. ADR-0010 (CLI surface) specifies the `kernbench
|
||||
list/run` interface, but **how benches are registered and what signature
|
||||
they must follow** had no ADR-level coverage.
|
||||
|
||||
## First action
|
||||
|
||||
When `kernbench.benches` is imported, `__init__.py` immediately calls
|
||||
`_eager_import_and_audit(__path__, __name__)`. Its first action is to
|
||||
enumerate every sibling module in the package directory via
|
||||
`pkgutil.iter_modules(__path__)` and **eagerly import** each one via
|
||||
`importlib.import_module(...)` — except modules matching either:
|
||||
|
||||
- name `registry` (the infrastructure module itself), or
|
||||
- name starting with `_` (helper modules).
|
||||
|
||||
At import time, each `@bench(name=..., description=...)` decorator inside
|
||||
the imported module runs, appending `(name, description, fn)` to
|
||||
`_PENDING` and adding `fn.__module__` to `_REGISTERED_MODULES`.
|
||||
|
||||
Once imports finish, `_audit_modules(imported, _REGISTERED_MODULES)`
|
||||
runs; if any imported module did not invoke `@bench` at least once, it
|
||||
raises `RuntimeError("Bench module(s) missing @bench decorator: ...")`
|
||||
immediately. At this point indices are still unassigned — the first call
|
||||
to `list_all()` / `resolve(...)` triggers `_finalize()`, which sorts
|
||||
`_PENDING` alphabetically by name and assigns 1-based indices.
|
||||
|
||||
In short, **the bench infrastructure's first act is "eagerly import
|
||||
every non-helper module in the package and audit that each one
|
||||
registered at least one bench"**.
|
||||
|
||||
## Context
|
||||
|
||||
`src/kernbench/benches/` currently holds 8 bench modules (`ccl_allreduce`,
|
||||
`gemm_single_pe`, `gpt3_qkv`, `ipcq_allreduce`, `matmul_composite`,
|
||||
`qkv_gemm`, `qkv_gemm_multi_pe`, `va_offset_verify`). Every bench follows
|
||||
the same unified flow:
|
||||
|
||||
```
|
||||
kernbench run --topology <T> --bench <N>
|
||||
↓
|
||||
cli/main.py::cmd_run
|
||||
↓ resolve_topology(T) + resolve(N) + resolve_device(device_arg)
|
||||
↓
|
||||
runtime_api/bench_runner.py::run_bench(topology, bench_fn, device, engine_factory)
|
||||
↓ engine_factory(topology, device) → GraphEngine
|
||||
↓ RuntimeContext(engine, target_device, correlation_id, spec)
|
||||
↓
|
||||
bench_fn(ctx) ← invokes the bench's run(torch)
|
||||
↓ ctx.empty/zeros/from_numpy/launch/distributed.* etc. submit work
|
||||
↓
|
||||
ctx.wait_all() ← drains any outstanding handles
|
||||
↓
|
||||
BenchResult(completion, correlation_id, trace, traces, engine)
|
||||
```
|
||||
|
||||
ADR-0010 covers only the CLI surface (`run/list/probe/web`); ADR-0007
|
||||
covers only the runtime API ↔ sim_engine boundary. The question "what
|
||||
shape must a new bench file take?" had to be answered by grepping the
|
||||
codebase. As a result:
|
||||
|
||||
- The @bench decorator contract (kebab-case name, non-empty description)
|
||||
lived only in the source.
|
||||
- The bench function signature (`def run(torch)`) was a de-facto
|
||||
convention enforced by the CLI dispatcher calling `spec.run`.
|
||||
- New bench authors learned the "helpers must use `_` prefix" rule only
|
||||
after seeing the audit's RuntimeError.
|
||||
- The single-device convention (CLAUDE.md Part 2 CLI Semantics) and its
|
||||
interaction with multi-SIP CCL benches was ambiguous for bench
|
||||
authors.
|
||||
|
||||
This ADR consolidates all of it in one place.
|
||||
|
||||
## Decision
|
||||
|
||||
### D1. @bench decorator contract
|
||||
|
||||
```python
|
||||
from kernbench.benches.registry import bench
|
||||
|
||||
@bench(name="my-bench", description="Short, complete-sentence description.")
|
||||
def run(torch):
|
||||
...
|
||||
```
|
||||
|
||||
- `name`: kebab-case string matching `^[a-z][a-z0-9]*(-[a-z0-9]+)*$`.
|
||||
Lowercase letters, digits, and dashes only; underscores forbidden;
|
||||
must start with a letter.
|
||||
- `description`: non-empty string (stripped length > 0). Displayed
|
||||
verbatim by `kernbench list`.
|
||||
- The decorator **returns the function unchanged** — direct invocation
|
||||
is fine. Its only side effect is appending to `_PENDING`.
|
||||
|
||||
Violations of the first two rules raise `ValueError` at decoration time.
|
||||
Duplicate names are caught at `_finalize()` with
|
||||
`RuntimeError("duplicate bench name: ...")`.
|
||||
|
||||
### D2. Module-file convention
|
||||
|
||||
Every `src/kernbench/benches/<slug>.py` must be one of:
|
||||
|
||||
- **A bench module**: at top-level import, `@bench(...)` runs at least
|
||||
once to register at least one bench.
|
||||
- **A helper module**: the filename starts with `_` (e.g.,
|
||||
`_shared_helpers.py`). `iter_modules` skips it.
|
||||
|
||||
The audit (`_audit_modules`) rejects any non-helper that fails to call
|
||||
`@bench`. Intended consequence: dropping a new file into `benches/`
|
||||
automatically registers its benches, and helper modules are clearly
|
||||
flagged by their filename prefix alone.
|
||||
|
||||
### D3. The bench function signature is `def run(torch)`
|
||||
|
||||
The decorator does not enforce a function name, but **CLI dispatch calls
|
||||
`spec_entry.run`** (the decorated callable). The convention is therefore:
|
||||
|
||||
- Function name: `run`. Other names work, but always use `run` for
|
||||
readability and grep-ability.
|
||||
- Argument: a single positional `torch`. In practice this is a
|
||||
`RuntimeContext` instance exposing PyTorch-style namespaces
|
||||
(zeros/empty/launch/distributed/...) — see ADR-0024 D3.
|
||||
- Return value: any (`Any`). `run_bench` ignores it and tracks
|
||||
completion via `ctx.handles()` / `engine.get_completion()`.
|
||||
|
||||
The `torch` name imitates a PyTorch-compatible idiom; the actual PyTorch
|
||||
module is not passed in (aligned with ADR-0024's "rank = SIP" launcher
|
||||
convention).
|
||||
|
||||
### D4. A bench must submit at least once
|
||||
|
||||
If `ctx.handles()` is empty after the bench returns, `run_bench` reports
|
||||
`BenchResult.completion = ok=False, error_code="NO_REQUESTS"`. So a
|
||||
meaningful bench must invoke at least one of:
|
||||
|
||||
- Tensor-creation APIs: `torch.zeros(...)`, `torch.empty(...)` — these
|
||||
internally submit `MmuMapMsg` and (for zeros) `MemoryWriteMsg`.
|
||||
- Kernel-launch API: `torch.launch(name, fn, *args)` — submits per-SIP
|
||||
`KernelLaunchMsg`.
|
||||
- (Exception) Empty placeholder benches: e.g.,
|
||||
`ipcq_allreduce.py`'s `print(...)`-only stub will receive a
|
||||
NO_REQUESTS result. CI is expected to recognize and handle placeholder
|
||||
benches specially.
|
||||
|
||||
### D5. Single-device convention + multi-SIP exception (ADR-0024/0027)
|
||||
|
||||
CLAUDE.md Part 2 CLI Semantics' **"benchmarks MUST remain
|
||||
single-device"** rule is interpreted as follows:
|
||||
|
||||
- **Standard bench (single-SIP use)**: define tensor placement with
|
||||
`dp = DPPolicy(...)` and launch with `torch.launch(...)`. The SIP
|
||||
index is chosen by `--device` (CLI's responsibility).
|
||||
- **CCL bench (multi-SIP use)**: as an exception, use
|
||||
`torch.distributed.init_process_group(backend="ahbm")` plus
|
||||
`torch.multiprocessing.spawn(_worker, ..., nprocs=ws)` for the
|
||||
rank = SIP pattern (ADR-0024 D3). `--device` is ignored (or treated
|
||||
as `all`); each spawned worker calls `torch.ahbm.set_device(rank)` to
|
||||
bind to its SIP.
|
||||
|
||||
Multi-device patterns outside these two (e.g., one bench function
|
||||
launching across multiple SIPs in the same process) are forbidden by
|
||||
this ADR. Even with `--device all`, the bench runs once; multi-SIP use
|
||||
inside that single run must follow D5's second pattern.
|
||||
|
||||
### D6. Name/index resolution (`resolve`)
|
||||
|
||||
`resolve(identifier: str)` returns a BenchSpec via:
|
||||
|
||||
1. If `identifier.isdigit()`: convert to int and find the spec where
|
||||
`index ==` that value. If none, `ValueError("No bench with index
|
||||
...")`.
|
||||
2. If `identifier in _REGISTRY`: direct lookup.
|
||||
3. Otherwise: `ValueError("Unknown bench ...")`.
|
||||
|
||||
Empty or whitespace-only identifiers raise `ValueError("bench
|
||||
identifier must be a non-empty string.")`.
|
||||
|
||||
The CLI passes `--bench` directly to `resolve`, so users can use either
|
||||
`kernbench run --bench gemm-single-pe` or `kernbench run --bench 2`.
|
||||
|
||||
### D7. Indices are not a stable API
|
||||
|
||||
`_finalize()` sorts `_PENDING` alphabetically by name and assigns
|
||||
1-based indices. Adding a new bench can shift existing benches'
|
||||
indices. Therefore:
|
||||
|
||||
- Human-interactive use: indices are fine.
|
||||
- Scripts / CI automation: always use the name.
|
||||
|
||||
This caveat is documented in `registry.py`'s module docstring.
|
||||
|
||||
### D8. Surface RuntimeContext exposes to benches
|
||||
|
||||
A bench's `torch` parameter may legitimately use:
|
||||
|
||||
- **Tensor creation**: `torch.empty(shape, dtype=..., dp=DPPolicy(...),
|
||||
name=...)`, `torch.zeros(...)`, `torch.from_numpy(arr)`. All submit
|
||||
host-side metadata plus device deployment (`MmuMapMsg` +
|
||||
`MemoryWriteMsg`).
|
||||
- **Kernel launch**: `torch.launch(kernel_name, kernel_fn, *args)` —
|
||||
converts `(Tensor, int, float)` positional args to `TensorArg` /
|
||||
`ScalarArg`, submits per-SIP `KernelLaunchMsg`, and drains.
|
||||
- **Synchronization**: `torch.wait(handle)`, `torch.wait_all()`
|
||||
(`run_bench` calls the latter automatically).
|
||||
- **Distributed**: `torch.distributed.init_process_group(backend="ahbm")`,
|
||||
`torch.distributed.get_world_size()`,
|
||||
`torch.distributed.all_reduce(t, op=...)` (ADR-0024/0027).
|
||||
- **Multi-process (rank = SIP)**:
|
||||
`torch.multiprocessing.spawn(_worker, ..., nprocs=ws)` (ADR-0024 D3 /
|
||||
ADR-0027).
|
||||
- **Device binding**: `torch.ahbm.set_device(rank)` or
|
||||
`torch.accelerator.set_device_index(rank)` (both point to the same
|
||||
namespace).
|
||||
- **IPCQ install**: `torch.install_ipcq(algorithm=..., ccl_yaml=...)`
|
||||
(ADR-0023 D10).
|
||||
- **Spec lookup**: `torch.spec` — the dict produced by the topology
|
||||
builder (system / cube_mesh / HBM parameters etc.). Use it so the
|
||||
bench does not hardcode topology.yaml values.
|
||||
|
||||
Benches must not access RuntimeContext private members (`_handles`,
|
||||
`_traces`, `_allocators`, etc.) directly. This aligns with ADR-0007's
|
||||
layer-boundary spirit: bench → runtime API → sim_engine flows in one
|
||||
direction.
|
||||
|
||||
### D9. Environment-variable parameterization is allowed
|
||||
|
||||
Benches may parameterize themselves via `os.environ.get(...)`, as
|
||||
`matmul_composite.py` does for `MATMUL_M`, `MATMUL_K`, `MATMUL_N`,
|
||||
`MATMUL_DTYPE`, `MATMUL_VARIANT`. Rationale:
|
||||
|
||||
- The bench function signature is fixed by D3 to `def run(torch)`, so
|
||||
positional/keyword arguments cannot carry parameters.
|
||||
- The env-var pattern is a natural hook for operational sweeps (e.g.,
|
||||
`MATMUL_VARIANT`).
|
||||
- External drivers such as `scripts/gemm_sweep.py` (ADR-0044) consume
|
||||
this hook (it sets `MATMUL_M/K/N/VARIANT` at
|
||||
`scripts/gemm_sweep.py:115-118`).
|
||||
|
||||
When environment variables alter bench behavior, the module docstring
|
||||
must list every variable used (`matmul_composite.py` is the canonical
|
||||
example).
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### A1. An explicit manifest file (YAML) listing benches
|
||||
|
||||
Rejected. The `@bench` + audit pattern guarantees "drop in file → auto-
|
||||
register", concentrating cognitive cost in one place (the file itself).
|
||||
A separate manifest is prone to drift, and helper separation is already
|
||||
clear via the `_` prefix.
|
||||
|
||||
### A2. Allowing the bench's entry-point name in the decorator
|
||||
(`@bench(name=..., entry="run_xxx")`)
|
||||
|
||||
Rejected. Breaks the simplicity of dispatch (`spec.run` is a single
|
||||
callable). The `run` convention is sufficient; variants can register
|
||||
multiple `@bench`-decorated functions in the same module.
|
||||
|
||||
### A3. A separate `@multi_device_bench` decorator for CCL
|
||||
|
||||
Rejected. The two patterns named in D5 (single + ADR-0024 multi-SIP)
|
||||
cover all 8 current benches. A separate decorator would force dispatch
|
||||
to branch and add complexity; the multi-SIP intent is already obvious
|
||||
from the bench's `init_process_group(...)` call.
|
||||
|
||||
### A4. Make indices a stable API (registration order or explicit
|
||||
`index=` argument)
|
||||
|
||||
Rejected. D7's trade-off favors user-friendliness — alphabetically
|
||||
sorted 1-based indices read naturally in the `list` output. Scripts can
|
||||
use names.
|
||||
|
||||
## Consequences
|
||||
|
||||
- "How to add a bench" is consolidated in one ADR — new authors only
|
||||
need to read D1-D3 and D8 without grepping source.
|
||||
- The `_`-prefixed helper-module pattern is legitimized at ADR level,
|
||||
so future `benches/_*.py` shared helpers are free to be added.
|
||||
- The CLI's single-device convention and CCL's multi-SIP exception are
|
||||
shown to be consistent (D5) — they are orthogonal.
|
||||
- The rationale for ADR-0044's GEMM eval harness using env-var hooks
|
||||
(D9) is now ADR-pinned.
|
||||
- Indices are explicitly unstable (D7), so any CI code calling
|
||||
`kernbench run --bench 3` is flagged for review after this ADR is
|
||||
accepted.
|
||||
Reference in New Issue
Block a user