| // Copyright 2026 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| //go:build goexperiment.simd && arm64 | |
| // SGEMM (C += A*B on float32) written with the scalable SVE API, checked | |
| // against a scalar reference and benchmarked against the hand-written | |
| // assembly kernel in sgemm_sve_arm64.s. | |
| package simd | |
| import ( | |
| "fmt" | |
| "simd/archsimd" | |
| "testing" | |
| ) | |
| // sgemmSIMD computes C += A(m×k) * B(k×n), for any m, k, n. | |
| // A, B, C are row-major matrixes, e.g: | |
| // | |
| // A (2×3) B (3×6) => C (2×6) | |
| // [ 1 2 3 ] [ 10 11 12 13 14 15 ] [ 140 146 152 158 164 170 ] | |
| // [ 4 5 6 ] [ 20 21 22 23 24 25 ] [ 320 335 350 365 380 395 ] | |
| // [ 30 31 32 33 34 35 ] | |
| // | |
| // Matmul is paralleized at the C-row level, the final C is completed in | |
| // in mxn/Float32s.len() steps (the well-aligned case). | |
| // The code documentation in the function body are written assuming 2D slice, | |
| // i.e. a[i][j] is a[i * m + j]. | |
| func sgemmSIMD(c, a, b []float32, m, k, n int) { | |
| if len(c) < m*n || len(a) < m*k || len(b) < k*n { | |
| panic("sgemmSIMD: insufficient input slice length") | |
| } | |
| vl := archsimd.Float32s{}.Len() | |
| allTrue := archsimd.Mask32sAllTrue() | |
| // Computes the the i-th row of c | |
| for i := 0; i < m; i++ { | |
| // Take a[i] | |
| ai := a[i*k : i*k+k] | |
| // c[i] will be completed, take its reference here | |
| ci := c[i*n : i*n+n] | |
| j := 0 | |
| // SIMD loop, parallel level vl, parallelize on c[i] | |
| for ; j+vl <= n; j += vl { | |
| // acc is the accumulator for c[i][j:j+vl] | |
| var acc archsimd.Float32s | |
| off := j // offset in b | |
| // Run over each column of a[i] | |
| for p := range k { | |
| // av is [a[i][p], a[i][p], ..., a[i][p]] | |
| av := archsimd.BroadcastFloat32s(ai[p]) | |
| // bv is [b[p][j], b[p][j+1], ..., b[p][j+vl]] | |
| bv := archsimd.LoadFloat32s(b[off : off+vl]) | |
| // Update c[i]: | |
| // c[i][j] += a[i][p] * b[p][j] | |
| // c[i][j+1] += a[i][p] * b[p][j+1] | |
| // ... | |
| // c[i][j+vl] += a[i][p] * b[p][j+vl] | |
| // | |
| // Note: MulAdd is a predicated-only instruction, | |
| // to provide the API unpredicated the compiler | |
| // will insert a synthesized all-true predicate | |
| // for the instruction. However LICM doesn't work | |
| // on SIMD operations yet, this all-true predicate | |
| // will be re-generated for every iteration of this | |
| // loop. To avoid this, we explicitly create | |
| // an all-true predicate for the instruction, bypassing | |
| // the synthesized all-true predicate, i.e. essentially | |
| // doing LICM manually. | |
| acc = av.MulAdd(bv, acc).IfElse(allTrue, acc) | |
| // Move b to next row | |
| off += n | |
| } | |
| acc.Store(ci[j : j+vl]) | |
| } | |
| sgemmTail(ci, ai, b, j, n, allTrue) | |
| } | |
| } | |
| // sgemmTail is the masked tail of the inner-most loop of | |
| // sgemmSIMD and sgemmSIMD2, replacing the panicing unmasked loads | |
| // with the masked partial loads. | |
| func sgemmTail(ci, ai, b []float32, j, n int, allTrue archsimd.Mask32s) { | |
| if j >= n { | |
| return | |
| } | |
| var acc archsimd.Float32s | |
| off := j | |
| for p := range ai { | |
| av := archsimd.BroadcastFloat32s(ai[p]) | |
| bv, _ := archsimd.LoadFloat32sPart(b[off : off+n-j]) | |
| acc = av.MulAdd(bv, acc).IfElse(allTrue, acc) | |
| off += n | |
| } | |
| acc.StorePart(ci[j:]) | |
| } | |
| // sgemmSIMD2 manually unrolled the inner-most loop of sgemmSIMD by 2 times, | |
| // which we can save one broadcast out of it. | |
| func sgemmSIMD2(c, a, b []float32, m, k, n int) { | |
| if len(c) < m*n || len(a) < m*k || len(b) < k*n { | |
| panic("sgemmSIMD2: insufficient input slice length") | |
| } | |
| vl := archsimd.Float32s{}.Len() | |
| allTrue := archsimd.Mask32sAllTrue() | |
| for i := 0; i < m; i++ { | |
| ai := a[i*k : i*k+k] | |
| ci := c[i*n : i*n+n] | |
| j := 0 | |
| for ; j+2*vl <= n; j += 2 * vl { | |
| var acc0, acc1 archsimd.Float32s | |
| off := j | |
| for p := range k { | |
| av := archsimd.BroadcastFloat32s(ai[p]) | |
| bv0 := archsimd.LoadFloat32s(b[off : off+vl]) | |
| acc0 = av.MulAdd(bv0, acc0).IfElse(allTrue, acc0) | |
| bv1 := archsimd.LoadFloat32s(b[off+vl : off+2*vl]) | |
| acc1 = av.MulAdd(bv1, acc1).IfElse(allTrue, acc1) | |
| off += n | |
| } | |
| acc0.Store(ci[j : j+vl]) | |
| acc1.Store(ci[j+vl : j+2*vl]) | |
| } | |
| for ; j+vl <= n; j += vl { | |
| var acc archsimd.Float32s | |
| off := j | |
| for p := range k { | |
| av := archsimd.BroadcastFloat32s(ai[p]) | |
| bv := archsimd.LoadFloat32s(b[off : off+vl]) | |
| acc = av.MulAdd(bv, acc).IfElse(allTrue, acc) | |
| off += n | |
| } | |
| acc.Store(ci[j : j+vl]) | |
| } | |
| sgemmTail(ci, ai, b, j, n, allTrue) | |
| } | |
| } | |
| // sgemmAsm is asm-equivalent of sgemmSIMD. | |
| // Unlike sgemmSIMD it has no masked tail, so n must be a | |
| // multiple of the vector length. | |
| func sgemmAsm(c, a, b []float32, m, k, n int) { | |
| if len(c) < m*n || len(a) < m*k || len(b) < k*n { | |
| panic("sgemmAsm: insufficient input slice length") | |
| } | |
| vl := archsimd.Float32s{}.Len() | |
| if n%vl != 0 { | |
| panic("sgemmAsm: n must be a multiple of the vector length") | |
| } | |
| for i := 0; i < m; i++ { | |
| for j := 0; j <= n-vl; j += vl { | |
| sgemmKernelSVE(&c[i*n+j], &a[i*k], &b[j], int64(k), int64(n)*4) | |
| } | |
| } | |
| } | |
| // sgemmScalar is the scalar reference implementation of sgemm. | |
| func sgemmScalar(c, a, b []float32, m, k, n int) { | |
| if len(c) < m*n || len(a) < m*k || len(b) < k*n { | |
| panic("sgemmScalar: insufficient input slice length") | |
| } | |
| for i := 0; i < m; i++ { | |
| for j := 0; j < n; j++ { | |
| s := float32(0) | |
| for p := 0; p < k; p++ { | |
| s += a[i*k+p] * b[p*n+j] | |
| } | |
| c[i*n+j] = s | |
| } | |
| } | |
| } | |
| // sgemmData returns integer-valued inputs for a(mxk), b(kxn) matrixes. | |
| // | |
| // The test data is integer-valued and small enough that every product and sum is | |
| // exact in float32, so the fused and unfused accumulations agree bit for bit in the scalar impl. | |
| func sgemmData(m, k, n int) (a, b []float32) { | |
| a = make([]float32, m*k) | |
| b = make([]float32, k*n) | |
| for i := range a { | |
| a[i] = float32(i%9) - 4 | |
| } | |
| for i := range b { | |
| b[i] = float32(i%7) - 3 | |
| } | |
| return | |
| } | |
| var sgemmImpls = []struct { | |
| name string | |
| f func(c, a, b []float32, m, k, n int) | |
| }{ | |
| {"simd", sgemmSIMD}, | |
| {"simd2", sgemmSIMD2}, | |
| {"asm", sgemmAsm}, | |
| } | |
| func TestSGEMMSVE(t *testing.T) { | |
| if !archsimd.ARM64.SVE() { | |
| t.Skip("no SVE") | |
| } | |
| vl := archsimd.Float32s{}.Len() | |
| for _, sz := range []struct{ m, k, n int }{ | |
| {7, 37, 64}, // no tail, full alignment to VL | |
| {7, 37, 61}, // aligned + unaligned, triggers tail | |
| {3, 5, 2}, // tail only | |
| } { | |
| m, k, n := sz.m, sz.k, sz.n | |
| a, b := sgemmData(m, k, n) | |
| want := make([]float32, m*n) | |
| sgemmScalar(want, a, b, m, k, n) | |
| for _, impl := range sgemmImpls { | |
| if impl.name == "asm" && n%vl != 0 { | |
| continue // the assembly kernel has no masked tail | |
| } | |
| got := make([]float32, m*n) | |
| impl.f(got, a, b, m, k, n) | |
| for i := range got { | |
| if got[i] != want[i] { | |
| t.Errorf("%s %dx%dx%d: c[%d,%d] = %v, want %v", impl.name, m, k, n, i/n, i%n, got[i], want[i]) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| func BenchmarkSGEMMSVE(b *testing.B) { | |
| if !archsimd.ARM64.SVE() { | |
| b.Skip("no SVE") | |
| } | |
| for _, s := range []int{64, 256} { | |
| m, k, n := s, s, s | |
| am, bm := sgemmData(m, k, n) | |
| c := make([]float32, m*n) | |
| impls := append([]struct { | |
| name string | |
| f func(c, a, b []float32, m, k, n int) | |
| }{{"scalar", sgemmScalar}}, sgemmImpls...) | |
| for _, impl := range impls { | |
| b.Run(fmt.Sprintf("%s/%d", impl.name, s), func(b *testing.B) { | |
| for range b.N { | |
| impl.f(c, am, bm, m, k, n) | |
| } | |
| flops := 2 * float64(m) * float64(k) * float64(n) | |
| b.ReportMetric(flops*float64(b.N)/b.Elapsed().Seconds()/1e9, "GFLOPS") | |
| }) | |
| } | |
| } | |
| } |