Files
micarrsim/benchmarks/webgpu-scaling.js
T
2026-08-28 11:26:19 +09:00

184 lines
7.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { distance3 } from "/packages/acoustics-core/math.js";
import { synthesizeMicrophoneSignals } from "/packages/acoustics-core/propagation.js";
import { computeDasWebGpu } from "/packages/webgpu-kernels/das.js";
const status = document.querySelector("#benchmark-status");
const output = document.querySelector("#benchmark-output");
const runButton = document.querySelector("#run-benchmark");
const downloadButton = document.querySelector("#download-result");
const query = new URLSearchParams(location.search);
const requestedRuns = Number.parseInt(query.get("runs") ?? "3", 10);
const runCount = Number.isInteger(requestedRuns) ? Math.min(5, Math.max(1, requestedRuns)) : 3;
let latestResult = null;
async function loadJson(path) {
const response = await fetch(path);
if (!response.ok) throw new Error(`${path}를 불러오지 못했습니다 (${response.status}).`);
return response.json();
}
function median(values) {
const sorted = [...values].sort((a, b) => a - b);
const middle = Math.floor(sorted.length / 2);
return sorted.length % 2 === 1 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
}
function round(value, digits = 3) {
const scale = 10 ** digits;
return Math.round(value * scale) / scale;
}
function cloneExperiment(experiment, size) {
return {
...experiment,
id: `single-tone-${size}x${size}`,
scan: { ...experiment.scan, columns: size, rows: size },
beamforming: { ...experiment.beamforming, backend: "webgpu" },
};
}
function downloadJson(value, filename) {
const blob = new Blob([`${JSON.stringify(value, null, 2)}\n`], { type: "application/json" });
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = filename;
anchor.click();
setTimeout(() => URL.revokeObjectURL(url), 0);
}
async function runBenchmark() {
runButton.disabled = true;
downloadButton.disabled = true;
document.documentElement.dataset.benchmarkStatus = "running";
delete document.documentElement.dataset.benchmarkError;
latestResult = null;
globalThis.__micarrsimBenchmarkResult = null;
output.textContent = "";
try {
const [arrayDefinition, scene, baseExperiment] = await Promise.all([
loadJson("/assets/array-geometries/rectangular-16x12.array.json"),
loadJson("/assets/sample-scenes/single-tone.scene.json"),
loadJson("/assets/sample-scenes/single-tone.experiment.json"),
]);
status.textContent = "192채널 마이크 신호를 한 번 합성하고 있습니다…";
const signalStartedAt = performance.now();
const synthesis = synthesizeMicrophoneSignals({ scene, arrayDefinition });
const signalGenerationMs = performance.now() - signalStartedAt;
let adapterInfo = null;
status.textContent = "WebGPU pipeline을 예열하고 있습니다…";
await computeDasWebGpu({
channels: synthesis.channels,
sampleRate: synthesis.sampleRate,
arrayDefinition,
scene,
experiment: cloneExperiment(baseExperiment, 16),
});
const resolutions = [];
for (const size of [50, 100, 200]) {
const samples = [];
for (let runIndex = 0; runIndex < runCount; runIndex += 1) {
status.textContent = `${size}×${size} · ${runIndex + 1}/${runCount} 실행 중…`;
const result = await computeDasWebGpu({
channels: synthesis.channels,
sampleRate: synthesis.sampleRate,
arrayDefinition,
scene,
experiment: cloneExperiment(baseExperiment, size),
onProgress: ({ stage, completedUnits, totalUnits }) => {
if (stage === "beamforming" && totalUnits) {
status.textContent =
`${size}×${size} · ${runIndex + 1}/${runCount} · GPU ${completedUnits}/${totalUnits}`;
}
},
});
adapterInfo = result.gpu.adapterInfo;
const localizationErrorM = distance3(result.bands[0].peak.position, scene.sources[0].position);
samples.push({
totalMs: result.timingMs.total,
stftMs: result.timingMs.stft,
gpuWallMs: result.timingMs.gpuWall,
beamformingMs: result.timingMs.beamforming,
localizationErrorM,
complexMacCount: result.workload.complexMacCount,
estimatedGpuWorkingBytes: result.gpu.batchPlan.estimatedGpuWorkingBytes,
binBatchSize: result.gpu.batchPlan.binBatchSize,
pointBatchSize: result.gpu.batchPlan.pointBatchSize,
});
}
resolutions.push({
grid: `${size}x${size}`,
pointCount: size * size,
runs: runCount,
medianTotalMs: round(median(samples.map((sample) => sample.totalMs))),
medianStftMs: round(median(samples.map((sample) => sample.stftMs))),
medianGpuWallMs: round(median(samples.map((sample) => sample.gpuWallMs))),
medianBeamformingMs: round(median(samples.map((sample) => sample.beamformingMs))),
localizationErrorM: round(median(samples.map((sample) => sample.localizationErrorM)), 6),
complexMacCount: samples[0].complexMacCount,
estimatedGpuWorkingMiB: round(samples[0].estimatedGpuWorkingBytes / 1024 ** 2),
binBatchSize: samples[0].binBatchSize,
pointBatchSize: samples[0].pointBatchSize,
samples: samples.map((sample) => ({
totalMs: round(sample.totalMs),
stftMs: round(sample.stftMs),
gpuWallMs: round(sample.gpuWallMs),
beamformingMs: round(sample.beamformingMs),
})),
});
output.textContent = JSON.stringify(resolutions, null, 2);
}
const baselineGpuMs = resolutions[0].medianGpuWallMs;
for (const resolution of resolutions) {
resolution.gpuWallScalingVs50 = round(resolution.medianGpuWallMs / baselineGpuMs, 2);
}
latestResult = {
schemaVersion: "1.0.0",
kind: "micarrsim.webgpu-benchmark",
createdAt: new Date().toISOString(),
fixture: {
arrayId: arrayDefinition.id,
microphoneCount: arrayDefinition.microphones.length,
sceneId: scene.id,
durationSeconds: scene.duration,
sampleRate: scene.sampleRate,
fftSize: baseExperiment.stft.fftSize,
hopSize: baseExperiment.stft.hopSize,
selectedBandHz: baseExperiment.frequencyBands.map(({ minHz, maxHz }) => [minHz, maxHz]),
},
environment: {
userAgent: navigator.userAgent,
adapterInfo,
timingKind: "wall-clock",
note: "GPU wall time starts after host packing/writeBuffer calls and waits for queued transfers, two compute passes, and readback; total also includes device/pipeline setup and CPU STFT.",
},
signalGenerationMs: round(signalGenerationMs),
warmupGrid: "16x16",
resolutions,
};
output.textContent = JSON.stringify(latestResult, null, 2);
globalThis.__micarrsimBenchmarkResult = latestResult;
document.documentElement.dataset.benchmarkStatus = "pass";
status.textContent = "benchmark 완료";
downloadButton.disabled = false;
} catch (error) {
document.documentElement.dataset.benchmarkStatus = "fail";
document.documentElement.dataset.benchmarkError = error.message;
status.textContent = `benchmark 실패: ${error.message}`;
output.textContent = error instanceof Error ? error.stack : String(error);
throw error;
} finally {
runButton.disabled = false;
}
}
runButton.addEventListener("click", () => runBenchmark().catch(() => {}));
downloadButton.addEventListener("click", () => {
if (latestResult) downloadJson(latestResult, "micarrsim-webgpu-benchmark.json");
});
if (query.get("autorun") !== "0") runBenchmark().catch(() => {});