Rebuild the exact INT8 MATMUL fast path on the raw MMUL with zero-point correction (#151) - #162
Merged
Merged
Conversation
…nt correction (#151) Profiling the shipped kernel on the 1022:17f0 NPU attributed ~99% of its time to scalar work: one lda.s8 per element with per-element address arithmetic and lane-by-lane vpush.hi.16 widening, feeding exactly one vmac per K step. The matrix unit was under 1% of kernel time, and the 64x64x32 benchmark measured 334 us / 0.785 GOPS against an ~86 us per-submission overhead floor. The zero-point identity C = A.B - zb*rowsum(A) - za*colsum(B) + K*za*zb lets the MMUL run on the caller's raw INT8 bytes with no widening and no packing. Shapes on the native 8x8x8 grid (M % 16, K % 8 with K >= 16, N % 16 - the vectorized kernel unrolls 2x2 micro-tiles, and a single-tile K degenerates the DMA transform into a zero step) now take a design that is structurally the BF16 _build_matmul: DMA-side micro-tile layout transforms deliver A, B, and C in MMUL order at zero core cost, the fork's vectorized i8/i32 mm kernel (and its zero sibling) computes the raw product, and one small correction kernel derives both sums via MMUL against a constant ones tile and applies the correction in place. Every term is provably inside INT32 for the admitted envelope (K <= 512); the proof is in the correction kernel's docstring. Off-grid shapes keep the scalar exact kernel; the INT16-widening template is retired. Admission, the binding plan, and the guest ABI are unchanged. Measured on the same NPU and toolchain: 72.089 us submit-to-complete median, 99.427 us p95, 3.636 effective GOPS - 4.7x the widening baseline, now at the measured per-submission overhead floor, with every output still matching the shared exact oracle. The remaining latency is submission-path cost; #151's overlap and striping steps stay open. The new tiled path is oracle-checked by a non-ignored hardware test across three shapes including zero points at both extremes, closing the gap where the fast path was validated only by the ignored benchmark. That test caught a real miscompile in the pinned Peano release: a default-constructed aie::mmul's zero-on-first-mac flag is mis-rotated in the software-pipelined loop epilogue at trip count exactly 2, re-zeroing the accumulator on the final mac. The correction kernel constructs its accumulators explicitly zeroed, which makes every mac's config uniform and is correct at every trip count; the erratum is documented at the workaround site. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
aravishankar-mp
added a commit
that referenced
this pull request
Aug 28, 2026
…148) The design doc pins what the prototype exposes (block-scaled MATMUL with MXINT8 semantics on the proven block-8 decomposition), the guest-visible numerical contract (bit-exact against an FP32 fold in documented ascending-k order, rather than an overclaimed order-independence), the XBFP experimental container, and the crate-integration sequencing that stays out of the concurrent #162 work until it merges. Step 1 of that sequencing is done on silicon: a K=512 flavor-1 kernel matches the fold-order oracle on all 64 output lanes, on inputs constructed so 51 of the 64 lanes distinguish the FP32 chain from a single-rounded f64 sum -- each later chunk sits just below the running accumulator's half-ULP, so only the documented per-step rounding produces the observed bits. The oracle (dot_fold_f32) rests on the fact that every block-8 MAC result is an integer times a power of two below 2^17 and hence exact in FP32, leaving the chain order as the only rounding source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7 tasks
SnowCheetos
approved these changes
Aug 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
First optimization pass for #151, following its investigation order: profile on metal, then keep
more native INT8 work on the matrix unit.
Step 1 evidence (Peano remarks / disassembly). The shipped kernel's K-step loop body was ~200
sequential scalar instructions — one
lda.s8per element with per-element address arithmetic andlane-by-lane
vpush.hi.16widening — feeding exactly onevmac. The matrix unit was <1% ofkernel time. Measured on the reference
1022:17f0NPU: 340 µs submit-to-complete p50 against an~86 µs per-submission overhead floor (the 1,024-element FP8 case), i.e. ~250 µs of almost purely
scalar kernel time.
The fix (investigation-order step 2). The exact expansion
C = A·B − zb·rowsum(A) − za·colsum(B) + K·za·zblets the MMUL run on the caller's raw INT8bytes — no widening, no packing. Shapes on the native 8×8×8 grid (
M % 16, K % 8 with K ≥ 16, N % 16; the vectorized kernel unrolls 2×2 micro-tiles, and a single-tile K degenerates the DMAtransform into a zero step size) now take a design that is structurally the BF16
_build_matmul(the fork's single-core design, per the OpenVINO-precedent invariants in #151):
i8/i32 mmkernel and itszerosibling compute the raw product(subsumed, not reimplemented);
the correction in place. Every term is provably inside INT32 for the admitted envelope (K ≤ 512);
the proof is in the kernel's docstring.
Off-grid shapes keep the scalar exact kernel (including the shared 2×3×2 corpus case); the
INT16-widening template is retired. Admission, the binding plan, and the guest ABI are unchanged —
the helper reports the identical
[4096, 2048] / [8192]plan for the benchmark shape.Result, on metal
Same NPU (
1022:17f0rev 0x20), same v2026.08 toolchain, same benchmark protocol (20 warmups, 200measured submissions, exact-oracle validation of every output after timing):
The p50 now sits at the measured per-submission overhead floor, so the remaining latency is
submission-path cost, not kernel cost — #151's overlap (ring depth) and striping steps stay open,
and are where the next order of magnitude lives.
A toolchain miscompile, found and worked around
The new non-ignored oracle test (below) caught a real code-generation bug in the pinned Peano
release: a default-constructed
aie::mmulcarries a zero-on-first-macflag, and thesoftware-pipelined loop epilogue mis-rotates that flag's config register at trip count exactly 2 —
the final
macre-zeroes the accumulator, collapsing the sum to the last tile. Diagnosed from thedisassembly (
vmac … r29wheremov r29, r28shares a bundle withmova r28, #0x308, reading thestale
#0x309). The 64×64×32 benchmark could never see it (K/S = 8); a 16×16×16 shape hits itdirectly. The correction kernel now constructs its accumulators explicitly zeroed
(
aie::zeros<acc32, …>(), the fork's own kernels' pattern), which makes everymac's configuniform and is correct at every trip count. The erratum is documented at the workaround site.
Note the shipped BF16/
mm.ccdesigns are structurally immune (they accumulate via C loaded frommemory, zeroed by
zero.cc).Coverage
tosa_int8_matmul_tiled_path_matches_the_exact_oracle_on_the_npu— non-ignored, hardware-gated,fail-loud under
VIRTIO_ACCEL_XDNA_REQUIRE_HARDWARE=1— checks the tiled path bit-exactly againstthe shared
dot_i8_i32oracle across three shapes (16×16×16, 32×24×16, 64×64×32) with zero pointsat both extremes (
[-128,127],[127,-128],[0,-1]) and full-range inputs including −128. Thiscloses the review finding that the fast path was oracle-checked only by an
#[ignore]d benchmark.Compatibility
Admission is unchanged; which compiled template a shape takes changes (INT16-widening path
retired; shapes like 64×68×32 that took it now take the scalar exact kernel — both exact, and the
retired path's throughput was within noise of scalar). Artifact cache keys rotate with the helper
source, so no stale kernel can be served.
Checklist
behavior.
layout.json,vectors.json,scenarios.json,requirements.json, and performance budgetsare still authoritative inputs, not regenerated by accident.
dependency changes.
unsafecode was added or modified.Verification
All on this branch, on the reference NPU host:
On metal with
VIRTIO_ACCEL_XDNA_REQUIRE_HARDWARE=1: the full hardware suite (26 passed, 2intentionally ignored benchmarks) run three times from a cold artifact cache (36–40 s each,
recompiling every tier through the new helper) plus warm; the shared conformance suite; and both
ignored benchmarks. One early suite run failed a single test immediately after a helper edit and
never reproduced across the three cold reruns whose logs I kept — I could not identify the test
from the discarded output, so flagging it here rather than pretending it didn't happen; the three
captured cold runs are clean.
🤖 Generated with Claude Code