Purpose
Enumerate every (kernel, shape) the model needs and verify each is numerically correct on real NPU2 before integrating them into a block. Phase 1 isolates per-kernel correctness from integration bugs and extends the shared kernel registry with the shapes this model exercises, so Phase 2+ (and future deployments) can rely on a verified shape catalog.
The core loop is small:
- Derive the kernel × shape list from the model's config (worked example:
the llama-3.2-1B rows in
kernel_registry/supported_kernels.md). - For each (kernel, shape) → obtain a correctness verdict (harness
atol/rtol PASS, or diagnosis cosine) → record verdict +
mean_rel_L1- perf.
- For each new (kernel, shape), append a row to that kernel's "tested
shapes" table in
kernel_registry/supported_kernels.mdand itsdetails/<Kernel>_bf16.md(Used by =<model>); record the full per-kernel results under<model>/docs/.
Two sources of the per-kernel correctness verdict (use whichever exists):
- Standalone harness (primary) — a self-contained
make runthat compiles ONE kernel/block to an ELF and checks its full output element-wise against an FP32 reference:np.isclose(|out−ref| ≤ atol + rtol·|ref|), every element must pass. The harness prints[precision] mean_rel_L1=… rtol=… atol=… PASS!/failed.— that PASS/FAIL IS the gate. This is the GPU/vLLM-aligned standard (rtol=1.6e-2is PyTorch/vLLM's canonical bf16 tolerance;atolis sized per kernel to the measured datapath error). The registry'sdetails/<Kernel>_bf16.md§Tolerances documents each kernel's exactrtol/atol. Upstream ships a harness for the FFN block (programming_examples/llms/llama_kernel_builder/ffn_swiglu/) plus the top-level kernel examplesmatrix_multiplication/bf16_in_bf16_outandmatrix_multiplication/bf16_in_fp32_out(the BF16 GEMM, split by output dtype — the legacymatrix_multiplication/bf16is kept for NPU1),matrix_vector_multiplication/bf16,flash_attention/kernel_fusion_based,eltwise_add,weighted_rms_norm,rms_norm,rope_lut. - In-context diagnosis (fallback) — for a kernel with no standalone
harness, there is no isolated output to run element-wise atol/rtol on,
so fall back to
make diagnosis: per-layerffn_outcosine vs the HF bf16 reference. The per-layer cosine reflects every kernel in that layer. Cosine is the fallback lens only — not the gate for any kernel that has a harness.
Phase 1 PASS criteria (HARD GATES)
Every (kernel, shape) the model needs must satisfy all four. Each catches a different bug class:
A correctness signal exists for the kernel × shape: either a standalone harness
make run(preferred — an isolated leaf check at the registry's atol/rtol) OR the kernel is covered bymake diagnosisper-layer cosine. A kernel with neither is a silently-unverified kernel — not allowed.Numerical correctness (the real correctness gate — not theoretical compile-time rules):
- Harness-backed kernel → the harness's full-output element-wise
np.isclosecheck PASSES at that kernel'srtol/atol(details/<Kernel>_bf16.md§Tolerances) vs the FP32 reference. This is the GPU/vLLM-aligned standard and the gate for every kernel that has a harness. - No-harness kernel → fall back to the
make diagnosisper-layer cosine vs HF bf16 staying healthy (no cliff at the layer exercising this kernel). Catches: silent-corruption tile configs (e.g. GEMMN % (tile_n × herd_n) != 0, which the builder does NOT assert). Trust the measured atol/rtol verdict, not the rule.
Record
mean_rel_L1(the headline metric) plusmax_abs/max_relwhen the harness prints them.mean_rel_L1is the registry's per-shape accuracy column and a cheap regression baseline for future deployments; the gate itself is the harness's pass/fail atrtol/atol.- Harness-backed kernel → the harness's full-output element-wise
Tile utilization documented: each (kernel, shape) records its herd config. Targets:
- Compute-bound (GEMM, FA): full 8×4 = 32 tiles
- Row-parallel (RMSNorm, RoPE, GEMV-decode, SiLU+Mul, Eltwise): 8×1 = 8 tiles When achieved < target, justify in the catalog's Notes column (e.g. "M=1 decode RMSNorm uses 1 tile because batch=1 has no row-parallelism"). Catches: silent under-utilization.
Registry row written: every verified (kernel, shape) that is new to the registry is appended to that kernel's "tested shapes" table in
programming_examples/kernel_registry/supported_kernels.mdand itsdetails/<Kernel>_bf16.md, with Used by =<model>, matching the existing rows' column schema. The full per-kernel results (mean_rel_L1+ harness PASS or diagnosis cosine, max_abs/max_rel, perf, tile config) also go to<model>/docs/. Catches: deployments that pass without leaving a reusable record.
Failure on ANY criterion blocks Phase 2.
Knowledge base references
PRIMARY (read before starting):
programming_examples/kernel_registry/supported_kernels.md— index of every supported leaf kernel + its "tested shapes" table (shape, tile config, perf,mean_rel_L1, Used by, status) across deployments. This is the menu of known-good shapes to copy from and the table you extend.programming_examples/kernel_registry/details/<Kernel>_bf16.md— per-kernel detail: the numerical datapath, Tunable parameters (knobs + hard constraints + tradeoffs), tolerances, per-shape data, and the reproduce commands (which harness, how to run). Read the page for each kernel your model needs. GEMM is the exception: it is split by output dtype intodetails/GEMM_bf16_in_bf16_out.mdanddetails/GEMM_bf16_in_fp32_out.md(BF16-out has a--high-precisiontier — fused-cast / drain = FP32-accumulate + single cast, GPU-standard ~9.3e-3; F32-out always FP32-accumulates).programming_examples/kernel_registry/registry_lookup.py— the machine-readable half of the registry.gemm_config(M,K,N, output_dtype, precision)returns the registry's best measured{method, tile, gflops, mean_rel_L1}for a shape from the companiondetails/*.json, and raises (no silent guess) for an unmeasured shape — so Phase 1 recording a new GEMM shape is what unlocks the programmatic lookup that Phase 4 builders consume. The.mdtables mirror these.jsonfiles for humans.
Workflow
Step 1: Derive the model's shape list
Read the HF config.json (or the model's <model>_weights.py:Config
dataclass after Phase 0). Map each kernel call site to its shape using
standard transformer identities (Q proj output dim = n_heads * head_dim,
etc.). The llama-3.2-1B rows in kernel_registry/supported_kernels.md
(and programming_examples/llms/llama32_1b/'s prefill/decode call sites) are the worked
example of that call-site → shape mapping.
Write the working shape list — one row per (kernel, shape) with
mean_rel_L1 + verdict + perf + status columns empty (Step 2 fills
them) — under <model>/docs/
(e.g. <model>/docs/development_progress/phase1_kernels.md).
This is scratch tracking for the deployment, not a registry file; the
registry rows get written in Step 3.
Before running anything, read each needed kernel's details/<Kernel>_bf16.md
Tunable parameters / constraints section and check the model's dims
against it (alignment, max-K, placeability notes) — flag likely walls
BEFORE compile time.
(If the model has unusual ops — Q/K Norm, post-norm, ops between
currently-fused launches — flag in <model>/TODO.md as a Phase 2
prerequisite. The actual integration decision lives in
phase-2-single-block-validation Step 0a.)
Step 2: Per-kernel verification
For each (kernel, shape) in your Step 1 shape list:
a. Pick the correctness source + initial tile config.
Each kernel's details/<Kernel>_bf16.md has its reproduce commands (which
harness, how to run) and a Tunable parameters table (knobs + hard
constraints + tradeoffs). If your shape exists in that kernel's "tested
shapes" table in supported_kernels.md → reuse the tile config. Else →
mirror the nearest-shape entry; verify the hard constraints hold for your
shape; adjust if not (e.g., GEMM N % (tile_n × herd_n) != 0 → pick
smaller tile_n).
b. Run correctness (+ profile where a harness exists).
If a standalone harness or top-level example covers the shape:
cd programming_examples/<harness_or_example_dir>
flock -x -w 1800 /tmp/mlir-air-npu.lock make run # element-wise atol/rtol vs FP32 ref → PASS!/failed.
flock -x -w 1800 /tmp/mlir-air-npu.lock make profile # timing
If no standalone harness exists for the kernel, use the deployment's per-layer diagnosis:
cd programming_examples/llms/<model>
flock -x -w 1800 /tmp/mlir-air-npu.lock make diagnosis # per-layer ffn_out cosine vs HF bf16
NPU is shared on this machine — every NPU command must be flock-wrapped
(see project memory). Compile-only steps don't need the lock.
If the harness reports failed. (or, for a no-harness kernel, the
diagnosis cosine drops) → see "Failure modes" below. Bound: 1 retry per
recipe per shape; if still failing, escalate to TODO.md "Active blockers".
c. Record results in <model>/docs/. Fill the scratch row:
mean_rel_L1 (+ harness PASS / diagnosis cosine), profile (ms / GFLOPS)
where measured, tile config used, tiles in flight, status, source (harness
vs diagnosis), and Notes (especially when tiles-in-flight is below
target — justify why).
Step 3: Extend the kernel registry
For each (kernel, shape) verified in Step 2 that's new (not already
in that kernel's "tested shapes" table in supported_kernels.md), append
a row to both supported_kernels.md and the kernel's
details/<Kernel>_bf16.md per-shape table: shape + tile config +
tiles-in-flight, "Used by" listing your new model, mean_rel_L1 +
profile from Step 2, status. Match the existing rows' column schema for
that kernel exactly.
This grows the registry organically: each verified deployment extends the
menu of known-good shapes future deployments can copy from. (Adding a
new kernel — not just a new shape of an existing one — is the heavier
add-kernel workflow with its own parity checklist; Phase 1 only adds
shapes to kernels the registry already covers.)
Failure modes
When a test fails (harness failed. / diagnosis cosine drop, compile
error, hang), match the symptom to a likely cause. These are debug
starting points, not gates — trust the measured atol/rtol verdict, not the
rule. The relevant kernel's details/<Kernel>_bf16.md (constraints /
placeability section) is the authority for that kernel's hard limits.
| Symptom | Likely cause | Where to look |
|---|---|---|
'aiex.npu.push_queue' op Repeat count exceeds [0:255] |
GEMV K too large; auto-split outer dim ≥ 256 | Set k_split so K = k_split × inner and k_split ≤ 255; see details/GEMV_bf16.md |
Allocator exhausted available buffer descriptor IDs |
BD pool exhausted (non-aligned dim, or non-monotonic placeability of the iteration count) | Pad dim to 1024-aligned (GQA-aware reindexed padding) OR use kernel-first split-ELF path; see the placeability notes in the kernel's details/ page |
L2 capacity exceeded (matvec builder assert) |
GEMV staged buffer > 512 KiB | Reduce tile_m (e.g., 8 → 2 for K=8192) or herd_m; see details/GEMV_bf16.md |
| Output all-zero / cosine = NaN | Bare-herd kernel without launch+segment wrapper | Wrap the bare herd in air.launch/air.segment (see ffn_swiglu harness for the multi-launch pattern) |
| Cosine = 0.02 or other small | GEMM N % (tile_n × herd_n) != 0 silent corruption (builder does NOT assert) |
Pick tile_n so divisibility holds at this N |
| FA all-NaN at runtime | Compile-flag mismatch on attn_npu2.cc macros; OR seq-first dk_chunks>1 path at head_dim≥128 |
see debug-fa-runtime-failure skill |
| Compile hangs > 10 min | Compiler scaling issue at large multi-launch | Cap and document; don't retry |
For any failure not in the table, invoke superpowers:systematic-debugging.
Update protocol
On Phase 1 PASS:
- Mark Phase 1 in
<model>/TODO.md, append "(N/N kernels PASSED)" - Append summary to
<model>/docs/development_progress/progress.md(per-kernelmean_rel_L1+ verdict, total time) — the per-model durable record kernel_registry/supported_kernels.md+details/<Kernel>_bf16.mdcarry a "tested shapes" row (Used by =<model>) for every new (kernel, shape) this model exercises — the cross-model durable record