A team sizing a browser inference path pulls a CPU spec sheet, reads a peak GFLOPS figure, and budgets latency from it. The model runs three-to-ten times slower than that number implies. The spec was not wrong — it was measuring something the WASM path can never reach. This is the most common misread we see when a workload moves off a native server and into the browser via Pyodide or a WASM runtime. The GFLOPS number on the spec sheet is a real, defensible figure. It just answers a different question than “how fast will my model run here.” Used correctly, CPU GFLOPS sets an upper bound and tells you whether a workload is even compute-bound. Used as a latency predictor, it produces budgets that miss by a wide margin. What does GFLOPS on a CPU actually mean? GFLOPS is billions of floating-point operations per second. The theoretical peak for a CPU is a multiplication, not a measurement: peak GFLOPS = cores × clock frequency (GHz) × FLOPs per cycle The last term is where most of the confusion lives. FLOPs per cycle is set by the SIMD width and how many vector units a core can issue per cycle. A core with AV2 (256-bit) fused-multiply-add can retire 16 single-precision FLOPs per FMA, and modern cores issue two FMA units per cycle — so 32 FP32 FLOPs per cycle before you count the core loop overhead. Widen to AVX-512 and that doubles again. The difference SIMD width makes to a ported inference path is exactly this multiplier, and it is why the same clock speed produces wildly different peak figures across CPU generations. So a rough worked example. Take an 8-core CPU at 3.5 GHz with AVX2 (32 FP32 FLOPs/cycle, two FMAs): 8 × 3.5 × 32 ≈ 896 GFLOPS theoretical FP32 peak. That figure is honest as a ceiling. It is also completely unreachable by any real inference workload, and the gap widens further the moment the code runs in a browser. This is the divergence point the rest of the article is about. Why is sustained GFLOPS so much lower than the peak? Theoretical peak assumes every core is issuing two fully-packed FMA instructions every single cycle with no stalls — no cache misses, no branch mispredictions, no time spent moving data. Real code never gets there. In our experience profiling ported inference paths, sustained achievable GFLOPS on a well-vectorized native kernel typically lands somewhere between 30% and 70% of theoretical peak, and only the most cache-friendly, hand-tuned GEMM kernels reach the top of that band (observed pattern across TechnoLynx porting engagements; not a published benchmark). Three things drain the gap: Vectorization. If the runtime cannot emit SIMD instructions, you are computing one FLOP per cycle per core instead of 16 or 32. That alone is a 16–32× cliff before anything else. Memory bandwidth. A matrix multiply that does not fit in cache is fed by DRAM. Once the arithmetic intensity (FLOPs per byte moved) drops below what the memory subsystem can sustain, the cores stall waiting for data and the FLOP counter idles. Threading. Peak assumes all cores are busy. A single-threaded path leaves 7 of 8 cores idle, capping you at one-eighth of peak regardless of vectorization. The reason this matters is that the failure class — budgeting latency off peak GFLOPS — sets a target the hardware was never going to hit, and the miss shows up late, in production, when the browser tab feels sluggish. How WASM and Pyodide shrink the usable GFLOPS Everything above is true for native code. The WASM path makes it worse, and this is the specific failure the spec-sheet approach walks into. Under Pyodide, CPython itself is compiled to WebAssembly. The interpreter overhead you already pay in native Python is still there, and now it runs inside a WASM runtime that has historically had limited access to the CPU’s native SIMD units and threading. WebAssembly SIMD is a fixed 128-bit width — narrower than AVX2’s 256-bit and far narrower than AVX-512 — so even when the runtime vectorizes, the FLOPs-per-cycle multiplier is smaller than the native peak assumed. Threading depends on SharedArrayBuffer and cross-origin isolation being available and enabled, which is not a given in every deployment target. The practical consequence: the achievable GFLOPS under Pyodide is a fraction of the native peak, and that fraction is not a fixed discount you can memorize. It depends on whether the specific NumPy or ONNX kernel your model leans on was compiled with WASM SIMD, whether threads are available, and how much time is lost to the Python interpreter versus the numeric kernels. This is why CPU GFLOPS matters most when profiling a Pyodide path against a native one — the compiler flags you build the WASM artifact with directly change how much of that theoretical peak survives into the browser. None of this means CPU GFLOPS is useless for a WASM plan. It means you use it for what it can do: bound the problem. Using GFLOPS to estimate a latency ceiling The one calculation GFLOPS is genuinely good for is a floor on inference time — the fastest the model could possibly run if the hardware hit its sustained rate. You need two numbers: the model’s FLOPs per inference (from a profiler, or estimated from the architecture) and a sustained GFLOPS figure you actually measured on the target. latency floor (seconds) ≈ FLOPs per inference ÷ (sustained GFLOPS × 10⁹) Worked example, with explicit assumptions. Say a model does 2 GFLOP per inference (2 × 10⁹ floating-point ops), and you measured — not read off a spec sheet — a sustained 120 GFLOPS on your target CPU running the native path: 2 × 10⁹ ÷ (120 × 10⁹) ≈ 17 ms compute floor, native. Now the same model under Pyodide, where you measured sustained throughput at 25 GFLOPS because WASM SIMD is narrower and threading was unavailable: 2 × 10⁹ ÷ (25 × 10⁹) ≈ 80 ms compute floor, WASM. Those are floors, not predictions — the real latency includes data marshalling, the Python interpreter, and I/O, so it will be higher. But the gap between 17 ms and 80 ms is the decision-grade signal: if your latency target is 50 ms, the WASM path is out on compute alone, and no amount of tuning the surrounding code will save it. That is the kind of ceiling that belongs in a release-readiness gate for any inference path, including a browser deployment. Quick reference: peak vs sustained vs WASM-achievable Metric What it tells you How you get it Use it for Theoretical peak GFLOPS Absolute upper bound cores × GHz × FLOPs/cycle Sanity ceiling; is the workload plausibly compute-bound? Sustained native GFLOPS Realistic native rate Measured (e.g. a GEMM microbenchmark) Native latency floor Sustained WASM/Pyodide GFLOPS What the browser path can use Measured under Pyodide with SIMD/threading as-deployed WASM latency floor; go/no-go on the browser path Model FLOPs per inference Work the model demands Profiler or architecture estimate Numerator of the latency-floor calculation When is a workload memory-bound instead of GFLOPS-bound? There is a second failure hiding behind the GFLOPS number: sometimes the CPU is not the bottleneck at all. If your model’s arithmetic intensity is low — few FLOPs per byte of data moved — then the cores finish their math and sit idle waiting for the memory subsystem. Adding compute (more cores, wider SIMD, a faster clock) buys you nothing, because the wall is bandwidth, not FLOPs. The tell is that sustained GFLOPS stays flat well below peak no matter how you tune the kernel, while memory bandwidth counters sit near their ceiling. Small batch sizes, embedding lookups, and elementwise-heavy graphs frequently land here. When they do, the whole GFLOPS conversation is the wrong frame — you are looking at where the inference cost actually lives, and it is in data movement, not arithmetic. Knowing which regime you are in before you commit to a WASM port is the difference between a fix that works and a month spent optimizing a term that was never the constraint. FAQ How does gflops cpu work? GFLOPS on a CPU is billions of floating-point operations per second. The theoretical peak is a product of cores, clock frequency, and FLOPs per cycle (set by SIMD width), so it describes the maximum arithmetic the hardware could ever do — not what your model will achieve. In practice it works as a ceiling and a compute-bound test, not a latency predictor. How do you calculate theoretical peak GFLOPS from cores, clock frequency, and SIMD width? Multiply cores × clock frequency in GHz × FLOPs per cycle. The FLOPs-per-cycle term comes from the SIMD width and the number of vector FMA units a core can issue: AVX2 gives roughly 32 FP32 FLOPs per cycle with two FMAs, AVX-512 doubles that. An 8-core, 3.5 GHz, AVX2 CPU works out to roughly 896 GFLOPS theoretical FP32 peak. Why is sustained achievable GFLOPS so much lower than theoretical peak, and what limits it? Peak assumes every core issues fully-packed FMAs every cycle with no stalls, which real code never sustains. Vectorization gaps, memory-bandwidth limits, and idle cores from single-threaded execution each drain the gap. Well-vectorized native kernels typically reach only 30–70% of peak in our porting engagements (observed pattern, not a published benchmark). How does GFLOPS relate to a model’s FLOPs-per-inference when estimating latency? Divide the model’s FLOPs per inference by the sustained GFLOPS rate (times 10⁹) to get a compute-time floor — the fastest the model could run if the CPU held its measured rate. A 2 GFLOP model at a measured 120 sustained GFLOPS gives roughly a 17 ms floor. Real latency is higher because it adds data movement, interpreter, and I/O; the floor is a go/no-go bound, not a prediction. Why does CPU GFLOPS matter when profiling a Pyodide/WASM inference path against a native one? Because the achievable GFLOPS differs sharply between the two, and the difference decides whether the browser path can meet a latency target. Measuring sustained GFLOPS on both paths turns “how fast will this run” into two concrete latency floors you can compare against your budget before committing to a port. How do WASM’s SIMD and threading limits reduce the GFLOPS you can actually use? WebAssembly SIMD is a fixed 128-bit width, narrower than AVX2 (256-bit) and AVX-512, so the FLOPs-per-cycle multiplier is smaller even when the runtime vectorizes. Threading depends on SharedArrayBuffer and cross-origin isolation being enabled, which is not guaranteed. Under Pyodide, CPython-in-WASM also carries interpreter overhead, so achievable GFLOPS is a fraction of native peak. When does a workload become memory-bandwidth-bound rather than GFLOPS-bound? When arithmetic intensity is low — few FLOPs per byte moved — the cores finish their math and stall waiting on the memory subsystem, so adding compute buys nothing. The tell is sustained GFLOPS staying flat well below peak while bandwidth counters sit near their ceiling. Small batches, embedding lookups, and elementwise-heavy graphs often land here. The question worth asking before the port The useful reframe is not “how many GFLOPS does this CPU have” but “how much of that peak survives into the browser, and is what survives enough.” Answer it by measuring sustained throughput on the actual WASM artifact, deriving a latency floor, and checking whether the workload is compute-bound or bandwidth-bound in the first place. That profiling pass — peak-vs-sustained on the target, model FLOPs-per-inference, and the derived ceiling — is the same baseline the Inference Cost-Cut Pack uses to judge whether a Pyodide path moves the bottleneck or just relocates it. Skip it, and the spec sheet will quietly write a latency budget the browser was never going to keep.