Skip to content

arm64: SME2 bf16 sbgemm kernel for VORTEXM4 (Apple M4) - #6142

Draft
0tomhoyt wants to merge 3 commits into
OpenMathLib:developfrom
0tomhoyt:sme2-sbgemm
Draft

0tomhoyt wants to merge 3 commits into
OpenMathLib:developfrom
0tomhoyt:sme2-sbgemm

Conversation

@0tomhoyt

@0tomhoyt 0tomhoyt commented Oct 20, 2026 •

Copy link
Copy Markdown

Adds an SME2 sbgemm (bf16 × bf16 → fp32) kernel in the style of the fp32/fp64 SME2 kernels of #6074, plus the packed-layout fixes it needed, and one crash fix in the shared SME2 detection.

What it does

BFMOPA ZA32 with .H operands accumulates 2-wide dot products, so one instruction covers two k steps. The kernel (kernel/arm64/sbgemm_kernel_sme2.c) zips rows l and l+1 of the standard packed [k][m] / [k][n] panels into the lane layout 2*i + (k&1) that the outer product needs; a missing second row of an odd k is loaded under a false predicate, which zero-fills. C accumulates in four ZA32 tiles of 16×16 fp32 through the load/store helpers of sme2_gemm_tile.h (SBGEMM_UNROLL_M 64, SBGEMM_UNROLL_N 16), the bfloat16 counterpart of the fp32 micro-kernel shape.

New bfloat16 packing copies (sbgemm_{n,t}copy_{64,16}_sme2.c) emit [k][w] panels for both operand layouts — 64-row chunks on the M side, power-of-two groups on the N side. These are needed because the generic copies emit column-major groups on the T side, which the kernel does not expect. Without SME2 at run time (or in the compiler), plain loops run on the same packed layout.

Fix included: the SME2 run-time check could not return false

s2_usable() computed sme2 && svcntsw() == 16. clang if-converts the conjunction and speculates svcntsw() (RDSVL — undefined without FEAT_SME) above the sysctl test, so the guard itself trapped instead of returning false. I ran into this by testing a dedicated TARGET=VORTEXM4 build on an M3 Pro (unsupported, but it showed the guard never gets a chance to work); with DYNAMIC_ARCH the VORTEXM4 code is only entered on M4, so this is defensive. The vector length is now read through a noinline helper with inline asm, which clang will not speculate.

Status

  • Static make TARGET=VORTEXM4 (Apple clang 21), make DYNAMIC_ARCH=1, and CMake all build cleanly; GCC keeps the generic kernels as before (the KERNEL entries sit inside the existing clang guard).
  • Correctness: 39,116 randomized cblas_sbgemm cases (all four transpose combinations, m/n/k from 1 to 257 plus 1000–1200 sized cases, α ∈ {1, −1, 0.5, 3}, β ∈ {0, 1, 0.25}, odd strides) against a double-precision reference: 0 failures on this branch, same as develop.
  • Not yet run on Apple M4 hardware: this machine is an M3 Pro, so the BFMOPA path itself is compile-verified only (the disassembly shows the intended bfmopa × 4 per k-pair on ZA0–ZA3 with predicated zips). The exercised path is the fallback loops plus the full driver/packing pipeline, which shares the packed-layout contract with the SME2 path. Marking as draft until someone can run the BLAS tests and a benchmark on an M4 — happy to iterate on the numbers.

Add an SME2 sbgemm (bf16 x bf16 -> fp32) kernel in the style of
gemm_kernel_sme2.c, for a 512-bit streaming vector length:

- BFMOPA ZA32 with .H operands accumulates 2-wide dot products, so the
  kernel zips rows l and l+1 of the packed [k][m] and [k][n] panels
  into the lane layout 2*i + (k & 1); a missing second row of an odd k
  is loaded under a false predicate, which zero-fills. C accumulates
  in four ZA32 tiles of 16x16 fp32 via the helpers of
  sme2_gemm_tile.h (SBGEMM_UNROLL_M 64, SBGEMM_UNROLL_N 16).
- Custom bfloat16 A and B copies (sbgemm_{n,t}copy_{64,16}_sme2.c)
  emit [k][w] panels for both operand layouts, in 64-row chunks on
  the M side and power-of-two groups on the N side, like the A copies
  of the fp32 SME2 kernels (the generic copies emit column-major
  groups on the T side).
- Without SME2 at run time, or in the compiler, plain loops run on
  the same packed layout.

Also fix a crash of the existing SME2 kernels on cores without
FEAT_SME2 in s2_usable(): clang if-converts the conjunction and
speculates svcntsw() (RDSVL, undefined without FEAT_SME) above the
sysctl test. Reading the vector length through a noinline helper with
inline asm prevents that.
@martin-frbg

Copy link
Copy Markdown
Collaborator

Thanks - it doesn't make sense to undefine the SBGEMM_DEFAULT_(P,Q,R) of unrelated cpus though, this will only break builds that were using either dedicated sbgemm kernels or the generic C fallback

@martin-frbg

Copy link
Copy Markdown
Collaborator

Regarding your comment, a dedicated VORTEXM4 build is not expected/intended to work on M1 to M3 hardware - cpu autodetection at build or (in the case of a DYNAMIC_ARCH-built library) runtime will select the non-SME "VORTEX" target for these.

Only the UNROLL values are specific to the SME2 kernel (the packed
panel layout); P, Q and R stay at the shared defaults, which are
compatible with the 64-row M chunks.
@0tomhoyt

Copy link
Copy Markdown
Author

Fair point — dropped in 0bc31e8. The VORTEXM4 block now only overrides the UNROLL values (these define the packed panel layout, same #undef + redefine pattern as the NEOVERSEN2 block), and P/Q/R keep the shared defaults, which are compatible with the 64-row M chunks (re-ran the sbgemm correctness sweep on top: unchanged).

@0tomhoyt

Copy link
Copy Markdown
Author

Understood on the intended usage — I'd misframed that section (this machine is an M3 Pro, so I was testing the dedicated build directly on unsupported hardware). Two clarifications on why the s2_usable() change is still in the PR, feel free to disagree:

  • The check as written is meant to gate at run time, but clang if-converts sme2 && svcntsw() == 16 and hoists RDSVL above the sysctl test, so the check itself trapped before it could return false — i.e. the guard existed but could never do its job. The noinline helper just makes the existing guard behave as written.
  • It also means a DYNAMIC_ARCH library that somehow lands on the VORTEXM4 table entry (mis-detection, or a future core reporting an M4-ish cpuid without FEAT_SME2) fails closed instead of SIGILLing.

By the same token I'm fine leaving the SME1 smstart path as you prefer — it only bites the same unsupported dedicated-build case, so I've nothing pending on that.

@0tomhoyt
0tomhoyt marked this pull request as ready for review October 21, 2026 02:46
@0tomhoyt
0tomhoyt marked this pull request as draft October 21, 2026 02:48
The edit that added the SBGEMM P/Q/R overrides to the VORTEXM4 block
matched on the SBGEMM UNROLL undef lines, which occur in several
other cpu blocks as well, so the undefs landed in four of them and
broke their setparam compilation under DYNAMIC_ARCH (SBGEMM_DEFAULT_P
undeclared, e.g. for NEOVERSEV1 with GCC). This is what the review
comment about unrelated cpus was about; the overrides themselves were
already dropped in 0bc31e8.
0tomhoyt added a commit to 0tomhoyt/OpenBLAS that referenced this pull request Oct 21, 2026
sbgemm on the Apple targets so far ran the generic C fallback; with
FEAT_BF16 (present at least since M2, probed at run time so older
cores keep the fallback) the new kernel accumulates C in sixteen quad
registers of eight columns by two row groups. One BFDOT does a
two-wide dot per lane, so rows l and l+1 of the packed panels are
zipped into the lane layout (i, k & 1); the B pair of a column is
duplicated across the lanes and the A pairs cover four rows each. An
odd k is a zero vector, blocks narrower than eight run in scalar code.

With SBGEMM_UNROLL_M = SBGEMM_UNROLL_N the driver routes the A side
through the O copies as well, so both panels come in the power-of-two
grouping of OpenMathLib#6142's copies, which this renames to role-based names
(oncopy/otcopy for both sides, ncopy/tcopy kept for the M-side-only
use of the M4 SME2 target).

On an M3 Pro: 39116-case randomized sbgemm sweep against a
double-precision reference passes unchanged, and against the generic
fallback of develop the packed path is 34x at 1024^3 (57 vs 1.7
GFLOPS) and 63x at 2048x2048x1024 (114 vs 1.8 GFLOPS), about a
quarter of the fp32 NEON sgemm of the same build.
@0tomhoyt

Copy link
Copy Markdown
Author

Now I see what you were pointing at, and I owe you a correction: my reply above fixed only the undefs inside the VORTEXM4 block. The edit that had added them matched on the SBGEMM UNROLL undef lines, which occur verbatim in several other cpu blocks, so the same three undefs had landed in four unrelated blocks as well - and their setparam then failed under DYNAMIC_ARCH (this is also what broke CI on #6145: SBGEMM_DEFAULT_P undeclared for NEOVERSEV1 with GCC). All stray copies are gone in c151d85, the param.h diff against develop is now down to the two SBGEMM_UNROLL overrides plus the comment.

This branch has not been deployed

No deployments
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