Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
57 changes: 51 additions & 6 deletions docs/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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

Expand Down Expand Up @@ -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`
Expand All @@ -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
Expand Down
15 changes: 8 additions & 7 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down Expand Up @@ -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
Expand Down
Loading