| // 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") |
| }) |
| } |
| } |
| } |