Skip to content

W4A16 GEMM on XDNA2: 5.94 TOPS via open stack (IRON 1.4.x + Peano) - #8

Merged
Jonas-Augustinus-Linus merged 5 commits into
mainfrom
w4a16-gemm
Aug 16, 2026
Merged

W4A16 GEMM on XDNA2: 5.94 TOPS via open stack (IRON 1.4.x + Peano)#8
Jonas-Augustinus-Linus merged 5 commits into
mainfrom
w4a16-gemm

Conversation

@Jonas-Augustinus-Linus

Copy link
Copy Markdown
Owner

TileFuse-style W4A16 GEMM (int4 AWQ-g128 weights, bf16 activations) running on the whole 4×8 Strix array under the fully open stack.

Results

  • Numerics PASS on 512³, 2048³ (4/8 col), 2048×4096×4096 — max normalized err 8.95e-3
  • 2048³ 8-col: 2.89 ms = 5.94 TOPS (+28% vs the 4.64 TFLOPS bf16 baseline, 89% of the i8 record, 66% of chess-TileFuse's 9 TOPS)
  • Prefill-like 2048×4096×4096: 6.24 TOPS
  • Column scaling 1.99× (4→8), compute-bound

Contents

  • examples/mlir-aie/w4a16_gemm/: vendored pinned kernel (byte-identical, now sha256-gated in validate-repo.sh), NumPy AWQ-g128 packer (bit-exactness test shipped: test_packing.py), IRON 1.4.x whole-array design with packed-byte B fifos (3.76× less weight DRAM traffic) and L1 weight-stationary dequant
  • Docs: example README, MLIR-AIE.md/XDNA2.md results, GOTCHAS M12–M14, stale compile-only claims cleaned repo-wide incl. translations
  • A high-effort code review (10 verified findings) was applied in the last two commits: silent-corruption guard on the pack format, shape guards for the 2²⁰ BD stride cap, sha256 vendored-file gate, measured-number consistency (A/B 0.94 vs 5.81 = 6.2×; final headline 5.94)

Known limits

  • N ≤ 4096 at m=64 (BD stride cap), <4 columns unsupported — both guarded and documented
  • Peano vs chess gap (~5.9 vs 9 TOPS) is kernel-loop scheduling, next optimization target
  • llama.cpp #21725 integration path is the follow-up

🤖 Generated with Claude Code

Jonas-Augustinus-Linus and others added 5 commits August 16, 2026 12:45
…N whole-array design

- mix_int4_ATB.cc / zero.cc vendored byte-identical from
  glassescrab/mlir-aie@8c3d2be (sha256 matches scripts/check-w4a16-compile.sh pins)
- packing.py: exact 4352 B/tile byte layout (int4 data in 8x8 micro-tiles,
  bf16 per-column scales, duplicated int8 zero-points) + NumPy dequant and
  tile-faithful bf16-accumulation references; packer verified bit-exact
  against a scalar model of the kernel's read path
- w4a16_gemm.py: stock whole_array dataflow with packed-byte B fifos and
  half-M-tile A consumption (consumer_obj_type) matching the kernel's DIV=2
  weight-stationary L1 dequant cache

Not yet hardware-verified; first NPU run follows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Numerics (signed A, random int4 AWQ-g128 weights, seed 1726250518):
- 512^3:  max err/|A||B| scale 1.07e-2, mean 2.1e-3  -> PASS
- 2048^3: max err/|A||B| scale 8.95e-3, mean 1.3e-3  -> PASS
  (elementwise-relative stats vs f32 reference are cancellation-dominated
  on near-zero outputs; the |A||B| accumulation-scale bound is the honest
  bf16/bfp16 criterion and a real dataflow bug would sit ~100x above it)

Performance (NPU time, 20 iters):
- 2048^3: 2.89 ms = 5.94 TOPS vs 4.64 TFLOPS bf16-via-bfp16 baseline (+28%)
  with 3.76x less B DRAM traffic; i8 record on this array is 6.65 TOPS
- 512^3:  158 us = 1.70 TOPS

Three fixes en route:
- -DAIE_API_EMULATE_BFLOAT16_MMUL_WITH_BFP16 on the kernel: without it the
  8x8x8 bf16 aie::mmul lowers via 1/4-rate native bf16 (0.94 TOPS, 6.2x)
- B packed in column-distribution order so each column's per-pass stream is
  one contiguous fill: a strided multi-row B tap needs an inexpressible 4D
  BD at 2048^3, and per-row fills exhaust the 16 BDs per shim tile
- verifier bounds error against the |A| @ |Bdq| accumulation scale

Gotcha found: changing ExternalFunction compile_flags inside a helper does
not invalidate the @iron.jit cache (recipe hash covers only the jit'd
function's bytecode and its own flags) - clear ~/.npu/cache or rename the
object file when toggling kernel defines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…14, changelog

- examples/mlir-aie/w4a16_gemm/README.md: measured table (1.70/2.98/5.94/6.24
  TOPS, all CPU-reference PASS), design delta vs stock whole_array, packed
  tile format, numerics methodology, shape constraints
- MLIR-AIE.md: W4A16 section beside the GEMM baselines
- XDNA2.md: 'where this goes next' item 3 upgraded from compile-only to
  hardware-verified; headline bullet added
- GOTCHAS.md M12 (1/4-rate 8x8x8 bf16 mmul without the bfp16 define, 6.2x),
  M13 (ExternalFunction compile_flags invisible to the @iron.jit cache),
  M14 (shim-DMA BD dims/count/stride limits that only bite at larger shapes)
- validate-repo.sh: exempt the two sha256-pinned vendored kernel files from
  whitespace normalization (byte-identity is the provenance guarantee)

Translations (de/fr/ko/ja) deliberately not updated in this commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ipped packer test

- packing: pack_b returns a PackedB blob tagged with the n_aie_cols it was
  packed for; assert_packed_for turns a blob/design column mismatch (same
  size, same dtype, silently wrong C) into a loud error, checked in main()
- design: enforce N <= 4096 (inclusive 2^20 C write-back stride range) and
  even M/(m*4) row-block count as clear asserts in _build_design, instead of
  cryptic aiecc/step_tiler failures minutes later
- DIV is now derived from the vendored source's 'constexpr int DIV' (no
  #ifndef guard upstream, so -D cannot override it without breaking the
  byte pin); core_fn documents the exactly-DIV-calls-per-B-tile g_counter
  invariant and why desync is persistent and silent
- ship test_packing.py: bit-exactness harness walking the kernel's exact
  read path over the packed blob, including a non-identity column ordering
  and a negative assert_packed_for check (CPU-only)
- check-w4a16-compile.sh: pin -DAIE_API_EMULATE_BFLOAT16_MMUL_WITH_BFP16
  (the M12 6.2x trap) and point its scope line at the verified example
- validate-repo.sh: vendored files are now sha256-asserted against the pins
  instead of skipped — byte-identity subsumes whitespace/conflict scanning
  and catches formatter or merge damage (negative-tested)
- --atol help now states it only floors the printed rel-err denominators
  and takes no part in the PASS/FAIL verdict

Re-verified after the changes: test_packing.py PASS, 2048^3 NPU rerun PASS
(max normalized err 8.95e-3), compile probe PASS, validate-repo.sh PASS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mber consistency

- retire every remaining 'W4A16 is compile-only' claim contradicting the
  hardware result: OPEN-NPU-LAB.md (asset table, project ladder, boundary
  bullet) + OPEN-NPU-LAB.ko.md, APPLICATIONS.md table row + ko/de/fr/ja,
  XDNA2 item-3 status paragraph in ko/de/fr/ja, plus two more found by
  sweep: LLM-ROADMAP.md step 1 (now struck through as done) and
  RESEARCH.md's TileFuse note — all now state CPU-reference PASS and
  5.94 TOPS with the open problems (energy, llama.cpp, chess gap) explicit
- 0.94-vs-X consistency: the controlled A/B pair is 0.94 vs 5.81 TOPS
  (6.2x, same session, 10 iters); 5.94 is the separate final tuned run.
  GOTCHAS M12, example README, and the kernel-flag comment now all say so
- GOTCHAS M14 + README: BD stride limit corrected to inclusive <= 2^20
  (the verified N=4096 C stride is exactly 2^20)
- README documents the shipped test_packing.py harness and the PackedB
  column-count tag; CHANGELOG references the harness by name

validate-repo.sh (link checks included): PASS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Jonas-Augustinus-Linus
Jonas-Augustinus-Linus merged commit 9f05d55 into main Aug 16, 2026
1 check passed
@Jonas-Augustinus-Linus
Jonas-Augustinus-Linus deleted the w4a16-gemm branch August 16, 2026 12:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant