From 0b92ccdaf39c3a1a90d3dea27eaf80fa25f70c9e Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sun, 4 Oct 2026 18:23:07 +0200 Subject: [PATCH] Sync the docs with ndarray v0.2.3: ppc64le VSX kernels ndarray v0.2.3 (go-ndarray/ndarray#20) adds VSX SIMD kernels on ppc64le. - performance.md: re-synced verbatim from docs/perf.md at v0.2.3 (new section 'ppc64le: VSX kernels (2026-10-04)' and the SIMD coverage update); the summary is now 'Where it stands at v0.2.3', names amd64, arm64 and ppc64le and gives the POWER9 GEMM figures from that section. - index.md: SIMD on amd64, arm64 and ppc64le, worded as the ndarray README at v0.2.3 (max/min SIMD on amd64 only, VSX 8x8 micro-kernel). - roadmap.md: the 'Today (v0.2.1)' sentence and the 'other four targets' clause no longer leave ppc64le on scalar code. Co-Authored-By: Claude Opus 5.5 --- docs/index.md | 20 ++++++++-------- docs/performance.md | 57 ++++++++++++++++++++++++++++++++++++++++----- docs/roadmap.md | 15 ++++++------ 3 files changed, 69 insertions(+), 23 deletions(-) diff --git a/docs/index.md b/docs/index.md index a7f66af..0dbc311 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,8 +7,8 @@ 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 and arm64), and a -**`Workspace`** arena that takes the garbage collector out of loops. **100% +core with **multicore fan-out + go-asmgen SIMD** (amd64, arm64 and ppc64le), +and a **`Workspace`** arena that takes the garbage collector out of loops. **100% coverage**, differentially checked against NumPy. ```go @@ -60,14 +60,14 @@ 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 and arm64** the hot loops are go-asmgen SIMD kernels: -sum, max/min, add/sub/mul/div, sqrt and the dot product (SSE2 or AVX2/FMA on -amd64, NEON on arm64), 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). riscv64, loong64, ppc64le, 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 +`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 +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` is wrong on subnormals diff --git a/docs/performance.md b/docs/performance.md index f3c353b..5273158 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.2.** This page grew round by round: the sections +> **Where it stands at v0.2.3.** 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: @@ -25,9 +25,12 @@ here is measured, every win is real, and where NumPy still leads it says so. > On Apple silicon the GEMM reached parity with tuned BLAS at 1024² (OpenBLAS in > an arm64 VM, single-threaded vecLib on an M4 Max) and beats the pure-Go > `gonum` 4–10× ([`BENCHMARKS.md`](https://github.com/go-ndarray/ndarray/blob/main/BENCHMARKS.md)). -> SIMD kernels exist on **amd64 and arm64**; 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 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). ## How a pure-Go library can beat NumPy @@ -590,6 +593,44 @@ this log. Speed, 16 Ki elements, one core: M4 37 µs (`math.Log` 72), Zen 3 73 µs (`math.Log` 175, NumPy 69). +## ppc64le: VSX kernels (2026-10-04) + +Go's ppc64 assembler has the VSX loads, stores and permutes but no vector +double arithmetic, so ppc64le ran the scalar code. go-asmgen v0.13.0 encodes +`xvadddp`, `xvsubdp`, `xvmuldp`, `xvdivdp`, `xvmaddadp`, `xvmaxdp`, `xvmindp` +and `xvsqrtdp` as `WORD`s, pinned against GNU as 2.44 and run bit-identical to +Go's arithmetic and `math.FMA` on POWER8 and POWER9. With them, go-ndarray has +VSX kernels for sum, dot, sqrt, add/sub/mul/div and the GEMM. + +The GEMM micro-kernel (8×8, 32 accumulators) follows OpenBLAS's POWER9 dgemm: +no splats. Each A pair and its swap multiply each B column pair, so one +accumulator holds a diagonal (C[r][c], C[r+1][c+1]) and another the +anti-diagonal; the store rebuilds the rows with `xxpermdi`. On one POWER9 core, +measured in one binary: 13.3 GFLOP/s with `lxvdsx` splats, 14.8 with `xxpermdi` +splats, 15.4 with those unrolled by two, **16.9** with the diagonal scheme +unrolled by two (shipped). + +POWER9 (cfarm29, 8 cores × SMT4), against the scalar code (best of 3–4 +interleaved runs): + +| op | before | after | gain | +|----|--:|--:|:--:| +| micro-kernel, 1 core | 8.3 GFLOP/s | 16.2 GFLOP/s | ×1.96 | +| serial GEMM 512², 1 core | 7.9 GFLOP/s | 15.8 GFLOP/s | ×2.0 | +| MatMul 1024², 32 threads | 39.2 ms | 23.6 ms | ×1.66 | +| MatMul 128² / 512² | | | ×1.8 / ×1.5 | +| MatVec 1024² | 823 µs | 267 µs | ×3.1 | +| Sum 1 Ki / 16 Ki / 256 Ki | | | ×6.6 / ×7.3 / ×3.2 | +| AddInto 1 Ki–256 Ki | | | ×2.7–4.1 | +| Sqrt, 1 core | | | ×1.05–1.18 | + +At 4 Mi elements sum and add are memory-bound (×1.1). **Against OpenBLAS** +0.3.34 (its POWER9 kernel, loaded through `scipy-openblas64` on the same host; +the ppc64le NumPy wheel there has no BLAS): one thread 22.8 GFLOP/s at 512², +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. + ## SIMD coverage - **amd64 (SSE2)** ships hand-vectorized `sum` (4-accumulator `ADDPD`), `sqrt` @@ -613,13 +654,17 @@ Speed, 16 Ki elements, one core: M4 37 µs (`math.Log` 72), Zen 3 73 µs `FMAXD`/`FMIND`); the C-accumulate fold still uses `VFMLA` against `1.0` (no plain vector FP add exists). All beat/parity NumPy via SIMD + multicore, no `func`-pointer indirection. -- The other four 64-bit Go targets — **riscv64, loong64, ppc64le, s390x** — keep +- **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 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 not been measured against NumPy). What the Go assembler offers them, checked on Go 1.26.4 and 1.27.1 by assembling and disassembling (2026-10-04): - **ppc64le** has no vector-double arithmetic (no `XVADDDP`/`XVMADDADP`); + **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; **s390x** has them, FMA included (`VFADB`, `VFMADB`); **riscv64** has them diff --git a/docs/roadmap.md b/docs/roadmap.md index 39f8e3b..055d8b3 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -53,10 +53,11 @@ 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.1) only amd64 and arm64 have SIMD kernels**; riscv64, loong64, ppc64le, -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), but not on -ppc64le, so those three are work not yet done, not a toolchain limit. Because the +(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 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`). @@ -124,9 +125,9 @@ 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 four targets 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 + 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 + 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. - **SIMD `Sqrt`/`Max` and the packed, cache-blocked GEMM — DONE (no longer