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
16 changes: 8 additions & 8 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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`
Expand Down
55 changes: 48 additions & 7 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.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:
Expand All @@ -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

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