Conversation
?GEMMT / ?GEMMTR has been a loop over the columns of the triangle with one ?GEMV per column since it was added in 0.3.22. Besides being much slower than GEMM (issue OpenMathLib#4921), every column above the GEMV multithreading threshold becomes its own thread dispatch, so the routine gains almost nothing from more cores. Add a real level-3 driver. No new microkernels are needed: the blocks that cross the diagonal are handled by the existing SYRK kernel, which already clips a block against the diagonal at register-block granularity. - driver/level3/level3_gemmt.c is derived from level3_syrk.c. The blocking, the diagonal handling and the loop structure are the same; the packed B buffer is filled from a second matrix, so the buffer sharing SYRK can do is not available, and op(A) / op(B) are selected by the NN/NT/.../CC defines that level3.c uses. - driver/level3/gemmt_k.c holds the triangular beta scaling and the choice of kernel, and is built once per uplo x op(A) x op(B) just as syrk_k.c is built per uplo x trans. - For complex, syrk_kernel.c is also built with CONJA / CONJB; the file already supported those defines but nothing instantiated them. interface/gemmt.c dispatches through a [uplo][op(B)][op(A)] table like interface/gemm.c, and above the existing SMP threshold splits the triangle into column ranges of equal area with syrk_thread(), so every thread writes a disjoint set of columns of C. Only the blocks that intersect the requested triangle are packed and computed, and only the triangle is scaled by beta. This also stops complex GEMMT from writing to the caller's B: conjugation used to be done by conjugating B in place and undoing it afterwards, which is unsafe for a shared or read-only B. dgemmt on an 8-core/16-thread i7-11800H, SKYLAKEX kernels, time for one call, with dsyrk on the same data for reference: n=1600 k=128, 1 thread: 18.7 ms -> 5.14 ms (dsyrk 5.12 ms) n=1600 k=128, 4 threads: 6.92 ms -> 1.54 ms (dsyrk 1.53 ms) n=1600 k=128, 16 threads: 11.7 ms -> 1.01 ms (dsyrk 1.01 ms) n=400 k=128, 16 threads: 1.31 ms -> 0.10 ms (dsyrk 0.11 ms) One deliberate deviation from level3_syrk.c: for a row band that lies entirely below the column block, level3_syrk.c bounds its column loop with `jjs < min_j`, which mixes an absolute index with a length and packs nothing when js > 0. The GEMMT driver uses `jjs < js + min_j`. Neither interface passes a row range, so the branch is only reachable by calling the driver directly; level3_syrk.c is left alone here. Add a utest case per precision at a size that is neither a multiple of the register blocking nor small enough to stay single-threaded, which is where the packing and the diagonal clipping have to agree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
jgillis
added a commit
to casadi/casadi
that referenced
this pull request
Sep 25, 2026
OpenBLAS implements ?GEMMT as a loop over ?GEMV, one thread dispatch per column, which made MUMPS (and so Ipopt) up to 2x slower on many-core machines once 0.3.24 exposed dgemmt_. 0.3.24.mod is v0.3.24 plus the blocked level-3 GEMMT driver submitted upstream as OpenMathLib/OpenBLAS#6060 (and the upstream SBGEMMT interface split it depends on). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…make
A bare -DCR expands every CR token in the MinGW system headers to 1 and
breaks the complex ?gemmt_?CR objects ("expected identifier or '(' before
numeric constant"). The GEMM rules and cmake/utils.cmake already use the
self-referencing form for exactly this reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
interface/gemmt.chas implemented?GEMMT/?GEMMTRas a loop over the columns ofthe triangle with one
?GEMVper column ever since the routine was added in 0.3.22.That is correct but, as #4921 notes, far off the performance of
GEMM, and above theGEMV multithreading threshold every single column becomes its own thread dispatch, so
on a many-core machine it gains almost nothing from extra cores and ends up far slower
than computing the whole product with
GEMMand throwing half of it away.This replaces it with a real level-3 driver. No new microkernels are needed: the blocks
that cross the diagonal are handled by the existing
SYRKkernel, which already clips ablock against the diagonal at register-block granularity.
What is added
driver/level3/level3_gemmt.c— the driver, derived fromlevel3_syrk.c. Theblocking, the diagonal handling and the loop structure are the same; the difference is
that the packed B buffer is filled from a second matrix, so the buffer sharing that
SYRK can do (
shared) is not available, andop(A)/op(B)are selected by the sameNN/NT/…/CCdefines thatlevel3.cuses.driver/level3/gemmt_k.c— the per-variant wrapper: the triangularbetascaling andthe choice of triangular kernel. Built once per
uplo×op(A)×op(B), exactly assyrk_k.cis built peruplo×trans.syrk_kernel.cis additionally built withCONJA/CONJB(
?gemmt_kernel_UCN,_UNC,_UCCand theLcounterparts); the file alreadysupported those defines but nothing instantiated them.
interface/gemmt.cnow sets upblas_arg_tand dispatches through a[uplo][op(B)][op(A)]table likeinterface/gemm.c. Above the existing SMP threshold itsplits the triangle into column ranges of equal area with
syrk_thread()— thepartitioner SYRK and SYR2K already use — so every thread runs the driver on a disjoint
set of columns of C.
Only the blocks that intersect the requested triangle are packed and computed, and only
the triangle is scaled by
beta, so no work is wasted on the half of C that is notreferenced.
Side effect: complex GEMMT no longer writes to B
For
transb = 'C'/'R'the old code conjugated the caller's B in place(
IMATCOPY_K_CNC) and conjugated it back at the end. That made a complexGEMMTunsafeagainst a B that is shared between threads or that lives in read-only memory. Conjugation
is now part of the kernel selection, so all input matrices are left untouched — the tests
below check that explicitly.
Numbers
dgemmt, 8-core/16-thread i7-11800H, GCC 13,TARGET=SKYLAKEX, time for one call(best of ≥5), and
dsyrkon the same data for reference:Measurements are interleaved old/new/syrk, each the best of three rounds of a separate
process (each round the best of >= 5 calls), on an otherwise idle machine.
Single-threaded the new driver is 3–4× faster than the loop and matches SYRK. The old
implementation barely benefits from more threads (18.7 → 11.7 ms from 1 to 16 threads at
n=1600), while the new one scales like SYRK (18.7 → 1.01 ms), so the gap grows to ~10–13×
on 16 threads. Under load, or with several GEMMT calls in flight, the old per-column
dispatch degrades much further — that is how it was noticed downstream, where MUMPS calls
GEMMT for every symmetric front update when the BLAS provides it
(casadi/casadi#4417).
Testing
utest,ctestandtestpass (make -C utest: 1539/1539, including four newcases, see below). Checked with 1, 8 and 16 threads.
uplo×op(A)×op(B)(including the CBLAS-onlyConjNoTrans) × Fortran/CBLAS ×column/row-major, with padded leading dimensions and
alpha/beta∈ {0, 1, −1,random, pure imaginary}, over n ∈ {0…200} and k ∈ {0…129} — 39 644 cases, plus 6 360
large cases (n up to 700) at 2, 3, 5, 7 and 16 threads. It also checks that the
opposite triangle, the
ldcpadding, the memory around every buffer and the inputmatrices A and B are left untouched.
DYNAMIC_ARCHbuild andOPENBLAS_CORETYPE=PRESCOTT, NEHALEM, SANDYBRIDGE, HASWELL, SKYLAKEX, ZEN (their register blocking, and
hence the diagonal clipping, differ).
never passes), for both triangles and
beta != 1, and the?gemmtrentry pointsthrough the same harness.
DYNAMIC_ARCH=1,USE_THREAD=0, and CMake.The four new
utestcases (?gemmt.upper_M_257_K_64_a_notrans_b_notrans) use a sizethat is not a multiple of the register blocking and is large enough to be split over
threads, which is the case the existing tests (capped at 100) did not reach; they compare
against
GEMMand assert that the lower triangle is bit-identical to its input.Note on
level3_syrk.cFor a row band that lies entirely below the column block,
level3_syrk.cbounds its columnloop with
for (jjs = js; jjs < min_j; …), mixing an absolute index with a length: whenjs > 0it packs nothing and the kernel then reads an unfilled buffer. The GEMMT driveruses
jjs < js + min_j. As far as I can see nothing in the tree calls the SYRK driver with arow range, so the branch is unreachable today; I left
level3_syrk.calone to keep this PRto GEMMT, but it is a one-line fix if you would like it here.
🤖 Generated with Claude Code