Files
tensor/linalg/bench_iterative_test.go
T
petrbalvin af4ee19703
Release / gates (push) Successful in 4m38s
Test / test (push) Successful in 5m16s
Release / release (push) Successful in 35s
feat: initial release
Assisted-by: GLM 5.3 Flash
2026-09-03 10:00:00 +02:00

252 lines
6.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package linalg
import (
"testing"
"sourcedock.dev/petrbalvin/tensor/internal/core"
)
// Iterative solver benchmarks: the Krylov solves and the least-squares
// recursions, with and without the ILU(0) preconditioner. Every system
// is built from fixed literals so a run is reproducible, and each one
// is sized to finish well inside a second, which keeps the timings on
// the solvers' own per-iteration work rather than on input
// construction.
// benchGridCOO builds the five-point stencil of an nx by ny grid as an
// n x n COO matrix (n = nx·ny) in natural row ordering. The diagonal is
// 4 and the vertical coupling −1 on both sides. A nonzero skew makes
// the horizontal coupling −1∓skew, which breaks the symmetry while
// keeping the matrix diagonally dominant; skew 0 leaves it symmetric
// positive-definite.
func benchGridCOO(b *testing.B, nx, ny int, skew float64) *core.SparseCOO {
b.Helper()
n := nx * ny
idx := make([]int64, 0, 5*n*2)
vals := make([]float64, 0, 5*n)
add := func(i, j int, v float64) {
idx = append(idx, int64(i), int64(j))
vals = append(vals, v)
}
for r := range ny {
for c := range nx {
i := r*nx + c
add(i, i, 4)
if c+1 < nx {
add(i, i+1, -1-skew)
}
if c > 0 {
add(i, i-1, -1+skew)
}
if r+1 < ny {
add(i, i+nx, -1)
}
if r > 0 {
add(i, i-nx, -1)
}
}
}
indices, err := core.FromInts(idx, len(vals), 2)
if err != nil {
b.Fatal(err)
}
values, err := core.FromFloats(vals, len(vals))
if err != nil {
b.Fatal(err)
}
coo, err := core.NewSparseCOO(indices, values, []int{n, n})
if err != nil {
b.Fatal(err)
}
return coo
}
// benchTallCOO builds the overdetermined (2n − 2) x n matrix whose
// first n rows are the identity and whose remaining rows are the
// second-difference stencil over three adjacent columns. It is full
// rank and banded, so LSQR and LSMR converge in a bounded number of
// steps.
func benchTallCOO(b *testing.B, n int) *core.SparseCOO {
b.Helper()
m := 2*n - 2
idx := make([]int64, 0, (4*n)*2)
vals := make([]float64, 0, 4*n)
add := func(i, j int, v float64) {
idx = append(idx, int64(i), int64(j))
vals = append(vals, v)
}
for j := range n {
add(j, j, 1)
}
for r := range n - 2 {
i := n + r
add(i, r, -1)
add(i, r+1, 2)
add(i, r+2, -1)
}
indices, err := core.FromInts(idx, len(vals), 2)
if err != nil {
b.Fatal(err)
}
values, err := core.FromFloats(vals, len(vals))
if err != nil {
b.Fatal(err)
}
coo, err := core.NewSparseCOO(indices, values, []int{m, n})
if err != nil {
b.Fatal(err)
}
return coo
}
// benchPatternRHS builds the deterministic right-hand side of length n
// from a fixed integer pattern.
func benchPatternRHS(b *testing.B, n int) *core.Array {
b.Helper()
v := make([]float64, n)
for i := range v {
v[i] = float64(i%7) - 3
}
a, err := core.FromFloats(v, n)
if err != nil {
b.Fatal(err)
}
return a
}
// BenchmarkGMRESSparse measures a restarted GMRES solve of a
// nonsymmetric five-point system.
func BenchmarkGMRESSparse(b *testing.B) {
const n = 400
coo := benchGridCOO(b, 20, 20, 0.5)
c, err := cooToCSR(coo, "BenchmarkGMRESSparse")
if err != nil {
b.Fatal(err)
}
rowStart, colIdx, vals := c.rowStart, c.colIdx, c.vals
op := func(v *core.Array) (*core.Array, error) {
out := core.New(core.Float, n)
dst := out.RawFloats()
for i := range n {
sum := 0.0
for p := rowStart[i]; p < rowStart[i+1]; p++ {
sum += vals[p] * v.FloatAt(colIdx[p])
}
dst[i] = sum
}
return out, nil
}
bv := benchPatternRHS(b, n)
b.ReportAllocs()
for b.Loop() {
if _, err := GMRES(op, bv, 30, 200, 1e-10); err != nil {
b.Fatal(err)
}
}
}
// BenchmarkSpSolveCG measures a conjugate-gradient solve of a
// symmetric five-point system under the Jacobi preconditioner.
func BenchmarkSpSolveCG(b *testing.B) {
coo := benchGridCOO(b, 20, 20, 0)
bv := benchPatternRHS(b, 400)
b.ReportAllocs()
for b.Loop() {
if _, err := SpSolve(coo, bv, 1e-10, 0); err != nil {
b.Fatal(err)
}
}
}
// BenchmarkSpSolveCGILU measures the same system under the ILU(0)
// preconditioner.
func BenchmarkSpSolveCGILU(b *testing.B) {
coo := benchGridCOO(b, 20, 20, 0)
bv := benchPatternRHS(b, 400)
ilu, err := NewSparseILU(coo)
if err != nil {
b.Fatal(err)
}
b.ReportAllocs()
for b.Loop() {
if _, err := SpSolve(coo, bv, 1e-10, 0, ilu); err != nil {
b.Fatal(err)
}
}
}
// BenchmarkSpSolveBiCGSTAB measures a BiCGSTAB solve of a nonsymmetric
// five-point system under the Jacobi preconditioner.
func BenchmarkSpSolveBiCGSTAB(b *testing.B) {
coo := benchGridCOO(b, 20, 20, 0.5)
bv := benchPatternRHS(b, 400)
b.ReportAllocs()
for b.Loop() {
if _, err := SpSolveBiCGSTAB(coo, bv, 1e-10, 0); err != nil {
b.Fatal(err)
}
}
}
// BenchmarkSpSolveBiCGSTABILU measures the same system under the
// ILU(0) preconditioner.
func BenchmarkSpSolveBiCGSTABILU(b *testing.B) {
coo := benchGridCOO(b, 20, 20, 0.5)
bv := benchPatternRHS(b, 400)
ilu, err := NewSparseILU(coo)
if err != nil {
b.Fatal(err)
}
b.ReportAllocs()
for b.Loop() {
if _, err := SpSolveBiCGSTAB(coo, bv, 1e-10, 0, ilu); err != nil {
b.Fatal(err)
}
}
}
// BenchmarkSpLSQR measures the Golub-Kahan least-squares recursion of
// SpLSQR on an overdetermined banded system.
func BenchmarkSpLSQR(b *testing.B) {
const n = 400
coo := benchTallCOO(b, n)
bv := benchPatternRHS(b, 2*n-2)
_, info, err := SpLSQR(coo, bv, 1e-10, 0, 0)
if err != nil {
b.Fatal(err)
}
steps := float64(info.Iterations)
b.ReportAllocs()
for b.Loop() {
if _, _, err := SpLSQR(coo, bv, 1e-10, 0, 0); err != nil {
b.Fatal(err)
}
}
// The system and the tolerance are fixed, so the step count is too:
// reporting it shows the size of the work the timing covers. It goes
// after the loop, which clears any metric reported before it.
b.ReportMetric(steps, "steps")
}
// BenchmarkSpLSMR measures the LSMR recursion on the same system.
func BenchmarkSpLSMR(b *testing.B) {
const n = 400
coo := benchTallCOO(b, n)
bv := benchPatternRHS(b, 2*n-2)
_, info, err := SpLSMR(coo, bv, 1e-10, 0, 0)
if err != nil {
b.Fatal(err)
}
steps := float64(info.Iterations)
b.ReportAllocs()
for b.Loop() {
if _, _, err := SpLSMR(coo, bv, 1e-10, 0, 0); err != nil {
b.Fatal(err)
}
}
b.ReportMetric(steps, "steps")
}