/ concept-collection / matmul-bench
concept-collection / matmul-bench
matmul-bench / src / components / BenchmarkRunner.tsx
241 lines · 8.8 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 const hasResults = Object.values(results).some(
122 (byN) => byN && Object.values(byN).some((c) => c && typeof c === 'object'),
123 )
125 // Tidy (long) CSV of every measured cell. Method labels carry the thread
126 // count (from displayMethods), so no separate threads column is needed.
127 const downloadCsv = () => {
128 const esc = (s: string) => (/[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s)
129 const lines = ['n,method,precision,gflops,ms,checksum']
130 for (const n of ALL_SIZES) {
131 for (const m of displayMethods) {
132 const r = results[m.id]?.[n]
133 if (r && typeof r === 'object') {
134 lines.push([n, esc(m.label), m.precision, r.gflops.toFixed(4), r.ms.toFixed(3), r.sample].join(','))
135 }
136 }
137 }
138 const blob = new Blob([lines.join('\n')], { type: 'text/csv' })
139 const a = document.createElement('a')
140 a.href = URL.createObjectURL(blob)
141 a.download = 'matmul-bench-results.csv'
142 a.click()
143 URL.revokeObjectURL(a.href)
144 }
146 return (
147 <Stack spacing={2}>
148 <Typography variant="body2" color="text.secondary" sx={{ maxWidth: 900 }}>
149 Multiplies two random n×n matrices with each method and reports GFLOP/s
150 (2n³ ÷ time). All methods use identical inputs per size — see the
151 cross-check below. <strong>WebGPU runs in single precision (f32)</strong>;
152 every other in-browser method uses double precision (f64).
153 </Typography>
155 <Stack direction="row" spacing={1} sx={{ flexWrap: 'wrap', alignItems: 'center' }}>
156 <Chip
157 size="small"
158 color={isolated ? 'success' : 'default'}
159 label={isolated ? 'cross-origin isolated ✓' : 'not cross-origin isolated'}
160 />
161 <Typography variant="caption" color="text.secondary">
162 {isolated
163 ? 'WASM threads (SharedArrayBuffer) available — threaded methods enabled.'
164 : 'Threaded methods need SharedArrayBuffer; they will show as n/a here.'}
165 </Typography>
166 </Stack>
168 <Stack direction="row" spacing={1} sx={{ flexWrap: 'wrap', alignItems: 'center' }}>
169 {ALL_SIZES.map((n) => (
170 <FormControlLabel
171 key={n}
172 control={<Checkbox size="small" checked={sizes.has(n)} onChange={() => toggleSize(n)} disabled={running} />}
173 label={`n=${n}`}
174 />
175 ))}
176 <FormControl size="small" sx={{ minWidth: 180 }} disabled={running}>
177 <InputLabel id="threads-label">threads (mt methods)</InputLabel>
178 <Select
179 labelId="threads-label"
180 label="threads (mt methods)"
181 value={threads}
182 onChange={(e) => {
183 // The measured mt numbers no longer match the new thread count
184 // (and the column headers would relabel), so drop stale results.
185 setThreads(Number(e.target.value))
186 setResults({})
187 setProgress(0)
188 }}
189 >
190 {THREAD_OPTIONS.map((t) => (
191 <MenuItem key={t} value={t}>{t} thread{t > 1 ? 's' : ''}</MenuItem>
192 ))}
193 </Select>
194 </FormControl>
195 </Stack>
197 <Stack direction="row" spacing={1} sx={{ flexWrap: 'wrap', alignItems: 'center' }}>
198 <Button variant="contained" onClick={runAll} disabled={running || activeSizes.length === 0}>
199 {running ? 'Running…' : 'Run all methods'}
200 </Button>
201 {displayMethods.map((m) => (
202 <Button key={m.id} size="small" variant="outlined" onClick={() => runMethod(m)} disabled={running || activeSizes.length === 0}>
203 Run {m.label}
204 </Button>
205 ))}
206 </Stack>
207 {running && <LinearProgress variant="determinate" value={progress} />}
209 <ResultsTable
210 methods={displayMethods}
211 sizes={activeSizes}
212 cell={(methodId, n) => results[methodId]?.[n]}
213 />
215 <Box>
216 <Button size="small" variant="outlined" onClick={downloadCsv} disabled={!hasResults}>
217 Download CSV
218 </Button>
219 </Box>
221 <Box>
222 <Typography variant="subtitle2">Cross-check (C[0] for identical inputs)</Typography>
223 <Typography variant="caption" color="text.secondary" component="div" sx={{ mb: 1 }}>
224 The f64 methods (JS, the WASM kernels, both BLIS builds) should agree
225 to ~1e-9; WebGPU (f32) will be close but not identical.
226 </Typography>
227 <Stack spacing={0.5}>
228 {activeSizes.map((n) => (
229 <Typography key={n} variant="caption" component="div">
230 n={n}: {displayMethods.map((m) => {
231 const r = results[m.id]?.[n]
232 const v = r && typeof r === 'object' ? r.sample.toFixed(6) : '—'
233 return `${m.label}=${v}`
234 }).join(' · ')}
235 </Typography>
236 ))}
237 </Stack>
238 </Box>
239 </Stack>
240 )