CPU vs GPU workload placement experiment — determining where a data engineering pipeline runs faster by benchmarking identical workloads on both processing units in the browser.
This experiment runs a four-stage data pipeline — filter, normalize, transform, aggregate — on both the CPU (single-threaded TypeScript) and the GPU (WebGPU via TypeGPU), then compares their performance across different input sizes. The goal is to find the crossover point where GPU parallelism overcomes the overhead of data transfer.
The same synthetic dataset is fed into two parallel paths. Both produce per-category aggregates (count, sum, mean, variance), which are then compared for correctness. Timing is recorded independently for each path, with GPU timings broken down into write, compute, and readback phases.
| Stage | CPU Implementation | GPU Implementation |
|---|---|---|
| Filter | Sequential scan, compacted index list | Parallel flag array (1/0 per record) |
| Normalize | Two-pass min/max, then (v - min) / range |
Atomic min/max via IEEE 754 bitcast trick |
| Transform | sin(norm * PI) * exp(-norm) + log(1 + norm) in f64 |
Same formula in f32 (GPU has no f64) |
| Aggregate | Direct f64 accumulation per category | Fixed-point integer atomics (scale = 100) |
- No WGSL written by hand. GPU kernels are authored as TypeScript functions with a
'use gpu'directive. The TypeGPU build plugin transpiles them to WGSL at build time. - Fixed-point aggregation. WGSL lacks
f32atomics, so sums are scaled by 100, truncated tou32, and accumulated withatomicAdd. This introduces quantization error (~2 decimal places). - Bitcast atomic trick. For non-negative floats, IEEE 754 bit patterns preserve ordering when reinterpreted as
u32, enablingatomicMin/atomicMaxfor the normalization bounds. - Verification with tolerance. Counts must match exactly. Means and variances are compared with 2% relative error tolerance to account for f32 vs f64 precision and fixed-point quantization.
Results from an Apple M-series GPU (Metal 3) via Chrome WebGPU:
Key observations:
- At 10K records, the CPU wins — GPU overhead (buffer creation, dispatch, readback) dominates.
- The crossover occurs around 100K records, where GPU compute starts to offset transfer costs.
- At 1M+ records, the GPU pulls ahead significantly (3.5-4.9x faster).
- At 10M records, a count mismatch appears in verification — this is a known limitation of the current buffer size handling on some WebGPU implementations.
src/
main.ts Entry point — WebGPU check, UI mount
types.ts Shared constants, interfaces, GPU struct schemas
generate.ts Deterministic synthetic data generation (seeded PRNG)
cpu/pipeline.ts CPU benchmark path (sequential TypeScript)
gpu/pipeline.ts GPU benchmark path (6 WebGPU compute kernels)
bench/runner.ts Orchestrator — warmup, trials, timing, verification
bench/timing.ts Median calculation for timing arrays
verify.ts CPU vs GPU correctness comparison
results.ts JSON export / download
ui.ts Browser UI (vanilla DOM)
- Node.js 18+
- A browser with WebGPU support (Chrome 113+, Edge 113+)
npm install
npm run devOpen the local URL shown by Vite. Select an input size and click Run Benchmark or Run All Sizes.
npm run build
npm run preview- CPU:
performance.now()wraps all four pipeline stages. Median of 10 trials after 3 warmup runs. - GPU: Split into two segments:
writeMs— buffer creation + CPU-to-GPU data transfercomputeAndReadMs— kernel dispatch + execution + GPU-to-CPU readbacktotalMs— sum of both
- Data generation is not timed — the same
ArrayBufferis reused across all trials.