Repository navigation
Description
Summary
Since 0.3.34, SGEMM results under a pinned OPENBLAS_CORETYPE (SkylakeX, Haswell) differ between a Zen 4 host and an Intel host for some K, while they are byte-identical with the same pin on every release from 0.3.23 through 0.3.33 (bisected with the conda-forge builds).
The cause is the block added by #5868 at the end of init_parameter() in kernel/setparam-ref.c (v0.3.34 lines 2087–2115): when the host CPUID reports AuthenticAMD, l3_kb % 32768 == 0, l2_kb == 1024 and CPUID.7:EBX bit 16 (AVX512F), it sets sgemm_p=384, sgemm_q=512 (and the d/c/z equivalents). driver/others/dynamic.c reads OPENBLAS_CORETYPE (line 1145), applies force_coretype() (1148) and then calls gotoblas->init() (1180), so the override rewrites the blocking of the forced table on Zen 4/5 hosts only. GEMM_Q is the K-chunk of the reduction, so partial sums are accumulated in a different order than on any other host running the same pinned kernel, and the bits differ. openblas_get_corename() still reports the pinned core, so nothing indicates that the pin no longer means the same thing everywhere.
Reproduction
conda-forge libopenblas 0.3.34 (openmp build), NumPy A @ B in float32, M = N = 64, K varied, OPENBLAS_NUM_THREADS=1. Hosts: AMD EPYC 9V44 (Genoa, Zen 4: L2 1 MiB, L3 32 MiB, AVX-512) vs Intel Xeon Platinum 8573C (Emerald Rapids). Digests of the result matrix compared across the two hosts:
| pin | identical K | different K |
|---|---|---|
OPENBLAS_CORETYPE=SkylakeX (table Q = 448) |
256, 384, 448, 513, 640, 768, 895, 896 | 449, 480, 512, 1024, 1536 |
OPENBLAS_CORETYPE=Haswell (table Q = 320) |
256, 320, 513, 600, 639, 640 | 321, 400, 512, 768, 1024 |
This is exactly the pattern of the K-chunking rule in driver/level3/level3.c with Q = 512 on the Zen 4 host against the table's Q elsewhere: identical wherever both Q values produce the same chunk sequence (including K = 2Q, where the halving rule coincides), different elsewhere. Predicted from the source before measuring; every K landed as predicted.
Controls, all consistent with the mechanism:
OPENBLAS_L2_SIZE=2048on the Zen 4 host (the block requiresl2_kb == 1024) restores byte-identity with the Intel host at every K under both pins, with no rebuild.OPENBLAS_L2_SIZE=1024changes nothing.OPENBLAS_L2_SIZEis inert on the Intel host.- A Zen 3 host (EPYC 7V13 Milan: L2 512 KiB, no AVX-512) on 0.3.34 is byte-identical to Intel with no workaround, and remains so with
OPENBLAS_L2_SIZE=1024forced (the AVX512F term fails there). - Every release 0.3.23 … 0.3.33 with the same pins: byte-identical at all K on the same hosts.
Why it matters
OPENBLAS_CORETYPE is documented as a kernel-selection override, but it is the route that has been suggested here for identical results across Intel and AMD (#4510), and up to 0.3.33 it delivered them for GEMM. #6013 found the same block as a crash on Windows; the fix on develop (a59a5c7) only wraps it in #ifndef NO_AVX512, so normal AVX-512 builds of 0.3.35 will keep the host-dependent override under a forced coretype.
Suggestion
Any of: skip the Zen 4 P/Q override when a coretype was forced via OPENBLAS_CORETYPE (so a pin means the same blocking on every host); apply it inside the Zen table's init rather than after force_coretype; or document that OPENBLAS_L2_SIZE=2048 is needed for cross-host identity on Zen 4/5 under a pin. Happy to test a patch on the same hosts.
Activity
iamgahr commentedon Sep 9, 2026
I found this by bisecting across seven machines on two vendors. Would a pre-release check for byte-identity regressions across microarchitectures be useful to you, or do you already have coverage for this? Happy to run master against my fleet and report.
iamgahr commentedon Sep 11, 2026
This isn't a Windows-specific path — it reproduces on every platform.
The .L12_02b loop in dgemm_kernel_4x8_haswell.S writes 96 bytes per k iteration, unbounded, into a fixed-size frame buffer. The overflow threshold differs only by ABI because the frame sizes differ:
SysV: 256812 + 4096 = 28672 B → overflows at K ≥ 299
Windows: 128812 + 512 = 12800 B → overflows at K ≥ 134
The Windows number is a useful cross-check: @bodono measured the first crash between k=144 and k=160, and the and rsp, -0x1000 stack alignment slack accounts for the 133–176 spread. Deriving the threshold from the source alone lands in the same place.
Reachability (from dynamic.c:913–923; Zen 4 = family 0x19 → exfamily == 10):
NO_AVX512 build, Zen 4/5 → ZEN table → crash. This is #6013, fixed by a59a5c7.
AVX-512 build, Zen 4, no env var → support_avx512_bf16() → COOPERLAKE → dgemm_kernel_16x2_skylakex → safe. (Zen 5 is exfamily 11 → NULL → capability fallback → also SkylakeX/Cooperlake.) This is why there's no mass Linux breakage, and why #5868's authors never saw it.
AVX-512 build + OPENBLAS_CORETYPE=Zen or =Haswell on Zen 4/5 → forced table → the block still fires (#ifndef NO_AVX512 is inert in an AVX-512 build) → dgemm_q=512 → the .S kernel → stack-frame corruption. Not fixed, on any OS.
So case 3 is the one still open. It's not a default-configuration bug post-a59a5c76 — it needs a deliberate opt-in env var plus specific AMD hardware, and it isn't remotely triggerable. That bounds the severity. But it's still live.
Concretely: the reported config in this thread — Linux, conda-forge 0.3.34, Genoa, OPENBLAS_CORETYPE=Haswell — is one DGEMM call away from it. It didn't trigger because KERNEL.HASWELL's SGEMM is sgemm_kernel_8x4_haswell_2.c, a C kernel with no such buffer; only DGEMM routes through the .S one. That's luck, not safety.
[-]0.3.34: Zen 4 GEMM P/Q override (#5868) runs after OPENBLAS_CORETYPE is forced, so a pinned coretype no longer gives host-independent SGEMM results[/-][+]0.3.34/develop: Zen 4 P/Q override applies to forced coretypes — #6013's dgemm stack overflow still reachable via OPENBLAS_CORETYPE in AVX-512 builds[/+]martin-frbg commentedon Sep 11, 2026
Right, that code needs to be guarded by support_avx512() in the DYNAMIC_ARCH case, and maybe it should be opt-in in general, if the changed blocking leads to significantly different results
iamgahr commentedon Sep 11, 2026
Agreed on the direction, with one correction:
support_avx512()is a host test (CPUID.7 + XGETBV), and a Zen 4 host passes it — the block already performs the equivalent check (CPUID.7:EBX bit 16) and fires precisely because the host has AVX-512. So wrapping it insupport_avx512()leaves case 3 unchanged: forcedZenorHaswelltable on a Zen 4/5 host, host passes both checks,dgemm_q = 512lands ondgemm_kernel_4x8_haswell.S.The condition that actually matters is "is the table being initialised an AVX-512 one", and that is known at compile time: in a
DYNAMIC_ARCHbuildsetparam-ref.cis compiled once per target, so the target macro is the right guard:Equivalently, move the block into the existing
#if defined(SKYLAKEX) || defined(COOPERLAKE) || defined(SAPPHIRERAPIDS)section ofinit_parameter()(line 1815 in v0.3.34), which is where per-target tuning already lives. Either form also subsumes theNO_AVX512guard (no AVX-512 targets are compiled in that case) and adds no symbols, so static builds are unaffected.I confirmed the kernel half in isolation, with the vendor check out of the picture: v0.3.34, static
TARGET=HASWELL,USE_THREAD=0, x86-64 SysV (so the 28672-byte buffer), only changeDGEMM_DEFAULT_Q256 → 512 in the HASWELL section ofparam.h. DGEMM with kerneln ≥ 24(the.L12_01path, the only one that uses the stack sub-buffer) faults from K = 336; the control is clean at every K. That is consistent with the derived threshold: the buffer is exceeded from K ≈ 299, and the overrun reaches the saved return address around 336 — checksums match the control through K = 328, so the frame is corrupted silently before it becomes fatal. At K = 336rip = 0x0000762800000000— the return address overwritten with matrix data, the same signature as the dump in #6013. At K = 512 it faults inside the copy loop itself (.L12_02b+18,vmovups %ymm2, 0x20(%rsi)).n < 24never faults, because.L8_0has noBUFFER1reference.On "significantly different results": no — measured, it is summation-order noise. Same inputs, SGEMM M = N = 64, table Q vs 512:
That is single-digit ULPs at the output's magnitude — an accuracy non-event. It only matters to someone pinning
OPENBLAS_CORETYPEfor bit-identity across a mixed fleet, which is the narrow case this issue is about. So I would not make the tuning opt-in for accuracy reasons; the target guard is needed for memory safety regardless. If you want a pin to mean the same blocking on every host as well, the smallest change is to skip the block whengetenv("OPENBLAS_CORETYPE")is set — but that is a policy call about whatOPENBLAS_CORETYPEpromises, and I'm fine either way.Happy to put up a PR: the target-macro guard (either placement, your call) plus a test that DGEMM at K = 512 under
TARGET=HASWELLwithDGEMM_DEFAULT_Q = 512completes and matches the reference — and, if you want theOPENBLAS_CORETYPEskip, that as a separate commit so it can be taken or dropped on its own.iamgahr commentedon Sep 11, 2026
Receipts for the reproduction above, so it can be checked rather than taken on trust.
Build. v0.3.34 (
e0166008), x86-64 SysV, static, one change:Control is the same tree unmodified. Archive contains
dgemm_kernel.obuilt fromdgemm_kernel_4x8_haswell.S(KERNEL.HASWELL). Host: macOS 26.6, Apple clang 17, x86-64binaries under Rosetta 2 (AVX2 verified). Same ABI, same
L_BUFFER_SIZE, same kernelmachine code as Linux; the exact fault K will move with stack layout.
driver (one process per K; exit 0 = survived)
Sweep, M = N = 64 (kernel
n= 64,.L12_01path):Path dependence, q=512 build, first faulting K in 256…1024 step 8, varying M
(row-major dispatch makes the kernel's
nthe caller's M):The flip is exactly at the
Ndiv12 = N/24branch:.L12_01copies 12 doubles = 96 B perk into
BUFFER1(threeymmstores at0x0/0x20/0x40);.L8_0has noBUFFER1reference.
28672 / 96 = 298.67, so the overrun begins at K ≥ 299.Silent band, checksum q=256 vs q=512 below the first fault:
So the frame is overrun from ~299 and corrupted silently until the overrun reaches the
saved return address; results are still correct in that band.
Fault sites (lldb, q=512 build):
Not executed here: the composition on Zen 4 hardware — pin
Haswell, block fires,dgemm faults. That follows from the ordering in the report plus the
OPENBLAS_L2_SIZE=2048control there, which shows the block does fire on that host underthe pin. I'll post the direct run when I'm back on that machine.
iamgahr commentedon Sep 11, 2026
Ran this on real AMD hardware. It reproduces, and the gate is confirmed rather than assumed.
Host: AWS
m8a.2xlarge, AMD EPYC 9R45, family 26 (0x1A) — Zen 5 Turin, not Zen 4Genoa. Same gate conditions; flagging the difference explicitly. 8 cores, L2 1024 KB/core,
L3 32768 KB, AVX512F + AVX512_BF16. Stock conda-forge
libopenblas 0.3.34(DYNAMIC_ARCH NO_AFFINITY Cooperlake MAX_THREADS=128), numpy 2.4.6,OPENBLAS_NUM_THREADS=1.The gate (a standalone CPUID probe replicating
setparam-ref.c:2095):Core selected per pin: unpinned →
Cooperlake;Haswell→Haswell;Zen→Zen;SkylakeX→SkylakeX. So the default path is the safe one, as expected.DGEMM K-sweep,
OPENBLAS_CORETYPE=Haswell, M=N=64 (exit code captured directly):First fault at K = 336 — the same K as the isolated x86-64 SysV run I described above,
which had no AMD CPU involved at all. Overrun begins at K ≥ 299 by the buffer arithmetic;
the fault lands at 336 on both.
Controls at K = 512:
OPENBLAS_L2_SIZE=2048makes the gate'sl2_kb == 1024fail, the block does not run, andthe identical call completes. That is the single-variable confirmation that the block is
the cause, not the Haswell kernel per se.
And for the original reproducibility report — same host, same pin, only
OPENBLAS_L2_SIZEdiffering, so no second machine is needed to isolate the override.SGEMM, M=N=64, sha256 of the result matrix:
Differing at exactly K = 449, 480, 512, 1024, 1536 and identical at 256, 384, 448, 513,
640, 768, 895, 896 — the same partition as the table in the original report, and the same
one the
GEMM_Qchunking rule indriver/level3/level3.cpredicts for Q = 448 vs 512.The
Haswellpin (table Q = 320) gives its own predicted partition, differing from 384upward and matching at 256, 513, 640.
So all three cases in the table above are now measured: default is safe, and the forced
Zen/Haswelltables both take the override and both fault.#ifndef NO_AVX512does notguard this build — the config string shows AVX-512 kernels present.
Apply the Zen 4 blocking override only to cores that can take it
ci: keep the forced OpenBLAS kernels on their own blocking on AMD Zen…