go_matmul_perf/src/main.go
2026-03-15 12:16:22 +01:00

232 lines
5.1 KiB
Go

package main
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/james-bowman/sparse"
"gonum.org/v1/gonum/mat"
)
var gNumTestIterations = 30
type SparseMatrixTiming struct {
Label string `json:"label"`
Rows int `json:"rows"`
Cols int `json:"cols"`
NNZ int `json:"nnz"`
MatMulRuns int `json:"matmul_runs"`
MatMulTotalNs int64 `json:"matmul_total_ns"`
MatMulAvgNs int64 `json:"matmul_avg_ns"`
SpMVRuns int `json:"spmv_runs"`
SpMVTotalNs int64 `json:"spmv_total_ns"`
SpMVAvgNs int64 `json:"spmv_avg_ns"`
}
type SparseBenchmarkCase struct {
Timing SparseMatrixTiming
Matrix *sparse.CSR
}
// The "SuiteSparse" collection of matrices come in a format called
// "market matrix" and so we parse and load them to a sparse.COO format here.
func matrixLoadMarket(path string) *sparse.CSR {
f, err := os.Open(path)
if err != nil {
panic(err)
}
defer f.Close()
scanner := bufio.NewScanner(f)
// skip header
scanner.Scan()
// skip comments
for scanner.Scan() {
line := scanner.Text()
if !strings.HasPrefix(line, "%") {
break
}
}
fields := strings.Fields(scanner.Text())
rows, _ := strconv.Atoi(fields[0])
cols, _ := strconv.Atoi(fields[1])
nnz, _ := strconv.Atoi(fields[2])
r := make([]int, nnz)
c := make([]int, nnz)
v := make([]float64, nnz)
i := 0
for scanner.Scan() {
f := strings.Fields(scanner.Text())
ri, _ := strconv.Atoi(f[0])
ci, _ := strconv.Atoi(f[1])
val, _ := strconv.ParseFloat(f[2], 64)
r[i] = ri - 1
c[i] = ci - 1
v[i] = val
i++
}
coo := sparse.NewCOO(rows, cols, r, c, v)
return coo.ToCSR()
}
// small helper just to strip path and file ending
func matrixGetLabel(path string) string {
base := filepath.Base(path) // f. ex "1138_bus.mtx"
ext := filepath.Ext(base) // ".mtx"
out := strings.TrimSuffix(base, ext) // just "1138_bus"
return out
}
func getSparseBenchmarkCase(path string) SparseBenchmarkCase {
var out SparseBenchmarkCase
loadMarketStart := time.Now()
label := matrixGetLabel(path)
m := matrixLoadMarket(path)
loadMarketEnd := time.Since(loadMarketStart)
loadedMarketMillisecs := float64(loadMarketEnd.Milliseconds())
fmt.Printf("Loaded market matrix %s in %.4f ms \n", label, loadedMarketMillisecs)
out.Matrix = m
out.Timing.Label = label
rows, cols := out.Matrix.Dims()
nnz := m.NNZ()
out.Timing.Rows = rows
out.Timing.Cols = cols
out.Timing.NNZ = nnz
return out
}
func timeSparseMatmuls(bcase *SparseBenchmarkCase) {
mCSR := bcase.Matrix
var warm sparse.CSR
for i := 0; i < 3; i += 1 {
warm.Mul(mCSR, mCSR)
}
// The CSR x CSR matrix multiplication is supposed to have a specific optimised
// path for matmuls
var out sparse.CSR
numberOfMults := gNumTestIterations
fmt.Printf("NNZ before matmuls: %d\n", mCSR.NNZ())
timeBegin := time.Now()
for i := 0; i < numberOfMults; i += 1 {
out.Mul(mCSR, mCSR)
}
timeElapsed := time.Since(timeBegin)
fmt.Printf("NNZ after matmuls: %d\n", out.NNZ())
if warm.NNZ() != out.NNZ() {
panic("Sparsity pattern changed unexpectedly after matmul!")
}
bcase.Timing.MatMulRuns = numberOfMults
bcase.Timing.MatMulTotalNs = timeElapsed.Nanoseconds()
timeAvgNS := timeElapsed.Nanoseconds() / int64(numberOfMults)
bcase.Timing.MatMulAvgNs = timeAvgNS
}
func timeSparseMatVec(bcase *SparseBenchmarkCase) {
A := bcase.Matrix
rows, cols := A.Dims()
x := mat.NewVecDense(cols, nil)
y := mat.NewVecDense(rows, nil)
for i := 0; i < cols; i++ {
x.SetVec(i, 1.0)
}
// warmup
A.MulVecTo(y.RawVector().Data, false, x.RawVector().Data)
norm := y.Norm(2)
if norm < 1e-12 {
panic("Norm of resulting SpMV is zero. Something went wrong.\n")
}
numberOfRuns := gNumTestIterations
timeBegin := time.Now()
for i := 0; i < numberOfRuns; i++ {
// fmt.Printf("spmv iteration %d\n", i)
A.MulVecTo(y.RawVector().Data, false, x.RawVector().Data)
}
timeElapsed := time.Since(timeBegin)
total := timeElapsed.Nanoseconds()
avg := total / int64(numberOfRuns)
bcase.Timing.SpMVTotalNs = total
bcase.Timing.SpMVAvgNs = avg
bcase.Timing.SpMVRuns = numberOfRuns
}
func timeNanoToMS(timeNS int64) float64 {
return float64(timeNS) / float64(1e6)
}
func doTimings(path string) {
bcase := getSparseBenchmarkCase(path)
rows := bcase.Timing.Rows
cols := bcase.Timing.Cols
if rows != cols {
panic("Sparse benchmark assumes square matrix")
}
doMatMul := true
if doMatMul {
fmt.Printf("Timing sparse matrix %s with size: %d x %d \n", bcase.Timing.Label, rows, cols)
fmt.Printf("Matmul:\n")
timeSparseMatmuls(&bcase)
avgMatMulTimeMS := timeNanoToMS(bcase.Timing.MatMulAvgNs)
fmt.Printf("Avg matmul time for %s: %.4f ms \n", bcase.Timing.Label, avgMatMulTimeMS)
}
doSpMV := true
if doSpMV {
fmt.Printf("SpMV:\n")
timeSparseMatVec(&bcase)
avgSpMVTimeMS := timeNanoToMS(bcase.Timing.SpMVAvgNs)
fmt.Printf("Avg SpMV time for %s: %.4f ms\n", bcase.Timing.Label, avgSpMVTimeMS)
}
}
func main() {
mainBegin := time.Now()
thermal2Path := "suitesparse_test_matrices/thermal2.mtx"
doTimings(thermal2Path)
mainElapsed := time.Since(mainBegin)
fmt.Printf("Program finished in %.4f seconds \n", float64(mainElapsed.Milliseconds())/1e3)
}