Skip to main content

aiter-op-test

Standard structure for aiter op_tests under op_tests/test_*.py — @benchmark + run_perftest candidate loop, a torch reference, a final markdown summary table, a __main__ guard so the module is importable, and faithful reproduction of the real model call (output buffer, layout, shapes). Use whenever writing, rewriting, or extending any aiter unit/perf test, or adding model-derived shapes (e.g. DeepSeek-V4) to an existing one.

インストールへ移動

ソース情報

リポジトリ
ROCm/aiter
ソースの最終更新活動
2026年6月29日 10:34
検出された SKILL.md の言語
英語
スター
562
フォーク
572

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
aiter-op-test
description
Standard structure for aiter op_tests under op_tests/test_*.py — @benchmark + run_perftest candidate loop, a torch reference, a final markdown summary table, a __main__ guard so the module is importable, and faithful reproduction of the real model call (output buffer, layout, shapes). Use whenever writing, rewriting, or extending any aiter unit/perf test, or adding model-derived shapes (e.g. DeepSeek-V4) to an existing one.
argument-hint
["op name or op_tests/test_*.py file"]
# aiter op_test standard How every aiter op test in `op_tests/test_*.py` must be built. The canonical reference in-tree is **`op_tests/test_quant.py`** — match its shape. A test is both a **correctness check** (vs a torch reference) and a **perf sweep** that ends in a **markdown summary table**. Follow this whenever you create a new `test_*.py`, rewrite an old one, or add shapes/candidates to an existing one. ## The hard rules 1. **Mirror `test_quant.py`.** Same imports, same decorator, same table-at-the-end flow. Don't invent a different structure. 2. **`@benchmark()` on the test fn.** It logs the function's call args (the shape params) as table columns automatically and merges the dict you `return`. So the test fn signature *is* the table's left-hand columns — name params accordingly. 3. **Candidates live in a dict; build `ret` in a loop.** Per candidate record raw `us`, plus **`TFLOPS` and `TB/s`**, plus `err` — `ret[f"{name} us"]`, `ret[f"{name} TFLOPS"]`, `ret[f"{name} TB/s"]`, `ret[f"{name} err"]`. Never hand-write ratio columns. (TFLOPS/TB-s section below.) 4. **torch is the reference only** — compute it, compare against it, but do **not** time it and do **not** put it in the table. (A pure-torch candidate is allowed only when torch *is* one of the kernels under test, e.g. `torch.einsum`.) 5. **Time with `run_perftest`, check with `checkAllclose`** — both, for every candidate. Compare in fp32 (`.to(dtypes.fp32)`). 6. **End with a markdown summary table — one per test function.** Sweep the shape lists with `itertools.product`, collect per-shape dicts into a `pd.DataFrame`, print via `aiter.logger.info("... :\n%s", df.to_markdown(index=False))`. A file with several test fns of different arg signatures emits **one table each** — never force-merge them (it scatters NaN columns). Mandatory — a test with no summary table is incomplete. 7. **`__main__` guard.** All argparse + the sweep loop go inside `main()`, called under `if __name__ == "__main__": main()`. The reference (`run_torch`) and the `@benchmark` test fn stay at module top level so other scripts can `import` them for combination testing. 8. **Standard argparse only.** Use `-d/--dtype`, `-b/--batch`, `-s/--mnk` plus *op-specific sweep axes* as needed (e.g. `--layout`, `--modes`, `--mtp`). Those are legitimate data lists. **Do not** add bespoke behavior-toggle flags (no `--dsv4`, no `--only-*`) — every flag is a list the sweep iterates. 9. **Run clean on every supported card.** Gate on `get_gfx()` in `main()` so the test passes on all supported archs; arch-unsupported ops/candidates are filtered out *before* launch. Prefer the kernel's arch-dispatching wrapper over a file-per-arch (full section below). ## Canonical template ```python # SPDX-License-Identifier: MIT # Copyright (C) 2024-2026, Advanced Micro Devices, Inc. All rights reserved. import argparse import itertools import aiter import pandas as pd import torch import torch.nn.functional as F from aiter import dtypes from aiter.test_common import ( benchmark, checkAllclose, run_perftest, ) from aiter.jit.utils.chip_info import get_gfx # "gfx942", "gfx950", "gfx1250", ... torch.set_default_device("cuda") SUPPORTED_GFX = ["gfx942", "gfx950"] # every card this op is built/validated for def run_torch(x, weight, dtype=dtypes.bf16): # Reference only: fp32 math, cast back. Not timed, not in the table. ... return out.to(dtype) @benchmark() # call args (b, m, n, k, dtype, ...) become the table's left columns def test_op(b, m, n, k, dtype, layout): # build inputs/outputs in the layout the MODEL actually uses (see below); # ref = run_torch(...) candidates = { "triton": lambda: ..., # the path the model really runs "torch_einsum": lambda: ..., # optional torch kernel under test } if <kernel supported on this arch + config>: # e.g. get_gfx() != "gfx1250" candidates["ck"] = lambda: ... # else skip it (see rules below) flops = 2 * b * m * n * k # roofline numerator nbytes = (b * m * k + b * n * k + b * m * n) * x.element_size() ret = {"gfx": get_gfx()} # record the card in the table for name, fn in candidates.items(): out, us = run_perftest(fn) err = checkAllclose(ref.to(dtypes.fp32), out.to(dtypes.fp32), rtol=1e-2, atol=1e-2, msg=f"{name}: <op>") ret[f"{name} us"] = us ret[f"{name} TFLOPS"] = flops / us / 1e6 ret[f"{name} TB/s"] = nbytes / us / 1e6 ret[f"{name} err"] = err return ret def main(): # Whole-op arch gate goes HERE, not inside test_op: @benchmark always returns # the call-args dict, so an in-fn `return` still emits an args-only row. if get_gfx() not in SUPPORTED_GFX: aiter.logger.warning("<op> unsupported on %s; skipping", get_gfx()) return parser = argparse.ArgumentParser( formatter_class=argparse.RawTextHelpFormatter, description="config input of test" ) parser.add_argument("-d", "--dtype", type=dtypes.str2Dtype, nargs="*", default="bf16,", ...) parser.add_argument("-b", "--batch", type=int, nargs="*", default=[...], ...) parser.add_argument("-s", "--mnk", type=dtypes.str2tuple, nargs="*", default=[...], ...) # add -l/--layout ONLY if the op has real layout variants args = parser.parse_args() for dtype in args.dtype: # one table per outer-config element df = [] for layout, b, (m, n, k) in itertools.product( # sweep via itertools.product args.layout, args.batch, args.mnk ): df.append(test_op(b, m, n, k, dtype, layout)) df = pd.DataFrame(df) aiter.logger.info("<op> summary (markdown):\n%s", df.to_markdown(index=False)) if __name__ == "__main__": main() ``` `op_tests/test_batched_gemm_bf16.py` is a complete worked instance of this template (GEMM with triton/CK/einsum candidates, `bmn`/`mbn` layout, V4 shapes). ## Faithful to the real model call (do not test an idealized op) The point of the test is the kernel **as the model invokes it**, not a clean textbook version. Reproduce exactly: - **Preallocated output buffers.** If the model passes `YQ=`/`out=` a buffer it allocated, allocate and pass the same — don't let the kernel allocate its own. - **Real tensor layout, including non-contiguous views.** If the model feeds a transposed view (e.g. `o.transpose(0, 1)`), build the input as a transposed view of a contiguous tensor — not a fresh contiguous tensor of that shape. - **Couple input and output layout.** They are linked in the model. If you sweep an output layout (`bmn` vs `mbn`), the *input* must follow: the `mbn` (model) case is a transposed view of `[m, b, k]` (physically `mbk`) **and** a transposed view of `[m, b, n]` — not contiguous `[b, m, k]`. - **For a torch.einsum candidate, switch the layout by editing the subscript string**, not by transposing afterwards: `->sgr` is physically `[m,b,n]` (mbn), `->gsr` is physically `[b,m,n]` (bmn). Feed it the model's natural contiguous operand (e.g. `o` is contiguous `[s,g,d]`, so `x.transpose(0,1).contiguous()`). A test that quietly uses contiguous inputs when the model uses a transposed view gives the wrong perf **and hides correctness bugs** (see next rule). ## Report TFLOPS and TB/s, not just `us` A bare `us` doesn't say whether a kernel is compute- or memory-bound. Always add both roofline metrics per candidate, derived from the same `us`: ```python flops = 2 * b * m * n * k # GEMM: 2*M*N*K mul-add nbytes = (b * m * k + b * n * k + b * m * n) * x.element_size() # in + weight + out ret[f"{name} TFLOPS"] = flops / us / 1e6 # us -> s is 1e-6, FLOP -> T is 1e-12 ret[f"{name} TB/s"] = nbytes / us / 1e6 ``` Count the FLOPs and bytes the *op* actually does (adjust the formula per op: a quant/norm/attention kernel has its own element-traffic and arithmetic). Use `tensor.element_size()` for dtype width so fp8/bf16/fp16 are handled. Reading the table: small `m` (decode) is memory-bound (high TB/s, low TFLOPS); large `m` is compute-bound (TFLOPS approaches peak). ## Multiple test functions → multiple tables; reference patterns - **One `@benchmark` fn per distinct arg signature, one table each.** A file may hold several (e.g. a main bf16/fp8 sweep + an fp8 nm-asm cross-check with different columns). Give each its own `pd.DataFrame` + `aiter.logger.info(...)` via a tiny `summarize(name, rows)` helper. Forcing them into one table scatters NaN columns and is unreadable. - **Two correctness shapes are common:** - *Multi-candidate* (this template): several kernels vs one torch reference, all timed, dict-loop. - *Single-kernel-vs-reference, in-place output* (e.g. cache-writing kernels): clone the output buffer, run the kernel into one copy and the reference into another, then `checkAllclose`. Still record `us`/TFLOPS/TB-s/err. - **Prefer a shared reference from `aiter.ops.torch_ref`** when one exists; only hand-write `run_torch` when there is none. - **Do all the asserts a case needs; record one representative `err`.** A quant path checks the dequantized output *and* bit-exact scales: use `tol_err_ratio=` for the fraction of allowed element mismatches (fp8/bf16 rounding), and `rtol=0, atol=0` for values that must match to the bit (scales). ## Skip a candidate in configs it does not support Some kernels are only correct for some layouts/dtypes. Running them anyway pollutes the table with wrong-but-fast numbers. **Conditionally add** such a candidate and leave its cells `nan` elsewhere — e.g. `batched_gemm_bf16_CK` returns garbage (`err ≈ 0.99`) on a non-contiguous `mbk` input, so it is only added for `bmn`: ```python if layout == "bmn": candidates["ck"] = lambda: aiter.batched_gemm_bf16_CK(x, weight) ``` When you skip something, say so in a code comment with the reason. The non-zero `err` column is exactly how you discover these — never silently drop a candidate because its error is high; first confirm whether it's a real bug or an unsupported config, then skip with a comment. ## Run on every supported card (arch gating) A test must run **clean on every currently-supported card** — today `gfx942` (MI300) and `gfx950` (MI35x), plus any arch the kernel specifically targets. Never assume one GPU. Detect arch at runtime and filter *before* launching: ```python from aiter.jit.utils.chip_info import get_gfx # "gfx942" / "gfx950" / "gfx1250" / ... ``` - **One test, all archs — drive the arch-dispatching wrapper; do NOT write a file per arch.** Most aiter kernels expose a public wrapper that routes to the wave64/wave32 (or gfx-specific) implementation internally by `get_gfx()` — call that wrapper and the single test covers every arch. Only the *behavioral* arch differences need handling in the test (e.g. gfx1250 uses a linear FP8 layout, so force `preshuffle=False` there; keep the kernel call and the reference in sync). Never import an `*_gfx1250`/arch-suffixed kernel directly to make a parallel test file. - **Op not built/supported on this arch → skip in `main()`** with an allow-list early `return` (skips the whole sweep cleanly — no rows): ```python SUPPORTED_GFX = ["gfx942", "gfx950", "gfx1250"] if get_gfx() not in SUPPORTED_GFX: aiter.logger.warning("<op> unsupported on %s; skipping", get_gfx()) return ``` Do **not** gate by returning from inside the `@benchmark` fn: that wrapper always returns the call-args dict, so an in-fn `return` still emits an args-only NaN row. Prefer a **positive allow-list** (`not in [...]`) over a deny-list so an unknown new card doesn't silently run an unbuilt kernel and crash. - **One candidate / one sub-check unsupported on this arch → drop just it**, with a warning naming arch + reason — e.g. the fp8 nm-asm cross-checks are wave64-only: ```python if get_gfx() != "gfx1250": summarize("hca_fp8", [test_hca_fp8(bs) for bs in args.fp8_bs]) else: aiter.logger.warning("gfx1250: skipping wave64-only fp8 cross-checks") ``` - **Record the card**: put `"gfx": get_gfx()` in the returned dict so one table is self-describing across cards. In-tree precedent: the flydsl `fused_compress_attn` wrappers dispatch wave64/wave32 internally (one `test_flydsl_compress_attn.py` covers all archs); `test_deepgemm.py` / `test_gemm_a4w4.py` allow-list a single arch; `test_gemm_a8w8.py` does per-arch candidate/dtype/shape gating with warnings. ## Deriving model shapes (don't guess) When adding "test op X for model Y" shapes: 1. **Read the real `config.json`** for the actual dims (`grep`/`python -json`), don't assume the dataclass defaults. 2. **Map model semantics → the kernel's `(b, m, n, k)`** and write the mapping in a comment. Worked example — DeepSeek-V4 grouped output LoRA (`atom/models/deepseek_v4.py`, `batched_gemm_bf16(o.transpose(0,1), wo_a, YQ=y)`):
GitHubで見る
この SKILL.md は非常に大きいため、SkillsMP では最初のセクションだけを表示しています。 GitHubで見る