Core: Read the cache topology from sysfs on Linux
queryCacheSizes() reports 0/0/0 on every non-x86 Linux, so GEMM there blocks against the compiled-in default sizes rather than the hardware.
The POSIX branch added in !2282 (merged) calls sysconf(_SC_LEVEL*_CACHE_SIZE), but glibc answers those from CPUID and so only implements them on x86. Confirmed on three aarch64 machines (Jetson Orin, Grace, DGX Spark): all return 0. This adds a sysfs fallback, which Linux populates on every architecture. EIGEN_NO_CPU_CACHE_SYSFS opts out.
Feeding the heuristic a real L2 then exposes a constant fitted to the placeholder: actual_l2 = 1.5 * l2 was calibrated against a 1MB modeled L2, so a core whose real L2 is 256KB gets a quarter of the intended rhs panel. Where sysfs describes the L3, one CPU's slice of it is used as a floor for the rhs-panel budget instead. Both halves of that slice come from the same sysfs entry: sysconf reports the package total (128MB on a 32-core Threadripper whose cores each reach one 16MB slice), so dividing it by a single instance's sharer count would overstate the share eightfold.
The slice lives in CacheSizes alongside l1/l2/l3, so setCpuCacheSizes() clears it and EIGEN_DEBUG_SMALL_PRODUCT_BLOCKS zeroes it — callers force small cache sizes precisely to exercise multi-pass blocking. It feeds only the rhs-panel budget, deliberately not actual_lm, whose blockA allocation tuning is separate.
Measured
| machine | detection before → after | ST N=2048 float |
|---|---|---|
| Grace (Neoverse V2) | 0/0/0 → 64K/1M/114M | 87.8 → 88.3 |
| Jetson Orin (A78AE) | 0/0/0 → 64K/256K/2M | 24.87 → 24.06 |
| DGX Spark (X925) | 0/0/0 → 64K/512K/8M | 155.3 → 158.1 |
| Threadripper 3975WX / 3960X | unchanged (sysconf works) | 80.3 → 81.0 / 82.5 → 83.3 |
| Apple M4 | unchanged (sysctl answers, no sysfs) | — |
Multithreaded blocking is bit-identical wherever detection already worked; only the single-threaded branch uses the slice.
The Jetson loss is the one regression and is specific to a core whose real L2 is far below the 1MB placeholder — Grace, at exactly 1MB, is neutral. Recalibrating the rest of the heuristic for small-L2 parts wants data from more ARM parts and is left to a follow-up.
Tested
product_large, product_small, product_notemporary, tensor_contraction on Grace, DGX Spark, Threadripper PRO 3975WX, and macOS M4. The new assertion fails at the parent commit on aarch64 Linux.
Review round 2
- Every CPU the thread may run on, not CPU 0 (19704125). The reader takes the sysfs root and a CPU; the query merges every CPU the calling thread's affinity mask admits, keeping per level the smallest cache any of them reports. Where the mask is unavailable, every online CPU is a candidate. A cpu-list range walker replaces the bare counter and rejects negative or impossible ids. Cost: 3ms for 32 CPUs on WSL2 (85us for one CPU), once per process.
- sysfs independent of the GNU
sysconfconstants (3c2ab3d0). Only thesysconfcalls are guarded by_SC_LEVEL1_DCACHE_SIZE; the sysfs pass runs on every Linux and supplies the share through a four-argumentqueryCacheSizes(), soCacheSizesreads all four numbers from one detection. Verified under musl in an Alpine 3.20 container withEIGEN_NO_CPUIDforcing the non-x86 path: parent-1/-1/-1, now48K/1M/32M+ 1M share. - The cache-detection coverage moves to a dedicated
cache_sizestest: parsers, a big.LITTLE sysfs fixture (masks confined to the big cores, excluding CPU 0, hitting a CPU without a cache directory, a malformed or missing online list), and a host check that confines the thread to every allowed CPU but 0 and verifies the production query follows the mask. ThesetCpuCacheSizes()override check stays inproduct_large.
Tested this round on x86-64 Linux (WSL2, GCC 13): cache_sizes, all product_large/product_small/product_notemporary parts (46/46), cache_sizes under taskset -c 1-3 and -c 5, and cache_sizes under musl. Not re-run on the ARM machines: the per-CPU minimum equals the CPU-0 reading on a homogeneous part, so their numbers above stand, but a Grace/Jetson run of cache_sizes would confirm.