Repository navigation
Conversation
The main 8x4 tile loaded and stored C with two stride-16 passes per segment (real and imaginary parts). Use a 2-field segment load/store instead, as is already done for the A panel, and update and store each C segment before loading the next so the 16 accumulators and the C tuple fit in the register file without spills. The arithmetic is unchanged and results are bit-identical. On a SpacemiT K3, single-thread zgemm gains 14-18% for n >= 512 on the X100 core and about 2.5% on the A100 core.
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.
Description
In
kernel/riscv64/zgemm_kernel_8x4_zvl256b.c, the main 8x4 tile updates C (C += alpha * ACC) with stride-16-byte accesses: per tile, 16vlse64(one for the real parts and one for the imaginary parts of each of the 8 C segments) and 16vsse64. Each cache line of C is therefore walked twice on load and twice on store.C is stored interleaved (re, im). A 2-field segment access (
vlseg2e64/vsseg2e64) movesgvlcontiguous complex elements in one instruction and de-interleaves them into the same real and imaginary vectors. #6051 already does this for the A panel in this file. This PR does the same for C.Why it matters: on a single-thread 512³ ZGEMM on the X100, the C update block takes 24.2% of the samples in
zgemm_kernel_n. The K loop has no spills and is not touched. WithZGEMM_DEFAULT_Q=64, the C update runs once every 64 K iterations, so at K=512 the whole of C is streamed 8 times.Change
For each of the 8 C segments of the main 8x4 tile:
Each segment is loaded, updated and stored before the next one is loaded. My first version loaded all 8 C tuples up front, as the old code did. With the 16 accumulators also live, that ran out of vector registers: GCC emitted 5 spill/reload pairs (
vs1r/vl1re64), andzgemm_kernel_ngrew from 1470 to 1933 instructions. Interleaving the updates keeps one C tuple live at a time.Only the main 8x4 path changes. The M/N edge tiles keep their strided accesses and are left for a follow-up.
Why the results are bit-identical
ciandci+gvlwithgvl = 4, and the columns areldc >= Mapart. Moving each store before the next segment's load therefore cannot change any value that is read.vlseg2e64field 0 / field 1 give exactly the elements that the oldvlse64at&C[2ci]/&C[2ci+1]with stride 16 gave, andvsseg2e64writes them back to the same addresses.vfmaccbyalphar, thenvfnmsac/vfmaccbyalphai), on the same operands and in the same order. No reassociation is involved.Validation
Differential test. I wrote a standalone program, linked once against the baseline
libopenblas.aand once against the patched one, and compared their raw output bytes withcmp. It covers 33040 cases:cblas_zgemm: both orders, all 16 (trans, conj) combinations for A and B, M, N ∈ {1,3,4,7,8,9,12,15,16,17,33,65}, K ∈ {1,5,8,63,64,65,129}, paddedlda/ldb/ldc, three alpha and three beta values including 0 and pure imaginary;cblas_ztrsm: all side/uplo/trans;cblas_zherk: both uplo, both trans.Every A/B/C buffer is
mmaped so that it ends exactly at aPROT_NONEguard page, so any read or write past the end faults. The outputs are bit-identical on both X100 and A100 (178 MB, sha256251ed73a50fcd0a8…), and nothing faulted.Tests (CMake build,
ctest, on each core type):openblas_utest,openblas_utest_ext)On the A100 the unpatched baseline already fails
matrix_packing:ssymm,matrix_packing:dsymm,kernel_regress:skx_avxanddgemmt:upper_M_257_K_64_a_notrans_b_notrans. The patched build fails the same four, with identical messages. All four are real precision and none goes through this kernel. LAPACK tests were not run (the build has no Fortran).Disassembly of
zgemm_kernel_n(GCC 15.2.0,-march=rv64imafdcv_zvfh_zfh_zvl256b):The extra
vmv1rcome from GCC copying the tuple into and out of a second register pair around the FMAs: 4 per segment, once per tile. GCC 15.2 emits the same copies whether the tuple is rebuilt withvcreate, updated withvset, or updated with nestedvset(vget(...)), so I left the source in its most readable form. Their cost is included in the numbers below.Performance
SpacemiT K3: X100 core (VLEN 256, 2.2 GHz) and A100 core (VLEN 1024, 1.8 GHz). Single thread, pinned,
TARGET=RISCV64_ZVL256B.Size sweep,
benchmark/gemm.c(zgemm NN, M=N=K), median of 5 paired rounds in alternating order, MFlops:The X100 gain is small at n=256 and 14–18% from n=512 up. I have not profiled why it is size-dependent. The A100 gains are small but consistent. Every cell's run-to-run spread is within 1%, except the A100 n=256 baseline (2.7%).
Other kernels that use the same tile path (median of paired rounds, bootstrap 95% CI):
The 8-thread zgemm results match the single-thread ones (X100 +14.9%, A100 +2.4%).