Skip to content

Add an ndarray-blas_gemm-backed AVX-512 f32 GEMM kernel candidate - #4

Merged
AdaWorldAPI merged 6 commits into
mainfrom
claude/ndarray-gemm-pilot-x9k2
Sep 4, 2026
Merged

Add an ndarray-blas_gemm-backed AVX-512 f32 GEMM kernel candidate#4
AdaWorldAPI merged 6 commits into
mainfrom
claude/ndarray-gemm-pilot-x9k2

Conversation

@AdaWorldAPI

Copy link
Copy Markdown
Owner

What

Additive-only pilot: a second MatMatMulKer GEMM candidate, ndarray_avx512_mmm_f32_16x8, alongside the existing hand-tuned AVX-512 asm kernel avx512_mmm_f32_16x8 (same 16x8 tile geometry, directly comparable). Its AddMatMul fused-op step calls ndarray::simd::BlasLevel3::blas_gemm from the AdaWorldAPI ndarray fork instead of a hand-written inner-product loop; every other fused op (bias, min/max, per-row/per-col, store) is the same scalar Rust the crate's own generic reference kernel (crate::generic::mmm::kernel) uses.

Import goes through ndarray::simd::BlasLevel3 — the ndarray fork's canonical consumer-facing re-export — never ndarray::hpc::blas_level3 directly, per that repo's own CLAUDE.md ("all SIMD from ndarray::simd"). This required a small companion change in the ndarray fork itself (src/simd.rs did not yet re-export blas_level3::{BlasLevel3, Side}), made locally in that checkout but not yet pushed upstream — so this PR currently only builds against a local path-patched ndarray, not the git-pinned fork this repo's [patch.crates-io] points at. The fork-side diff is one pub use line, in the same style as the existing hpc::amx_matmul / hpc::bf16_tile_gemm re-export blocks in that file.

Shape/dtype piloted

f32 GEMM, 16x8 tile (matching avx512_mmm_f32_16x8's mr/nr), on a full matrix multiply (not a single tile call) — m=n=k∈{512,1024} — via a criterion bench (linalg/benches/ndarray_gemm.rs) that packs real operands with PackedFormat::prepare_one and calls kernel.run(m, n, ops), so the kernel's own panel-walking machinery loops the 16x8 tile many times, same as production dispatch would.

Real benchmark numbers (this VM, RUSTFLAGS="-C target-cpu=native", CARGO_PROFILE_*_DEBUG=0, release/bench profile, criterion, sample-size 20, Sapphire-Rapids-class Xeon confirmed avx512f/bw/cd/dq/vl/vnni/bf16/fp16)

shape asm avx512_mmm_f32_16x8 ndarray-backed candidate
512×512×512 2.755 ms median (≈97.4 Gelem/s) 14.20 ms median (≈18.9 Gelem/s)
1024×1024×1024 22.90 ms median (≈93.8 Gelem/s) 143.8 ms median (≈14.9 Gelem/s)

What this shows, plainly: the ndarray-backed candidate is ~5-10x slower than the existing hand-tuned asm kernel at both shapes measured. The dominant cost is architectural, not the underlying GEMM math: each 16x8xk tile call allocates and transposes a fresh (16, k) buffer (the packed-A panel is k-major/MR-contiguous, so it must be copied into a contiguous (MR, k) layout before it can reach blas_gemm as a contiguous slice) and makes one blas_gemm call per tile, none of which the tight fused-op asm loop pays. This is not a case for swapping the default kernel — the numbers say the opposite, clearly.

Additive-only, confirmed

  • No existing .S.j2 asm kernel, tier, or preference table entry is touched.
  • The new kernel is registered purely as an additional candidate through the existing MMMRustKernel! macro (same registration path the crate's own generic Rust kernels use) — nothing about kernel selection changes.
  • A new test, x86_64::ndarray_gemm::dispatch_stays_default::adding_the_ndarray_candidate_does_not_change_default_pick, asserts the new kernel is suitable wherever avx512f is native but that MmmDispatch::pick still prefers the asm kernel — i.e. this PR provably changes nothing about default runtime dispatch.
  • Wired into the crate's existing MatMatMulKer correctness-test macros (test_mmm_kernel!mmm_packed_packed_tests! / mmm_frame_tests! / mmm_kernel_fuse_tests! / mmm_store_test!, auto-generated by the same MMMKernel! macro every other kernel uses) — no new test harness invented. 59 kernel-specific tests pass; the full tract-linalg lib suite (4439 tests) passes unchanged.
  • cargo fmt --all -- --check clean. cargo clippy -p tract-linalg --all-targets -- -D warnings: only 2 pre-existing errors remain (generic/reduce.rs::chunks_exact_mut, x86_64/mmm.rs::avx2_mmm_i32_8x8 needless-borrow), confirmed present identically on main before this change (clippy/toolchain drift, unrelated to this PR) — nothing new introduced by this diff.

AMX

Not attempted — the f32/AVX-512 candidate above is the full deliverable for this pilot; there was no remaining budget for an AMX bf16 stretch candidate in this pass.

🍍

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_012wrzeZAdwGYTCKoxamwQht


Generated by Claude Code

…yfill

Adds a [patch.crates-io] entry pointing ndarray at the AdaWorldAPI fork
(git, master branch, so it resolves in any checkout including CI). The
fork is API-compatible (same 0.17.2) but carries an additional simd/hpc
module (crate::simd::F32x16, AVX-512/AVX2/NEON/scalar tier dispatch)
that tract's own linalg kernels can be wired to later. This patch alone
changes no behavior; tract's element-wise/reduce kernels still use their
existing intrinsics path until a follow-up adds a parallel,
non-overlapping kernel registration that calls into it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wrzeZAdwGYTCKoxamwQht
CI was failing across the whole 1.91 matrix because the ndarray fork
patched in by the previous commit requires rustc 1.97 (its
rust-toolchain.toml is permanently pinned there for AdaWorldAPI-stack
alignment). CI derives its tested toolchain straight from
Cargo.toml's rust-version, so bumping that one field re-targets every
job at once; updated the README rustc badge to match per the comment
above that field.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wrzeZAdwGYTCKoxamwQht
Both api/rs/deny.toml and cli/deny.toml gate git dependencies with an
allow-git list that only permitted rustformers/llm; the ndarray patch
resolves ndarray/fractal/p64 from AdaWorldAPI/ndarray, which the
cargo-deny CI job would otherwise reject. Also trimmed the
patch.crates-io comment to the current constraint (why the git pin
exists) instead of narrating a follow-up change that hasn't happened.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wrzeZAdwGYTCKoxamwQht
…nal 16x8 candidate

avx512_mmm_f32_16x8 was the only f32 GEMM tile with no ndarray-backed sibling,
so this adds ndarray_avx512_mmm_f32_16x8: same tile geometry, same fused-op
interpreter as the generic reference kernel, but the AddMatMul accumulation
calls ndarray::simd::BlasLevel3::blas_gemm (the AdaWorldAPI fork's canonical
consumer-facing re-export, not hpc::blas_level3 directly) instead of a
hand-written inner-product loop. Registered purely additively through the
existing MMMRustKernel! machinery; no existing kernel, tier, or preference is
touched, and a new test pins that default dispatch still picks the asm
kernel. Requires a companion ndarray-fork change (adding the blas_level3
re-export to simd.rs, made in the local checkout) that is not yet pushed
upstream, so this currently only builds against a local path-patched
ndarray, not the git-pinned fork.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wrzeZAdwGYTCKoxamwQht
@cursor

cursor Bot commented Sep 4, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot 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_0d507683-0744-4108-814e-79f7a7056b08)

Copy link
Copy Markdown
Owner Author

CI red on build (x86_64-unknown-linux-gnu) and linux (wasm32-wasi), confirmed — both are the exact dependency gap this PR's description already calls out, not a bug in this diff:

error[E0432]: unresolved import `ndarray::simd::BlasLevel3`

The git-pinned ndarray ([patch.crates-io], branch master) doesn't yet carry the BlasLevel3 re-export this PR's kernel imports through ndarray::simd. The fix is AdaWorldAPI/ndarray#297 (opened, one-line additive pub use) — not yet merged.

Not re-running CI now since it will stay red until that PR merges to master; nothing to fix on this side in the meantime. I'll push nothing further here until ndarray#297 lands, then re-check.


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5e8250fe4d

ℹ️ 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".

Comment thread linalg/src/x86_64/ndarray_gemm.rs
Comment thread linalg/src/x86_64/ndarray_gemm.rs Outdated
Comment thread linalg/src/x86_64/mod.rs
Comment thread linalg/src/x86_64/ndarray_gemm.rs

Copy link
Copy Markdown
Owner Author

Pushed a merge + fix (c30c052):

  • Merge conflict: this branch was cut from a stale main (before PR Patch ndarray to AdaWorldAPI fork for SIMD polyfill access #2's MSRV bump / git-based ndarray patch and PR Pilot: ndarray-simd leaky_relu kernel as additional x86_64 candidate #3's leaky_relu pilot merged). Merged current main in, resolved the linalg/Cargo.toml dependency-comment conflict, regenerated Cargo.lock via cargo update -p ndarray.
  • The original CI-red blocker (ndarray::simd::BlasLevel3 missing) is resolved — ndarray#297 merged to master.
  • New bug found and fixed while validating: linalg/src/lib.rs compiles the x86_64 module tree under feature = "foreign-inventory" on any host arch (for cross-compile kernel-name metadata), but this PR's ndarray dependency is x86_64-only — the unconditional use ndarray::* would have broken aarch64-apple-darwin CI once the BlasLevel3 blocker cleared. Gated the ndarray-backed implementation and its test module to target_arch = "x86_64", with a stub for other arches (never reached at runtime — MMMRustKernel!(x86_64; ...) marks the real kernel unbuilt there).

Verified locally: cargo check/test -p tract-linalg clean against the merged ndarray@master, cargo fmt --all -- --check clean, cargo check --features foreign-inventory clean (the condition that broke aarch64), cargo clippy -p tract-linalg --all-targets -- -D warnings shows only the same 2 pre-existing unrelated failures already confirmed present on main.


Generated by Claude Code

Same defensive fix as pilot v2 (PR #5): the symbolic-N fallback in
core::ops::einsum::kernel_selection::strategize picks the largest-nr
kernel per packing group, bypassing preferred/boost entirely. This
kernel's nr=8 currently ties rather than exceeds the existing max
(avx512_mmm_f32_16x12's nr=12), but relying on that ordering to hold
is fragile and inconsistent with the "purely additive, no behavior
change" guarantee this pilot claims. Register it via MMMRustKernel!'s
lower-level form, which skips the inventory::submit! that makes a
kernel discoverable by MmmDispatch::native() -- the kernel stays
directly constructible for this pilot's own bench/tests, but is never
selected automatically. Rewrote dispatch_stays_default to assert
non-discoverability for both a concrete and a symbolic N.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wrzeZAdwGYTCKoxamwQht
@AdaWorldAPI
AdaWorldAPI merged commit 7fe3dc8 into main Sep 4, 2026
51 of 53 checks passed
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.

2 participants