Repository navigation
perf(native-cpu): AVX2/FMA register-tiled lane for the dense FP32 SGEMM on x86-64 - #1445
Open
michalharakal wants to merge 2 commits into
Open
michalharakal wants to merge 2 commits into
michalharakal wants to merge 2 commits into
Conversation
…MM on x86-64 skainet_fp32_matmul had register-tiled bodies for wasm SIMD128 only; the x86-64 library is built for baseline x86-64, so the kernel behind every FP32 A·B the JVM hands to the native lane ran a scalar/SSE2 outer-product loop without FMA. Hosts with AVX2+FMA now select, through the cached cpuid probe from the x·Wᵀ lane, a per-function avx2+fma body: a 6×16 tile of C in twelve eight-lane FMA accumulators over a 256-deep k-block, each sixteen-column panel of B packed once (a power-of-two n otherwise maps every panel row to one L1 set), 2×16 / 1×16 tiles for the rows left over and a lane-masked stripe for the columns left over. Every output takes one fused step per p in ascending p whichever tile holds it, so sliced, threaded and whole calls stay bit-identical to each other. Measured single call (i7-9750H, develop → this): 63×1024×1024 21 → 281 GFLOP/s, 1024³ 48 → 311, 512³ 59 → 358, 192×192×64 one thread 18 → 99. Also: NativeFp32MatmulKernel copied the whole staged C reach back into `out`, zeroing everything before `outOffset` and the stride padding of a sub-block caller's buffer; only the m×n block is copied back now. New NativeFp32MatmulX86LaneTest: tile edges vs a double reference, strided/offset operands with untouched padding, block independence bit-for-bit, opt-in single-call bench on a direct downcall.
…l library The Android library builds fp32_matmul.c for the x86_64 emulator ABI as well, and that file now calls skainet_cpu_has_avx2_fma(); the probe source was missing from its CMake list, so the x86_64 link failed. Verified with the pinned NDK for both ABIs.
This branch has not been deployed
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.
Summary
skainet_fp32_matmul— the plain row-major SGEMM behind every FP32A · Bthe JVM hands to the native lane (a weight stored[in, out], attention scores and values above the small-shape threshold, training) — gets the x86-64 AVX2/FMA lane that #1442 gavex · Wᵀ. Until now the x86-64 library, built for baseline x86-64, ran this kernel as a scalar/SSE2 outer-product loop without FMA that loaded and stored a row of C for every row of A; only the wasm SIMD128 sidecar had a register tile.Follow-on F1 of the plan behind #1442. C only, no Kotlin API change.
What changes
fp32_matmul.c: newskainet_fp32_block_avx2, compiled per function withtarget("avx2,fma")(empty on MSVC, which emits VEX intrinsics without/arch) and selected once per call by the cachedcpuidprobe fromskainet_cpu_features.h. The SIMD128 micro-kernel grown to the register file:__m256accumulators — two B loads and six broadcasts feed twelve FMAs perp, so the loop sits on the two FMA ports (hot loop verified spill-free in the disassembly);nputs every row of the panel in the same L1 set (4 KiB stride, 8 ways): 63 × 1024 × 1024 ran at half the speed of 63 × 1021 × 1023 before packing. Blocks under two row tiles read B in place;pin ascendingpwhichever tile holds it, so sliced, threaded and whole calls stay bit-identical to each other (the row pool and any sub-matrix call rely on that). Against the previous x86 numbers an output moves within rounding — mul-then-add became one fused step — the way the NEON and wasm lanes already differ. Machines without AVX2, and every other architecture, run the previous code.skainet-backend-jni-cpuCMake addsskainet_cpu_features.cto its kernel source list: the Android library compilesfp32_matmul.cfor thex86_64emulator ABI too, which now references the probe (first CI run failed to link there; verified with the pinned NDK 28.2 for both ABIs).NativeFp32MatmulKernelKDoc describes the lanes instead of "auto-vectorizes into FMA (vfmadd231ps on x86_64)", which was never true for the baseline build.NativeFp32MatmulKernelcopy-back fix (found by the new strided test): the heap face stages C off-heap and copied the whole staged reach back intoout, so a sub-block output (offset, or stride wider thann) had everything before the offset and the padding between rows zeroed. Only them × nblock is copied back now. Pre-existing; its own### Fixedentry.### Changed+### Fixedentries under Unreleased.Tests
New
NativeFp32MatmulX86LaneTest(runs the portable lane on hosts without AVX2):everyTileEdgeMatchesTheDoubleReference: m ∈ {1, 2, 3, 5, 6, 7, 13, 63} × n ∈ {1, 7, 8, 15, 16, 17, 33, 1021} × k ∈ {1, 255, 257, 1023} against a double-accumulated reference (maxAbs · 1e-5 + 1e-6).stridedOperandsAndAnOffsetOutputLeaveThePaddingAlone: non-contiguous A/B/C strides with offsets; padding stays untouched.anOutputDoesNotDependOnTheBlockThatComputesIt: full call vs rows split at 5, columns split at 3, row-at-a-time, including a shape the pool splits across threads and a pack-vs-in-place pair —assertContentEquals.singleCallBenchbehind-Dskainet.runBench=true, through a direct downcall with all three operands off-heap (the SPI face copies A, B, C per call and would hide the kernel).Suites:
skainet-backend-native-cpu:jvmTest,skainet-backend-cpu:jvmTest+apiCheck— see the checklist below. A standalone C harness with the module's ownskainet_row_threads.cswept 1763 shape/stride/offset/split cases (0 failures) before the Gradle round.Measured
i7-9750H (6c/12t), GCC 13.3, single call, develop → this PR. The C row pool threads over the longer side when it is ≥ 512, so the first eight rows run on four threads and 192 × 192 × 64 on one.
The gemv row is DRAM-bound (64 MB of B per call) and is included to show the lane does not regress it. The same shapes through the JVM downcall (
NativeFp32MatmulX86LaneTest.singleCallBench, JDK 21, operands off-heap):Checklist
:skainet-backends:skainet-backend-native-cpu:jvmTestgreen — 181 tests, 0 failures (incl. the new lane test):skainet-backends:skainet-backend-cpu:jvmTest+apiCheckgreen — 492 tests, 0 failures