AMX-native bf16 packed operands: pack once at prepare time, not per tile - #6
Conversation
…nce, not per tile Adds NdarrayAmxBf16A/NdarrayAmxBf16B, two MMMInputFormat implementations that convert to bf16 (and, for B, VNNI-pack) once at prepare_one time and hand a new kernel (ndarray_avx512_bf16_native_mmm_f32_16x16, packing index 1) owned Rust panels it consumes directly -- no allocation, conversion, or packing inside AddMatMul, unlike the existing kernel from PR #5 which redoes all three on every 16x16 tile call. The new benchmark cases (P0, P1) measure this through the real MatMatMulKer path at two operand-preparation lifetimes and compare against the earlier B1/B2 raw-AMX baselines. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012wrzeZAdwGYTCKoxamwQht
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_01060589-cf0c-43ec-9eed-283d1216fa51) |
Same class of bug as PR #5's earlier fix: linalg/src/lib.rs compiles the x86_64 module tree under feature = "foreign-inventory" on any host arch, to enumerate x86_64 kernel names as metadata for cross-compiled builds, but ndarray is only a Cargo dependency on x86_64. ndarray_amx_native_pack.rs and ndarray_bf16_native_gemm.rs use ndarray types throughout rather than in one or two functions, so rather than per-item stubs (this file's other pilot kernels' pattern) both get a whole-module #![cfg(target_arch = "x86_64")] gate, and the mmm.rs registration that references their symbols is gated to match. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012wrzeZAdwGYTCKoxamwQht
|
Fixed the Verified: Generated by Claude Code |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3116f22201
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| unsafe { | ||
| let src = t.as_ptr_unchecked::<f32>(); |
There was a problem hiding this comment.
Obtain approval before adding unsafe pointer packing
This introduces raw-pointer reads for the new packing path, and the B/kernel side additionally round-trips a PackedBf16B through *const u8; the repository explicitly prohibits adding unsafe without permission. Replace these operations with safe tensor/packing APIs or obtain explicit approval before merging.
AGENTS.md reference: AGENTS.md:L279-L282
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed the banner-comment finding in the sibling thread (2aae5c2).
On this one: I'm not removing the unsafe here. The repo's own sibling AMX kernel, amx_bf16.rs's PackedAmxBf16A/PackedBf16K2 (the production kernel this PR's packing formats mirror, per the panel-geometry note in the PR description), does the identical class of raw-pointer packing unconditionally (Blob::new_for_size_and_align + raw writes in its prepare_one_view, no extra annotation). CLAUDE.md's rule is "no new unsafe outside linalg kernels without explicit permission" — this file lives under linalg/src/x86_64/, is doing the same class of tile-packing work as its unsafe-using neighbor, and the *const u8 round-trip for PackedBf16B is the documented zero-copy design point of this PR (see the "Real complication" section of the PR description) — a safe API doesn't exist for it, that's the actual finding from W0's survey.
So this reads as in-scope, precedented unsafe in a linalg kernel rather than a new-kind-of-unsafe needing separate approval. Happy to be overruled by a human maintainer if the convention is read more narrowly than that.
Generated by Claude Code
| Ok(tensor) | ||
| } | ||
|
|
||
| // ───────────────────────────── A: activation lifetime ────────────────────── |
There was a problem hiding this comment.
Remove the section-banner comments
This newly added decorative section banner directly violates the repository convention against section banners. Rely on the following item documentation or split the A/B implementations structurally rather than retaining banner comments.
AGENTS.md reference: AGENTS.md:L268-L273
Useful? React with 👍 / 👎.
Codex review flagged the A/B section dividers as banner comments against the repo's inline-comment style; the doc comments directly above each section already carry the structure. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012wrzeZAdwGYTCKoxamwQht
|
The Generated by Claude Code |
Follow-up to #5's B0-B3 gap diagnosis (see the diagnostic comment/bench on that PR), which proved the B1-to-B3 gap was entirely
add_mat_mul_bf16re-converting and re-VNNI-packing both operands on every 16x16 tile call, notTDPBF16PSitself. This PR builds tract's packing layer for that fix: two newMMMInputFormatimplementations (NdarrayAmxBf16A,NdarrayAmxBf16Binlinalg/src/x86_64/ndarray_amx_native_pack.rs) that convert to bf16 (and, for B, VNNI-pack viandarray::simd::PackedBf16B) once atprepare_onetime, plus a new kernel (ndarray_avx512_bf16_native_mmm_f32_16x16, packing index 1, inndarray_bf16_native_gemm.rs) whoseAddMatMulstep callsbf16_tile_gemm_16x16_packeddirectly on the already-packed panels — zero allocation, zero conversion, zero VNNI packing inside the tile call. Note: this PR is stacked on #5 (it merges that branch's diagnostic work as its base) since it depends onndarray_bf16_gemm.rsand the shared bench harness.This branch is additive-only, zero effect on automatic dispatch. Same non-
inventory::submit!registration trick as #5's kernel — reachable only by direct construction (benches/tests), invisible toMmmDispatch::native(), verified by a dedicated test (amx_native_candidate_is_not_reachable_through_automatic_dispatch). Nocore::ops::einsum/ model-optimizer wiring in this PR.W0 survey findings
tract already has exactly the extension point this task needed:
MMMInputFormat/MMMInputValue(linalg/src/frame/mmm/input_store.rs) plus thepacking[$pnum] = $pid => |k| k.with_packing(...)clause onMMMKernel!/MMMRustKernel!. A production kernel already uses this pattern for real AMX bf16 packing —avx512amx_mmm_f32_16x16(linalg/src/x86_64/mmm.rs, gated#[cfg(tract_amx_bf16)]) withPackedAmxBf16A/PackedBf16K2(linalg/src/x86_64/amx_bf16.rs) — and its panel geometry (r=16, K padded to a multiple of 32, row-major bf16 for A, K=2-inner VNNI blocks for B) turns out to be bit-identical tondarray::simd::PackedBf16B's internal layout, which made mirroring that existing format straightforward.ConstvsValueoperand provenance (A activation / B weight) is visible incore/src/ops/einsum, confirming the packing lifetimes named in the brief are plausible, without needing to wire into it.Real complication:
bf16_tile_gemm_16x16_packedtakes&PackedBf16B, a struct whose fields (Vec<u16>,usize) are private — there is no public constructor that borrows bytes zero-copy (from_le_bytesallocates and copies). A byte-blobMMMInputValue(the pattern every existing packed format uses) would therefore force a per-tile-call allocation+copy to reconstitute aPackedBf16B, defeating the point. The fix:NdarrayAmxBf16BValuestores ownedVec<PackedBf16B>panels directly (not raw bytes in aBlob), andpanel_bytes()returns a pointer to thePackedBf16Bvalue itself (cast to*const u8), whichAddMatMulreinterprets back (&*(pb as *const PackedBf16B)) — zero-copy, zero-alloc, at the cost of a custom (non-EagerPackedInput)MMMInputValueimpl with hand-writtenClone/Debug/Hash/Eq(viaPackedBf16B::as_le_bytes/data()/k(), since the type itself derives none of them).Benchmark — the corrected four-rung methodology
The initial brief's
P0-under-prepare_one-outside-the-loop and B2's "convert A once per invocation" lifetime were not the same measurement; per the coordinator's correction this PR reports twoPcases at matched lifetimes against their respective baselines, both through the realMatMatMulKer::runpath (linalg/benches/amx_bf16_gap_decomposition.rs):prepare_one'd outside the timed loop (matches B1's lifetime: pure tract packed-execution tax over the raw AMX ceiling).prepare_one'd once outside the loop and reused (persistent weight); A re-prepare_one'd fresh once per whole matrix inside each timed iteration (matches B2's runtime-activation lifetime) — the realistic inference-shaped path.RUSTFLAGS="-C target-cpu=native",CARGO_PROFILE_BENCH_DEBUG=0, this Sapphire Rapids Xeon,cargo bench -p tract-linalg --bench amx_bf16_gap_decomposition -- --sample-size 20(median of 20 samples):(B0/B3, unchanged from #5, still show the original 22-114 ms b3 kernel and the 2.7/22.4 ms asm baseline for cross-reference.)
Ruling: FAIL against the 1.0-1.2 acceptance band — a real, honestly-reported tract-abstraction tax
P0/B1 and P1/B2 both land at ~1.5-1.8×, not the 1.0-1.2× that would mean "dense BF16 execution is essentially solved." The packing layer itself (
prepare_oneconverting/VNNI-packing once,AddMatMuldoing zero conversion) does what it was built to do — the correctness suite (17 auto-generatedmmm_packed_packed_testsfor packing 1, plus a dedicated tolerance test) all pass, and P0/P1 sit at roughly a third of B3's 512³ time and a tenth of its 1024³ time. But there is a real, unexplained ~50-80% tax between the raw hand-rolled tiling loop (B1/B2) and going throughMatMatMulKer::run's generic executor (P0/P1) even with every operand pre-packed. Candidate sources, not yet profiled:panel_bytes()is a vtable call (dyn MMMInputValue) invoked per output tile inside the generic panel walk, versus B1/B2 indexing flat pre-built Rust arrays directly;MatMatMulKer's fused-op interpreter (FusedKerSpeclinked-list walk, onematchper op per tile) versus B1/B2's tightforloop overbf16_tile_gemm_16x16_packedcalls; and generalmmm.run()m/n blocking/stride bookkeeping. This PR does not chase that down further, per scope.Follow-up work named, not attempted here
f32_to_bf16_batch_rnethroughout.panel_bytes(),FusedKerSpecinterpreter overhead, ormmm.run()blocking overhead.🍍
Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_012wrzeZAdwGYTCKoxamwQht
Generated by Claude Code