· 2 min read

Lapassay: Windows laptops lie about throttling

Notes from building a CPU+GPU benchmark that scores sustained perf instead of the first-second peak.

Windows benchmarks usually run hot for ten seconds and post the peak. Fine on a desktop. On a laptop it tells you almost nothing — the real question is whether the chassis can hold that score for five minutes, and the answer is usually no.

So I wrote Lapassay. CPU + GPU + telemetry, JSON in, JSON out, an Avalonia GUI for when I don’t feel like typing flags.

The CPU side is twelve workloads picked to hit different parts of the SoC — SGEMM, AES-NI, SHA-NI, Zstd-3, complex FFT, vectorized Mandelbrot, STREAM Triad, pointer-chase latency, plus a 1→N scaling sweep that produces a single “efficiency at full cores” percentage. The GPU side is FP32 and FP16 D3D12 compute matmul (half the laptops I test claim FP16 support and lie about it), plus a real AI workload via ONNX Runtime + DirectML.

There’s a sustained mode too: runs SGEMM in a loop for whatever duration you give it and reports first-window vs last-window. That’s the throttle number — the one I actually look at.

Two Windows settings matter more than anything in the benchmark code. Run as admin, so LibreHardwareMonitor can read RAPL MSRs. Enable Developer Mode, so ID3D12Device::SetStablePowerState does anything at all — without it the GPU clocks drift ±10–30 % from DVFS and every comparison is noise. There’s no API around it; Microsoft gated stable-power-state behind dev mode in Windows 10 1709 and never opened a side door. The tool runs without these but warns hard.

The unfun side quest: LibreHardwareMonitor reads CPU MSRs through a kernel driver called WinRing0. Same primitive is in some crypto miners, so some AV products quarantine it on first run. It’s signed and legitimate, but if your AV eats it the telemetry rows go blank — benchmarks still complete. Nothing I can fix from inside the app, so it’s a README note.

If I were starting today I’d write the GPU layer in D3D11. The D3D12 timestamp queries are nice, but the boilerplate to get one compute shader running was about 4× heavier than a tool this size needed. I picked D3D12 because I was already in the docs and it felt cleaner. It wasn’t worth it.

← all notes