Documentation
¶
Overview ¶
Package simdblas is a BLAS implementation for gonum, backed by github.com/sebishogun/simd.
Gonum's own BLAS is pure Go with SSE2-only assembly on amd64 and none at all on arm64. This replaces the routines where a vector unit helps, reaching AVX-512, NEON, SVE2, RVV, VSX, VX and LASX — with no cgo, because the kernels are compiled ahead of time and committed as assembly.
Install it once, at startup:
import (
"gonum.org/v1/gonum/blas/blas64"
"github.com/sebishogun/simdblas"
)
func init() {
blas64.Use(simdblas.Implementation{})
blas32.Use(simdblas.Implementation{})
}
Everything above BLAS — gonum's mat, stat and optimize — then runs on it without further change.
What is accelerated ¶
The routines where whole-slice vector work pays, in both precisions: the Level 1 set, gemv and ger, the symmetric rank-1 and rank-2 updates, and all of Level 3 except the banded and packed variants — gemm, symm, trsm, trmm, syrk and syr2k. Everything else is inherited from gonum.Implementation and behaves exactly as it did.
symv and trmv are deliberately absent. The trick that makes symm eighteen times faster — filling the implied half of the matrix in and handing the result to the accelerated multiply — costs n^2 for Level 3, where the multiply is n^3, and costs n^2 for Level 2, where the multiply is also n^2. Measured at 0.11x, so they stay gonum's.
Acceleration also requires unit strides and, for the matrix routines, no transpose, alpha of 1, beta of 0 and natural leading dimensions. Anything else falls through to gonum, so the answer is always a correct BLAS answer and never a wrong fast one. See Implementation for why that is the whole design.
Results differ from gonum's, and that is the point ¶
BLAS does not specify bit-exact results, and no two implementations agree. Reductions here use a fixed sixteen-accumulator tree, gonum's use a four-way unrolled loop, so a dot product of 4096 elements typically differs in the last unit in the last place.
The difference worth knowing runs the other way. Gonum's answer depends on what it was compiled for: its amd64 assembly and its pure-Go fallback do not agree with each other, so the same program gives different results on x86 and on arm64. This implementation gives the same bits on every architecture and every instruction set, because the accumulation order is fixed rather than following the vector width. If you need a result that reproduces across machines, that is the reason to use this.
Example ¶
Installing the backend is the whole integration. Do it once, at startup, before anything numerical runs — every gonum package that reaches BLAS picks it up from there, including mat, stat and optimize.
package main
import (
"fmt"
"github.com/sebishogun/simdblas"
"gonum.org/v1/gonum/blas/blas32"
"gonum.org/v1/gonum/blas/blas64"
"gonum.org/v1/gonum/mat"
)
func main() {
blas64.Use(simdblas.Implementation{})
blas32.Use(simdblas.Implementation{})
a := mat.NewDense(2, 2, []float64{1, 2, 3, 4})
b := mat.NewDense(2, 2, []float64{5, 6, 7, 8})
var c mat.Dense
c.Mul(a, b)
fmt.Println(mat.Formatted(&c))
}
Output: ⎡19 22⎤ ⎣43 50⎦
Index ¶
- type Implementation
- func (impl Implementation) Dasum(n int, x []float64, incX int) float64
- func (impl Implementation) Daxpy(n int, alpha float64, x []float64, incX int, y []float64, incY int)
- func (impl Implementation) Ddot(n int, x []float64, incX int, y []float64, incY int) float64
- func (impl Implementation) Dgemm(tA, tB blas.Transpose, m, n, k int, alpha float64, a []float64, lda int, ...)
- func (impl Implementation) Dgemv(tA blas.Transpose, m, n int, alpha float64, a []float64, lda int, x []float64, ...)
- func (impl Implementation) Dger(m, n int, alpha float64, x []float64, incX int, y []float64, incY int, ...)
- func (impl Implementation) Dnrm2(n int, x []float64, incX int) float64
- func (impl Implementation) Drot(n int, x []float64, incX int, y []float64, incY int, c, s float64)
- func (impl Implementation) Dscal(n int, alpha float64, x []float64, incX int)
- func (impl Implementation) Dswap(n int, x []float64, incX int, y []float64, incY int)
- func (impl Implementation) Dsymm(s blas.Side, ul blas.Uplo, m, n int, alpha float64, a []float64, lda int, ...)
- func (impl Implementation) Dsyr(ul blas.Uplo, n int, alpha float64, x []float64, incX int, a []float64, ...)
- func (impl Implementation) Dsyr2(ul blas.Uplo, n int, alpha float64, x []float64, incX int, y []float64, ...)
- func (impl Implementation) Dsyr2k(ul blas.Uplo, t blas.Transpose, n, k int, alpha float64, a []float64, lda int, ...)
- func (impl Implementation) Dsyrk(ul blas.Uplo, t blas.Transpose, n, k int, alpha float64, a []float64, lda int, ...)
- func (impl Implementation) Dtrmm(side blas.Side, ul blas.Uplo, tA blas.Transpose, d blas.Diag, m, n int, ...)
- func (impl Implementation) Dtrsm(side blas.Side, ul blas.Uplo, tA blas.Transpose, d blas.Diag, m, n int, ...)
- func (impl Implementation) Sasum(n int, x []float32, incX int) float32
- func (impl Implementation) Saxpy(n int, alpha float32, x []float32, incX int, y []float32, incY int)
- func (impl Implementation) Sdot(n int, x []float32, incX int, y []float32, incY int) float32
- func (impl Implementation) Sgemm(tA, tB blas.Transpose, m, n, k int, alpha float32, a []float32, lda int, ...)
- func (impl Implementation) Sgemv(tA blas.Transpose, m, n int, alpha float32, a []float32, lda int, x []float32, ...)
- func (impl Implementation) Sger(m, n int, alpha float32, x []float32, incX int, y []float32, incY int, ...)
- func (impl Implementation) Snrm2(n int, x []float32, incX int) float32
- func (impl Implementation) Srot(n int, x []float32, incX int, y []float32, incY int, c, s float32)
- func (impl Implementation) Sscal(n int, alpha float32, x []float32, incX int)
- func (impl Implementation) Sswap(n int, x []float32, incX int, y []float32, incY int)
- func (impl Implementation) Ssymm(s blas.Side, ul blas.Uplo, m, n int, alpha float32, a []float32, lda int, ...)
- func (impl Implementation) Ssyr(ul blas.Uplo, n int, alpha float32, x []float32, incX int, a []float32, ...)
- func (impl Implementation) Ssyr2(ul blas.Uplo, n int, alpha float32, x []float32, incX int, y []float32, ...)
- func (impl Implementation) Ssyr2k(ul blas.Uplo, t blas.Transpose, n, k int, alpha float32, a []float32, lda int, ...)
- func (impl Implementation) Ssyrk(ul blas.Uplo, t blas.Transpose, n, k int, alpha float32, a []float32, lda int, ...)
- func (impl Implementation) Strmm(side blas.Side, ul blas.Uplo, tA blas.Transpose, d blas.Diag, m, n int, ...)
- func (impl Implementation) Strsm(side blas.Side, ul blas.Uplo, tA blas.Transpose, d blas.Diag, m, n int, ...)
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Implementation ¶
type Implementation struct{ gonum.Implementation }
Implementation is a drop-in for gonum's BLAS.
It embeds gonum.Implementation rather than reimplementing the interface, so every routine exists and is correct from the first line, and each accelerated one is an override of a working method rather than a new one that has to be got right. The 60-odd routines nobody has measured a win for are gonum's, unchanged.
Each override checks whether its fast path applies and delegates to the embedded method when it does not — including for invalid arguments, so the panics a caller relies on come from gonum and read exactly as they always have.
Example (Cholesky) ¶
Cholesky is the cheapest factorisation for a symmetric positive-definite matrix, and the one that benefits most here: it alternates trsm with syrk, and both are blocked through the accelerated gemm.
package main
import (
"fmt"
"github.com/sebishogun/simdblas"
"gonum.org/v1/gonum/blas/blas64"
"gonum.org/v1/gonum/mat"
)
func main() {
blas64.Use(simdblas.Implementation{})
s := mat.NewSymDense(3, []float64{4, 1, 0, 1, 5, 2, 0, 2, 6})
var chol mat.Cholesky
if ok := chol.Factorize(s); !ok {
fmt.Println("not positive definite")
return
}
fmt.Printf("det = %.1f\n", chol.Det())
}
Output: det = 98.0
Example (DirectBLAS) ¶
The BLAS interface can also be used directly, without gonum's mat types. This is the same multiply as the first example, stated in the terms BLAS uses: row-major slices with an explicit leading dimension.
package main
import (
"fmt"
"github.com/sebishogun/simdblas"
"gonum.org/v1/gonum/blas"
)
func main() {
var impl simdblas.Implementation
a := []float64{1, 2, 3, 4} // 2x2
b := []float64{5, 6, 7, 8} // 2x2
c := make([]float64, 4)
// C = 1*A*B + 0*C
impl.Dgemm(blas.NoTrans, blas.NoTrans, 2, 2, 2, 1, a, 2, b, 2, 0, c, 2)
fmt.Println(c)
}
Output: [19 22 43 50]
Example (Float32) ¶
float32 works the same way and needs its own registration: blas32 and blas64 are separate globals, and installing one does nothing for the other.
package main
import (
"fmt"
"github.com/sebishogun/simdblas"
"gonum.org/v1/gonum/blas/blas32"
)
func main() {
blas32.Use(simdblas.Implementation{})
var impl simdblas.Implementation
x := []float32{1, 2, 3, 4}
y := []float32{5, 6, 7, 8}
fmt.Println(impl.Sdot(4, x, 1, y, 1))
}
Output: 70
Example (MatrixMultiply) ¶
Matrix multiplication is where the largest gains are, because gemm has enough arithmetic per byte to keep a vector unit busy. Nothing about the calling code changes.
package main
import (
"fmt"
"github.com/sebishogun/simdblas"
"gonum.org/v1/gonum/blas/blas64"
"gonum.org/v1/gonum/mat"
)
func main() {
blas64.Use(simdblas.Implementation{})
a := mat.NewDense(2, 3, []float64{1, 2, 3, 4, 5, 6})
b := mat.NewDense(3, 2, []float64{7, 8, 9, 10, 11, 12})
var c mat.Dense
c.Mul(a, b)
fmt.Println(mat.Formatted(&c))
}
Output: ⎡ 58 64⎤ ⎣139 154⎦
Example (Reproducibility) ¶
Results differ from gonum's in the last place, because no two BLAS implementations accumulate in the same order and the specification does not require them to. What this one guarantees instead is that the answer does not depend on the machine: the accumulation order is fixed rather than following the vector width, so a result computed here reproduces on any architecture.
package main
import (
"fmt"
"github.com/sebishogun/simdblas"
)
func main() {
var impl simdblas.Implementation
x := make([]float64, 4096)
for i := range x {
x[i] = 1.0 / float64(i+1)
}
// The same bits on amd64, arm64, riscv64 and the rest.
fmt.Printf("%.10f\n", impl.Ddot(len(x), x, 1, x, 1))
}
Output: 1.6446899560
Example (Solve) ¶
Solving a linear system goes through an LU factorisation, which is built out of gemm and trsm rather than out of a single multiply. Both are accelerated, so this benefits too — less than a bare multiply does, because a factorisation also contains scalar work that no vector unit reaches.
package main
import (
"fmt"
"github.com/sebishogun/simdblas"
"gonum.org/v1/gonum/blas/blas64"
"gonum.org/v1/gonum/mat"
)
func main() {
blas64.Use(simdblas.Implementation{})
// 4x + y = 9
// x + 3y = 8
a := mat.NewDense(2, 2, []float64{4, 1, 1, 3})
b := mat.NewVecDense(2, []float64{9, 8})
var x mat.VecDense
if err := x.SolveVec(a, b); err != nil {
fmt.Println("singular:", err)
return
}
fmt.Printf("x = %.4f, y = %.4f\n", x.AtVec(0), x.AtVec(1))
}
Output: x = 1.7273, y = 2.0909
Example (TriangularSolve) ¶
A triangular solve with several right-hand sides — the operation LAPACK spends half a factorisation in. Accelerated by blocking: gonum solves the small diagonal blocks, the fast gemm does the updates between them.
package main
import (
"fmt"
"github.com/sebishogun/simdblas"
"gonum.org/v1/gonum/blas"
)
func main() {
var impl simdblas.Implementation
// Lower triangular, unit diagonal:
// [1 0] [1] [1 ]
// [2 1] * X = [4] -> X=[2 ]
a := []float64{1, 0, 2, 1}
b := []float64{1, 4}
impl.Dtrsm(blas.Left, blas.Lower, blas.NoTrans, blas.Unit, 2, 1, 1, a, 2, b, 1)
fmt.Println(b)
}
Output: [1 2]
func (Implementation) Dasum ¶
func (impl Implementation) Dasum(n int, x []float64, incX int) float64
func (Implementation) Daxpy ¶
func (impl Implementation) Daxpy(n int, alpha float64, x []float64, incX int, y []float64, incY int)
Daxpy is y += alpha*x, the operation BLAS calls axpy and this library calls AddScaled. One pass over both slices rather than a scale followed by an add.
func (Implementation) Dger ¶ added in v0.2.0
func (impl Implementation) Dger(m, n int, alpha float64, x []float64, incX int, y []float64, incY int, a []float64, lda int)
Dger is the rank-1 update, and the shape it arrives in decides how much of it can be accelerated.
simd.RankOneInto takes neither a leading dimension nor a stride for x, so the single-call path needs a matrix whose rows are exactly n apart and an x that is contiguous. LAPACK's Dgetf2 — the inner loop of an LU — supplies neither: it passes a *column* of the working matrix as x, so incX is lda, and it updates a trailing submatrix, so lda is the width of the parent rather than n. Guarding on those two conditions alone meant Dger delegated on every call an LU ever made, which is exactly the case it was written for.
So there is a second path. A rank-1 update is m independent axpys, one per row, and simd.AddScaled handles an arbitrary row offset because each row is its own contiguous slice. That costs one call per row instead of one per matrix and works for any lda and any incX.
func (Implementation) Dnrm2 ¶
func (impl Implementation) Dnrm2(n int, x []float64, incX int) float64
Dnrm2 is the one routine here that cannot simply be handed to the vector unit, and the reason is in the BLAS specification rather than in the kernel.
nrm2 is *defined* to avoid spurious overflow and underflow: gonum computes it by scaling to the largest element first, so a vector of 1e200s returns 8.48e200 rather than +Inf. The sum of squares overflows there, and flushes to zero for a vector of 1e-200s. Swapping the backend must not turn a finite result into an infinity.
Checking the magnitudes first works and costs a whole extra pass — measured at 483us against 54us for a million elements, which spends most of the win to buy an answer that is almost always already correct. So instead the fast path runs and its own failure is detected: the sum of squares overflowing produces +Inf, and underflowing produces a zero that a non-empty vector can only otherwise reach by being all zeros. Both are exactly the cases gonum's scaled algorithm exists for, and both then go to it.
The common path pays one comparison.
func (Implementation) Dscal ¶
func (impl Implementation) Dscal(n int, alpha float64, x []float64, incX int)
func (Implementation) Dsymm ¶ added in v1.0.0
func (impl Implementation) Dsymm(s blas.Side, ul blas.Uplo, m, n int, alpha float64, a []float64, lda int, b []float64, ldb int, beta float64, c []float64, ldc int)
Dsymm computes C = alpha*A*B + beta*C with A symmetric, or C = alpha*B*A when side is Right.
func (Implementation) Dsyr ¶ added in v1.0.0
func (impl Implementation) Dsyr(ul blas.Uplo, n int, alpha float64, x []float64, incX int, a []float64, lda int)
Dsyr is A += alpha*x*xᵀ over one triangle.
No scratch and no densifying: the part of each row that lies in the triangle is a contiguous span, and adding a scaled vector to a contiguous span is exactly AddScaled. This is the rank-1 update restricted to half the matrix.
func (Implementation) Dsyr2 ¶ added in v1.0.0
func (impl Implementation) Dsyr2(ul blas.Uplo, n int, alpha float64, x []float64, incX int, y []float64, incY int, a []float64, lda int)
Dsyr2 is A += alpha*x*yᵀ + alpha*y*xᵀ over one triangle.
func (Implementation) Dsyr2k ¶ added in v1.0.0
func (impl Implementation) Dsyr2k(ul blas.Uplo, t blas.Transpose, n, k int, alpha float64, a []float64, lda int, b []float64, ldb int, beta float64, c []float64, ldc int)
Dsyr2k computes C = alpha*A*Bᵀ + alpha*B*Aᵀ + beta*C, or the transposed form, writing one triangle.
func (Implementation) Dsyrk ¶ added in v0.3.0
func (impl Implementation) Dsyrk(ul blas.Uplo, t blas.Transpose, n, k int, alpha float64, a []float64, lda int, beta float64, c []float64, ldc int)
Dsyrk computes C = alpha*A*Aᵀ + beta*C, or alpha*Aᵀ*A, writing only one triangle of C.
This one is done by computing the whole product and keeping half, which is twice the arithmetic a purpose-built syrk would do. It still wins, because the multiply it doubles is the accelerated one and the alternative is gonum's scalar triple loop — the factor of two is smaller than the factor the vector unit brings. A blocked form that only touches the triangle would be better and is not written; this is the version that could be measured today.
func (Implementation) Dtrmm ¶ added in v0.3.0
func (impl Implementation) Dtrmm(side blas.Side, ul blas.Uplo, tA blas.Transpose, d blas.Diag, m, n int, alpha float64, a []float64, lda int, b []float64, ldb int)
Dtrmm computes B = alpha*op(A)*B or B = alpha*B*op(A) with A triangular.
func (Implementation) Dtrsm ¶ added in v0.3.0
func (impl Implementation) Dtrsm(side blas.Side, ul blas.Uplo, tA blas.Transpose, d blas.Diag, m, n int, alpha float64, a []float64, lda int, b []float64, ldb int)
Dtrsm solves a triangular system with multiple right-hand sides.
Blocked through the accelerated gemm when the problem is big enough to pay for the bookkeeping; gonum's own otherwise, and for anything this cannot validate.
func (Implementation) Sasum ¶
func (impl Implementation) Sasum(n int, x []float32, incX int) float32
func (Implementation) Snrm2 ¶
func (impl Implementation) Snrm2(n int, x []float32, incX int) float32
func (Implementation) Sscal ¶
func (impl Implementation) Sscal(n int, alpha float32, x []float32, incX int)
func (Implementation) Ssyrk ¶ added in v0.3.0
func (impl Implementation) Ssyrk(ul blas.Uplo, t blas.Transpose, n, k int, alpha float32, a []float32, lda int, beta float32, c []float32, ldc int)
Ssyrk computes C = alpha*A*Aᵀ + beta*C, or alpha*Aᵀ*A, writing only one triangle of C.
This one is done by computing the whole product and keeping half, which is twice the arithmetic a purpose-built syrk would do. It still wins, because the multiply it doubles is the accelerated one and the alternative is gonum's scalar triple loop — the factor of two is smaller than the factor the vector unit brings. A blocked form that only touches the triangle would be better and is not written; this is the version that could be measured today.
func (Implementation) Strmm ¶ added in v0.3.0
func (impl Implementation) Strmm(side blas.Side, ul blas.Uplo, tA blas.Transpose, d blas.Diag, m, n int, alpha float32, a []float32, lda int, b []float32, ldb int)
Strmm computes B = alpha*op(A)*B or B = alpha*B*op(A) with A triangular.
func (Implementation) Strsm ¶ added in v0.3.0
func (impl Implementation) Strsm(side blas.Side, ul blas.Uplo, tA blas.Transpose, d blas.Diag, m, n int, alpha float32, a []float32, lda int, b []float32, ldb int)
Strsm solves a triangular system with multiple right-hand sides.
Blocked through the accelerated gemm when the problem is big enough to pay for the bookkeeping; gonum's own otherwise, and for anything this cannot validate.