From d928c80d6357da926e3dcefad1abeaa7731b7d93 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sun, 4 Oct 2026 19:22:29 +0200 Subject: [PATCH] Docs: sync with ndarray v0.2.5 (LASX kernels on loong64) - performance.md: re-synced verbatim from docs/perf.md at v0.2.5 (new section "loong64: LASX kernels (2026-10-04)", SIMD coverage updated); the docs-only summary now reads "Where it stands at v0.2.5", cites the Loongson 3C5000L figures and lists four SIMD architectures. - index.md: performance paragraph mirrors the README at v0.2.5. - roadmap.md: loong64 no longer described as running pure-Go code only. Co-Authored-By: Claude Opus 5.5 --- docs/index.md | 16 ++++++------- docs/performance.md | 55 +++++++++++++++++++++++++++++++++++++++------ docs/roadmap.md | 16 +++++++------ 3 files changed, 65 insertions(+), 22 deletions(-) diff --git a/docs/index.md b/docs/index.md index 0dbc311..6be1226 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,7 +7,7 @@ algebra, all with cgo disabled. Ruby has no cgo-free ndarray (`Numo::NArray`, `NMatrix` are C extensions) and `gonum`'s optimized assembly is amd64-only. go-ndarray pairs a portable pure-Go -core with **multicore fan-out + go-asmgen SIMD** (amd64, arm64 and ppc64le), +core with **multicore fan-out + go-asmgen SIMD** (amd64, arm64, ppc64le and loong64), and a **`Workspace`** arena that takes the garbage collector out of loops. **100% coverage**, differentially checked against NumPy. @@ -60,13 +60,13 @@ On an AMD Zen 3 (16 cores), `x + y` on 1 024 elements goes from 2.6 µs to ## Performance & architectures Large elementwise ops, reductions and products run **multicore** (across -`GOMAXPROCS`). On **amd64, arm64 and ppc64le** the hot loops are go-asmgen SIMD -kernels: sum, add/sub/mul/div, sqrt and the dot product (SSE2 or AVX2/FMA on -amd64, NEON on arm64, VSX on ppc64le), max/min too on amd64, and a -**panel-packed, cache-blocked GEMM** with an SIMD-FMA micro-kernel (NEON 4×8; -AVX2/FMA 6×8 chosen at run time by a CPUID probe, SSE2 fallback; VSX 8×8). -riscv64, loong64, s390x and the 32-bit targets run the same pure-Go code -those kernels are tested against. `Exp` and `Log` are ports of Arm's +`GOMAXPROCS`). On **amd64, arm64, ppc64le and loong64** the hot loops are +go-asmgen SIMD kernels: sum, add/sub/mul/div, sqrt and the dot product (SSE2 or +AVX2/FMA on amd64, NEON on arm64, VSX on ppc64le, LASX on loong64 when the CPU +has it), max/min too on amd64, and a **panel-packed, cache-blocked GEMM** with +an SIMD-FMA micro-kernel (NEON 4×8; AVX2/FMA 6×8 chosen at run time by a CPUID +probe, SSE2 fallback; VSX 8×8; LASX 8×8). riscv64, s390x and the 32-bit +targets run the same pure-Go code those kernels are tested against. `Exp` and `Log` are ports of Arm's optimized-routines (worst 0.504 and 0.508 ULP measured, against up to 0.88 and 0.72 for Go's `math.Exp`/`math.Log` on arm64), and they are correct where amd64's `math.Exp` returns +Inf ([golang/go#81995](https://github.com/golang/go/issues/81995)) and `math.Log` diff --git a/docs/performance.md b/docs/performance.md index 5273158..25b0221 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -4,7 +4,7 @@ Honest, reproducible head-to-head of `go-ndarray/ndarray` against **NumPy 2.2.4** on identical hardware. "On n'a pas le droit de se tromper": every number here is measured, every win is real, and where NumPy still leads it says so. -> **Where it stands at v0.2.3.** This page grew round by round: the sections +> **Where it stands at v0.2.5.** This page grew round by round: the sections > up to *Where go-ndarray still loses* were measured on an arm64 Tart VM > (4 vCPU, NumPy 2.2.4, OpenBLAS 0.3.29); the dated sections after it on an > AMD Zen 3, 16 cores, against **NumPy 2.5.3 + OpenBLAS 0.3.34**. On the Zen 3: @@ -28,9 +28,14 @@ here is measured, every win is real, and where NumPy still leads it says so. > On a POWER9 the VSX kernels added in v0.2.3 double the serial GEMM (7.9 → > 15.8 GFLOP/s at 512²), which is 0.69× OpenBLAS 0.3.34 on one thread; `MatMul` > at 1024² on 32 threads is at 0.56× (see *ppc64le: VSX kernels*). -> SIMD kernels exist on **amd64, arm64 and ppc64le**; the other targets run the -> pure-Go reference code. For loong64, s390x and riscv64 that is work not yet -> done, not a toolchain limit (see *SIMD coverage* at the end). +> On a Loongson 3C5000L the LASX kernels added in v0.2.5 lift the GEMM +> micro-kernel from 7.0 to 27.5 GFLOP/s and the serial GEMM from 5.3 to 20.0 +> GFLOP/s at 512², against the scalar code; no NumPy or BLAS reference was +> available on that host (see *loong64: LASX kernels*). +> SIMD kernels exist on **amd64, arm64, ppc64le and loong64** (LASX, used only +> when the CPU reports it); the other targets run the pure-Go reference code. +> For s390x and riscv64 that is work not yet done, not a toolchain limit (see +> *SIMD coverage* at the end). ## How a pure-Go library can beat NumPy @@ -631,6 +636,37 @@ so the serial GEMM is at 0.69×; 32 threads 13.1 ms at 1024², so MatMul is at 0.56×. OpenBLAS unrolls further and uses POWER9-only `lxv` loads; Go's ppc64le baseline is POWER8. +## loong64: LASX kernels (2026-10-04) + +Go's loong64 assembler has LASX vector float64 add/sub/mul/div, square root and +256-bit loads, but no fused multiply-add and no broadcast load; go-asmgen +v0.14.0 encodes `xvfmadd.d` and `xvldrepl.d` as `WORD`s (pinned against GNU as +2.43, bit-identical to `math.FMA` on a Loongson 3C5000L; transitional until +cmd/asm names them). go-ndarray uses them for sum, dot, sqrt, add/sub/mul/div +and an 8×8 GEMM micro-kernel (16 four-lane accumulators, A broadcast by +`xvldrepl.d`). LASX is not on every LoongArch CPU, so the kernels run only when +the kernel reports it in AT_HWCAP (`/proc/self/auxv`), with the scalar code as +the fallback; the parsing is a pure function tested on every target. + +Loongson 3C5000L (cfarm401, 32 cores), against the scalar code, best of 3 +interleaved runs (the host's load average of ~146 is processes stuck in +uninterruptible sleep; its CPUs measured 100% idle): + +| op | before | after | gain | +|----|--:|--:|:--:| +| micro-kernel, 1 core | 7.0 GFLOP/s | 27.5 GFLOP/s | ×3.95 | +| serial GEMM 512², 1 core | 5.3 GFLOP/s | 20.0 GFLOP/s | ×3.75 | +| MatMul 64² / 512² / 1024², 32 threads | | | ×1.9 / ×1.3 / ×1.1 | +| Sum 256 Ki / 4 Mi, 1 core | 658 / 11 750 µs | 65 / 4 615 µs | ×10.2 / ×2.5 | +| Sum 1 Ki / 16 Ki (below the parallel threshold: one core) | | | ×16 / ×13 | +| Sqrt 1 Ki–16 Ki | | | ×3.8 | +| MatVec 1024² | 364 µs | 224 µs | ×1.6 | + +At 4 Mi elements in parallel, sum and sqrt are memory-bound (parity); the +parallel 4 Mi sum is bimodal on this host (1.6 ms or 7 ms in either version), +so only the minima are compared. No NumPy or BLAS is installable there (the +host has no route to package mirrors), so there is no external reference. + ## SIMD coverage - **amd64 (SSE2)** ships hand-vectorized `sum` (4-accumulator `ADDPD`), `sqrt` @@ -657,7 +693,10 @@ baseline is POWER8. - **ppc64le (VSX)**, since v0.2.3: sum, dot, sqrt, add/sub/mul/div and an 8×8 GEMM micro-kernel; max/min stay scalar (the ISA's `xvmaxdp` NaN rule is not NumPy's). See the ppc64le section above. -- The other three 64-bit Go targets — **riscv64, loong64, s390x** — keep +- **loong64 (LASX)**, since v0.2.5, when the kernel reports LASX in AT_HWCAP + (not every LoongArch CPU has it): sum, dot, sqrt, add/sub/mul/div and an 8×8 + GEMM micro-kernel; max/min stay scalar. See the loong64 section above. +- The other two 64-bit Go targets — **riscv64, s390x** — keep the validated scalar oracles, using the same four-accumulator max/min, direct sqrt loop, and a **scalar 4×4 GEMM micro-kernel** over the packed panels, and still get the **packing + cache blocking + multicore** structure (they have @@ -666,10 +705,12 @@ baseline is POWER8. **ppc64le** has no vector-double arithmetic (no `XVADDDP`/`XVMADDADP`), which go-asmgen v0.13.0 now encodes as `WORD`s, so ppc64le has kernels; **loong64** has vector-double add/sub/mul/div (`VADDD` assembles to - `vfadd.d`, `VMULD` to `vfmul.d`, `XVADDD` to `xvfadd.d`) but no vector FMA; + `vfadd.d`, `VMULD` to `vfmul.d`, `XVADDD` to `xvfadd.d`) but no vector FMA + or broadcast load, which go-asmgen v0.14.0 encodes (transitionally), so + loong64 has kernels; **s390x** has them, FMA included (`VFADB`, `VFMADB`); **riscv64** has them (`VFADDVV`, `VFMACCVV`), but the V extension is optional and needs a run-time - check. So loong64, s390x and riscv64 kernels are work not yet done, not a + check. So s390x and riscv64 kernels are work not yet done, not a toolchain wall. (s390x additionally exercises the big-endian path in CI.) All six are exercised in CI (native amd64/arm64 + qemu for the rest); each diff --git a/docs/roadmap.md b/docs/roadmap.md index 055d8b3..13c2075 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -53,11 +53,13 @@ contiguous `[]float64`. These are the **reference** and the **fallback**. 64-bit Go SIMD targets: **amd64, arm64, riscv64, loong64, ppc64le (VSX), s390x (vector, big-endian)** — selected at runtime via build tags / CPU-feature detection, behind the *same* kernel signatures and the *same* tests. **Today -(v0.2.3) amd64, arm64 and ppc64le have SIMD kernels**; riscv64, loong64, s390x -and the 32-bit targets run the pure-Go reference code. Go's assembler has -vector-double arithmetic on loong64, s390x and riscv64 (V optional), so those -three are work not yet done, not a toolchain limit; it has none on ppc64le, whose -VSX kernels use the float64 encoders go-asmgen v0.13.0 emits as `WORD`s. Because the +(v0.2.5) amd64, arm64, ppc64le and loong64 have SIMD kernels** (loong64's LASX +kernels run only when the CPU reports LASX); riscv64, s390x and the 32-bit +targets run the pure-Go reference code. Go's assembler has vector-double +arithmetic on s390x and riscv64 (V optional), so those two are work not yet +done, not a toolchain limit; it has none on ppc64le, whose VSX kernels use the +float64 encoders go-asmgen v0.13.0 emits as `WORD`s, and no vector FMA or +broadcast load on loong64, which go-asmgen v0.14.0 encodes (transitionally). Because the public API and the test suite address the kernels through that narrow interface, no caller changes and correctness stays pinned by the existing 100%-coverage suite (plus the per-arch CI jobs already wired in `.github/workflows/ci.yml`). @@ -125,8 +127,8 @@ suite (plus the per-arch CI jobs already wired in `.github/workflows/ci.yml`). validated per-arch in CI (bit-identical to the scalar oracle for the elementwise/sqrt/max ops; tight-tolerance for the sum reduction, whose lane-parallel grouping is a valid reordering — the same trade-off as NumPy's - pairwise sum). The other targets (riscv64, loong64, s390x; ppc64le until - its VSX kernels in v0.2.3) keep the validated scalar oracles plus a scalar + pairwise sum). The other targets (riscv64, s390x; ppc64le until its + VSX kernels in v0.2.3, loong64 until its LASX kernels in v0.2.5) keep the validated scalar oracles plus a scalar 4×4 GEMM micro-kernel over the packed panels, and still win via packing + cache-blocking + multicore. Split-CI: the pure-Go core + multicore are held to 100% statement coverage; the generated `.s` is validated by the per-arch native/qemu execution jobs.