import { useMemo, useState } from 'react' import { Box, Button, Checkbox, Chip, FormControl, FormControlLabel, InputLabel, LinearProgress, MenuItem, Select, Stack, Typography, } from '@mui/material' import { jsMatmul } from '../methods/jsMatmul' import { webgpuMatmul } from '../methods/webgpuMatmul' import { wasmNaiveMatmul, wasmBlockedMatmul, wasmBlockedMtMatmul } from '../methods/wasmMatmul' import { blisStMatmul, blisMtMatmul } from '../methods/blisMatmul' import { runInWorker } from '../methods/workerClient' import { runInThreadedWorker } from '../methods/threadedClient' import { generateMatrix } from '../methods/random' import { gflops, type MatmulMethod } from '../methods/types' import { ResultsTable, type CellState } from './ResultsTable' const METHODS: MatmulMethod[] = [ jsMatmul, webgpuMatmul, wasmNaiveMatmul, wasmBlockedMatmul, wasmBlockedMtMatmul, blisStMatmul, blisMtMatmul, ] const ALL_SIZES = [128, 256, 512, 1024, 2048] const HW = typeof navigator !== 'undefined' ? navigator.hardwareConcurrency || 4 : 4 const THREAD_OPTIONS = [...new Set([1, 2, 4, 8, HW])].filter((t) => t <= HW).sort((a, b) => a - b) const isolated = typeof globalThis !== 'undefined' && globalThis.crossOriginIsolated === true const yield_ = () => new Promise((r) => setTimeout(r, 0)) // Deterministic per-size seeds so every method (in this run) multiplies // identical A and B — the `sample` (C[0]) values are then directly comparable. function seedsFor(n: number) { return { seedA: n * 7 + 1, seedB: n * 13 + 2 } } type Results = Record> export function BenchmarkRunner() { const [sizes, setSizes] = useState>(new Set(ALL_SIZES.filter((n) => n <= 1024))) const [threads, setThreads] = useState(Math.min(8, HW)) const [results, setResults] = useState({}) const [running, setRunning] = useState(false) const [progress, setProgress] = useState(0) // Threaded column headers reflect the currently selected thread count: // the word "threaded" in the label becomes "N threads". const displayMethods = useMemo( () => METHODS.map((m) => ( m.label.includes('threaded') ? { ...m, label: m.label.replace('threaded', `${threads} threads`) } : m )), [threads], ) const setCell = (methodId: string, n: number, state: CellState) => { setResults((prev) => ({ ...prev, [methodId]: { ...prev[methodId], [n]: state }, })) } const runOne = async (method: MatmulMethod, n: number) => { if (!method.available()) { setCell(method.id, n, 'unavailable') return } setCell(method.id, n, 'pending') await yield_() try { const { seedA, seedB } = seedsFor(n) let result if (method.threadedKind) { result = await runInThreadedWorker(method.threadedKind, n, seedA, seedB, threads) } else if (method.worker) { result = await runInWorker(method.id, n, seedA, seedB) } else { result = await method.run!(n, generateMatrix(n, seedA), generateMatrix(n, seedB)) } setCell(method.id, n, { gflops: gflops(n, result.ms), ms: result.ms, sample: result.sample }) } catch { setCell(method.id, n, 'error') } } const activeSizes = ALL_SIZES.filter((n) => sizes.has(n)) const runAll = async () => { setRunning(true) const total = activeSizes.length * METHODS.length let done = 0 for (const n of activeSizes) { for (const method of METHODS) { await runOne(method, n) done++ setProgress((done / total) * 100) } } setRunning(false) } const runMethod = async (method: MatmulMethod) => { setRunning(true) const total = activeSizes.length let done = 0 for (const n of activeSizes) { await runOne(method, n) done++ setProgress((done / total) * 100) } setRunning(false) } const toggleSize = (n: number) => { setSizes((prev) => { const next = new Set(prev) if (next.has(n)) next.delete(n) else next.add(n) return next }) } const hasResults = Object.values(results).some( (byN) => byN && Object.values(byN).some((c) => c && typeof c === 'object'), ) // Tidy (long) CSV of every measured cell. Method labels carry the thread // count (from displayMethods), so no separate threads column is needed. const downloadCsv = () => { const esc = (s: string) => (/[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s) const lines = ['n,method,precision,gflops,ms,checksum'] for (const n of ALL_SIZES) { for (const m of displayMethods) { const r = results[m.id]?.[n] if (r && typeof r === 'object') { lines.push([n, esc(m.label), m.precision, r.gflops.toFixed(4), r.ms.toFixed(3), r.sample].join(',')) } } } const blob = new Blob([lines.join('\n')], { type: 'text/csv' }) const a = document.createElement('a') a.href = URL.createObjectURL(blob) a.download = 'matmul-bench-results.csv' a.click() URL.revokeObjectURL(a.href) } return ( Multiplies two random n×n matrices with each method and reports GFLOP/s (2n³ ÷ time). All methods use identical inputs per size — see the cross-check below. WebGPU runs in single precision (f32); every other in-browser method uses double precision (f64). {isolated ? 'WASM threads (SharedArrayBuffer) available — threaded methods enabled.' : 'Threaded methods need SharedArrayBuffer; they will show as n/a here.'} {ALL_SIZES.map((n) => ( toggleSize(n)} disabled={running} />} label={`n=${n}`} /> ))} threads (mt methods) {displayMethods.map((m) => ( ))} {running && } results[methodId]?.[n]} /> Cross-check (C[0] for identical inputs) The f64 methods (JS, the WASM kernels, both BLIS builds) should agree to ~1e-9; WebGPU (f32) will be close but not identical. {activeSizes.map((n) => ( n={n}: {displayMethods.map((m) => { const r = results[m.id]?.[n] const v = r && typeof r === 'object' ? r.sample.toFixed(6) : '—' return `${m.label}=${v}` }).join(' · ')} ))} ) }