/ concept-collection / matmul-bench
Sign in
concept-collection / matmul-bench
matmul-bench / src / components / BenchmarkRunner.tsx
204 lines · 7.4 KBBlameHistoryRaw
1import { useMemo, useState } from 'react'
2import {
3 Box, Button, Checkbox, Chip, FormControl, FormControlLabel, InputLabel,
4 LinearProgress, MenuItem, Select, Stack, Typography,
5} from '@mui/material'
6import { jsMatmul } from '../methods/jsMatmul'
7import { webgpuMatmul } from '../methods/webgpuMatmul'
8import { wasmNaiveMatmul, wasmBlockedMatmul, wasmBlockedMtMatmul } from '../methods/wasmMatmul'
9import { blisStMatmul, blisMtMatmul } from '../methods/blisMatmul'
10import { runInWorker } from '../methods/workerClient'
11import { runInThreadedWorker } from '../methods/threadedClient'
12import { generateMatrix } from '../methods/random'
13import { gflops, type MatmulMethod } from '../methods/types'
14import { ResultsTable, type CellState } from './ResultsTable'
16const METHODS: MatmulMethod[] = [
17 jsMatmul, webgpuMatmul, wasmNaiveMatmul, wasmBlockedMatmul, wasmBlockedMtMatmul,
18 blisStMatmul, blisMtMatmul,
20const ALL_SIZES = [128, 256, 512, 1024, 2048]
22const HW = typeof navigator !== 'undefined' ? navigator.hardwareConcurrency || 4 : 4
23const THREAD_OPTIONS = [...new Set([1, 2, 4, 8, HW])].filter((t) => t <= HW).sort((a, b) => a - b)
24const isolated = typeof globalThis !== 'undefined' && globalThis.crossOriginIsolated === true
26const yield_ = () => new Promise((r) => setTimeout(r, 0))
28// Deterministic per-size seeds so every method (in this run) multiplies
29// identical A and B — the `sample` (C[0]) values are then directly comparable.
30function seedsFor(n: number) {
31 return { seedA: n * 7 + 1, seedB: n * 13 + 2 }
34type Results = Record<string, Record<number, CellState>>
36export function BenchmarkRunner() {
37 const [sizes, setSizes] = useState<Set<number>>(new Set(ALL_SIZES.filter((n) => n <= 1024)))
38 const [threads, setThreads] = useState<number>(Math.min(8, HW))
39 const [results, setResults] = useState<Results>({})
40 const [running, setRunning] = useState(false)
41 const [progress, setProgress] = useState(0)
43 // Threaded column headers reflect the currently selected thread count:
44 // the word "threaded" in the label becomes "N threads".
45 const displayMethods = useMemo<MatmulMethod[]>(
46 () => METHODS.map((m) => (
47 m.label.includes('threaded')
48 ? { ...m, label: m.label.replace('threaded', `${threads} threads`) }
49 : m
50 )),
51 [threads],
52 )
54 const setCell = (methodId: string, n: number, state: CellState) => {
55 setResults((prev) => ({
56 ...prev,
57 [methodId]: { ...prev[methodId], [n]: state },
58 }))
59 }
61 const runOne = async (method: MatmulMethod, n: number) => {
62 if (!method.available()) {
63 setCell(method.id, n, 'unavailable')
64 return
65 }
66 setCell(method.id, n, 'pending')
67 await yield_()
68 try {
69 const { seedA, seedB } = seedsFor(n)
70 let result
71 if (method.threadedKind) {
72 result = await runInThreadedWorker(method.threadedKind, n, seedA, seedB, threads)
73 } else if (method.worker) {
74 result = await runInWorker(method.id, n, seedA, seedB)
75 } else {
76 result = await method.run!(n, generateMatrix(n, seedA), generateMatrix(n, seedB))
77 }
78 setCell(method.id, n, { gflops: gflops(n, result.ms), ms: result.ms, sample: result.sample })
79 } catch {
80 setCell(method.id, n, 'error')
81 }
82 }
84 const activeSizes = ALL_SIZES.filter((n) => sizes.has(n))
86 const runAll = async () => {
87 setRunning(true)
88 const total = activeSizes.length * METHODS.length
89 let done = 0
90 for (const n of activeSizes) {
91 for (const method of METHODS) {
92 await runOne(method, n)
93 done++
94 setProgress((done / total) * 100)
95 }
96 }
97 setRunning(false)
98 }
100 const runMethod = async (method: MatmulMethod) => {
101 setRunning(true)
102 const total = activeSizes.length
103 let done = 0
104 for (const n of activeSizes) {
105 await runOne(method, n)
106 done++
107 setProgress((done / total) * 100)
108 }
109 setRunning(false)
110 }
112 const toggleSize = (n: number) => {
113 setSizes((prev) => {
114 const next = new Set(prev)
115 if (next.has(n)) next.delete(n)
116 else next.add(n)
117 return next
118 })
119 }
121 return (
122 <Stack spacing={2}>
123 <Typography variant="body2" color="text.secondary">
124 Multiplies two random n×n matrices with each method and reports GFLOP/s
125 (2n³ ÷ time). All methods use identical inputs per size — see the
126 cross-check below. <strong>WebGPU runs in single precision (f32)</strong>;
127 every other in-browser method uses double precision (f64).
128 </Typography>
130 <Stack direction="row" spacing={1} sx={{ flexWrap: 'wrap', alignItems: 'center' }}>
131 <Chip
132 size="small"
133 color={isolated ? 'success' : 'default'}
134 label={isolated ? 'cross-origin isolated ✓' : 'not cross-origin isolated'}
135 />
136 <Typography variant="caption" color="text.secondary">
137 {isolated
138 ? 'WASM threads (SharedArrayBuffer) available — threaded methods enabled.'
139 : 'Threaded methods need SharedArrayBuffer; they will show as n/a here.'}
140 </Typography>
141 </Stack>
143 <Stack direction="row" spacing={1} sx={{ flexWrap: 'wrap', alignItems: 'center' }}>
144 {ALL_SIZES.map((n) => (
145 <FormControlLabel
146 key={n}
147 control={<Checkbox size="small" checked={sizes.has(n)} onChange={() => toggleSize(n)} disabled={running} />}
148 label={`n=${n}`}
149 />
150 ))}
151 <FormControl size="small" sx={{ minWidth: 180 }} disabled={running}>
152 <InputLabel id="threads-label">threads (mt methods)</InputLabel>
153 <Select
154 labelId="threads-label"
155 label="threads (mt methods)"
156 value={threads}
157 onChange={(e) => setThreads(Number(e.target.value))}
158 >
159 {THREAD_OPTIONS.map((t) => (
160 <MenuItem key={t} value={t}>{t} thread{t > 1 ? 's' : ''}</MenuItem>
161 ))}
162 </Select>
163 </FormControl>
164 </Stack>
166 <Stack direction="row" spacing={1} sx={{ flexWrap: 'wrap', alignItems: 'center' }}>
167 <Button variant="contained" onClick={runAll} disabled={running || activeSizes.length === 0}>
168 {running ? 'Running…' : 'Run all methods'}
169 </Button>
170 {displayMethods.map((m) => (
171 <Button key={m.id} size="small" variant="outlined" onClick={() => runMethod(m)} disabled={running || activeSizes.length === 0}>
172 Run {m.label}
173 </Button>
174 ))}
175 </Stack>
176 {running && <LinearProgress variant="determinate" value={progress} />}
178 <ResultsTable
179 methods={displayMethods}
180 sizes={activeSizes}
181 cell={(methodId, n) => results[methodId]?.[n]}
182 />
184 <Box>
185 <Typography variant="subtitle2">Cross-check (C[0] for identical inputs)</Typography>
186 <Typography variant="caption" color="text.secondary" component="div" sx={{ mb: 1 }}>
187 The f64 methods (JS, both WASM kernels, both BLIS builds) should agree
188 to ~1e-9; WebGPU (f32) will be close but not identical.
189 </Typography>
190 <Stack spacing={0.5}>
191 {activeSizes.map((n) => (
192 <Typography key={n} variant="caption" component="div">
193 n={n}: {displayMethods.map((m) => {
194 const r = results[m.id]?.[n]
195 const v = r && typeof r === 'object' ? r.sample.toFixed(6) : '—'
196 return `${m.label}=${v}`
197 }).join(' · ')}
198 </Typography>
199 ))}
200 </Stack>
201 </Box>
202 </Stack>
203 )
moveopenescclose