A zero-knowledge proof verifies in milliseconds but takes half a minute to GENERATE, because proving means millions of operations on 256–753-bit integers — GZKP rebuilds both prover stages for the GPU (an NTT that never shuffles global memory, an MSM that merges every window into one bucket set, and a big-integer library running in double-precision floats) for an order-of-magnitude speedup over the best prior GPU provers.
The argument in one glance
-
Problem
Proving takes 30 seconds; verifying takes 10 milliseconds
A zero-knowledge proof lets you convince anyone a computation was done correctly without revealing its private inputs. The zkSNARK flavor makes the RECEIVING end nearly free — proofs under a kilobyte, verified in under 10 ms — but the SENDING end pays for that luxury. Generating one proof for a private Zcash transaction takes more than 30 seconds on a CPU, because it grinds through millions of arithmetic operations on integers 256 to 753 bits wide, where a single modular multiplication costs around 230 nanoseconds.
-
What people did
Put the two hot kernels on a GPU
Proof generation boils down to two stages: a POLY stage that runs a series of number-theoretic transforms (NTTs — FFTs over a finite field), and an MSM stage that computes huge inner products of scalars and elliptic-curve points, typically over 70% of CPU proving time. Both look embarrassingly parallel, so industry systems appeared: MINA moved MSM to the GPU, and Filecoin's bellperson moved both stages.
-
Why it fell short
753-bit numbers break the textbook parallelizations
GPU FFT tradition shuffles data between iterations so each thread reads contiguous memory — cheap for 8-byte doubles, but with up to 96-byte field elements the shuffling itself consumes 42–81% of each NTT batch's time. MSM's standard Pippenger algorithm ends in a window-reduction step that is inherently serial, its natural preprocessing wants over 5 GB of GPU memory, and real workloads' sparse scalars leave some buckets 2.85× heavier than average, idling most warps. Net effect: MINA barely beat a well-tuned CPU end to end.
-
This paper's idea
Redesign both kernels around what huge integers do to a GPU
GZKP never shuffles global memory in the NTT (small independent groups per thread block, reordered internally through shared memory, big integers stored column-major), and merges every MSM window into ONE bucket set using precomputed weighted points — eliminating the serial reduction and enabling fine-grained load balancing — all on a finite-field library that packs 52-bit limbs into double-precision floats. End to end: on average 48.1× faster than the best CPU prover and 33.6× faster than the best GPU prover on 753-bit workloads.
Why this problem is worth solving
A a cryptographic protocol in which a
prover convinces a verifier that a statement is true — “this
transaction is valid,” “this computation was done correctly” —
without revealing any of the private data behind it
Succinct
Non-interactive ARgument of Knowledge — the practical ZKP family used
by Zcash, Filecoin, and most blockchain privacy systems; “succinct”
means the proof is tiny and fast to check regardless of how big the
proven computation was
But the cost didn’t vanish — it moved to the prover. Every private
Zcash transaction requires its sender to generate a proof, and that
takes over 30 seconds of CPU time. Filecoin’s storage proofs, private
smart contracts, verifiable databases, even proofs about machine-learning
predictions all sit behind the same bottleneck. The arithmetic is
brutal by construction: zkSNARK security rests on a number system with a fixed prime modulus where addition and
multiplication wrap around — all zkSNARK arithmetic happens on
integers modulo a prime that is 256 to 753 bits long, far beyond what
any CPU or GPU register holds natively
What people did before, and where it broke
Proof generation has exactly two expensive stages. The POLY stage
runs a series of number-theoretic transform — the FFT’s
finite-field twin. Same butterfly structure and O(N log N) cost as an
FFT, but every element is a huge modular integer instead of a
complex number
multi-scalar multiplication — the inner
product Σ sᵢ⊗Pᵢ of a vector of scalars with a vector of
elliptic-curve points, where ⊗ means adding a point to itself sᵢ
times. Five of these per proof, and over 70% of CPU proving
time
Both kernels have decades of parallel-computing literature behind them, and the existing GPU provers imported it directly. That is where things broke:
- NTT: the shuffle that ate the kernel. A GPU wants the 32
threads of a to read consecutive addresses, but the NTT’s butterfly pattern reads elements a large stride apart. The classical fix — physically shuffle the array between iteration batches so the next batch reads contiguously — is nearly free for 8-byte floats. With 381-bit or 753-bit elements (48–96 bytes each), the shuffle is itself a giant data movement: measured in bellperson, shuffle steps consume 42–81% of each batch’s time. Skipping the shuffle is no better — the strided accesses it prevents grow from 13% to 53% of runtime between consecutive batches at scale 2²⁴.
the GPU’s scheduling unit — 32 threads that execute in lockstep and achieve full memory bandwidth only when they access consecutive addresses (a “coalesced” access)
- MSM: a serial tail and unbalanced buckets. Every fast MSM uses
. On a GPU the point-merging phase parallelizes well, but the final window-reduction is a dependency chain that must run serially — bellperson ships it back to the CPU. Worse, real proof workloads have sparse scalar vectors (mostly zeros and ones, an artifact of how circuits encode bound checks), so bucket sizes vary wildly: the heaviest bucket holds 2.85× the average, and every warp waits for the slowest one. This is why MINA’s GPU barely improved on libsnark’s CPU end to end.
the classic trick for computing Σ sᵢ⊗Pᵢ — slice each scalar into windows of k bits, and within each window throw points into 2ᵏ−1 “buckets” by their k-bit digit. Sum each bucket, weight buckets by their digit (bucket-reduction), then combine window results with repeated doubling (window-reduction). It replaces per-scalar doubling chains with mostly plain point additions
- Big-integer arithmetic itself. GPUs provision far more floating-point than integer multiply throughput, yet every prior prover ran its field arithmetic on the integer units, with each 753-bit number spread across 24 thirty-two-bit words.
How GZKP generates a proof
GZKP's proof-generation pipeline
-
Witness in
An application framework (e.g. xJsnark) compiles the private computation into a circuit and produces the scalar vectors the prover consumes. GZKP slots in below this as a drop-in proving backend.
-
POLY: shuffle-less NTT new
Data stays in its global-memory order for the entire transform. Each thread block takes G ≥ 4 SMALL independent groups instead of one big one; the block reads G strided chunks, transposes them inside 48 KB shared memory (the "internal shuffle"), and works on contiguous data from then on. Big integers are stored column-major so a warp reading limb j of 32 consecutive elements touches consecutive addresses.
-
MSM offline: weighted points new
Once per circuit, GZKP precomputes weighted copies of each base point (2^tk ⊗ Pᵢ for window offsets t) at checkpoint intervals of M windows — Algorithm 1 tunes M so the table fits any GPU's memory, trading at most (M−1)·k extra point-additions per element.
-
MSM online: consolidated buckets new
Using the weighted points, a digit landing in bucket b of ANY window is added to the SINGLE shared bucket b. Point-merging — 90% of MSM time — becomes one big, uniform accumulation, and the serial window-reduction step disappears entirely.
-
Load-balanced finish new
Buckets are grouped into similar-sized tasks, scheduled heaviest-first, with warp counts assigned proportional to load; each point-addition itself runs cooperatively across threads. A short bucket-reduction then yields the final point.
-
Foundation: floating-point field library new
All field arithmetic packs 52-bit limbs into double-precision floats (15 limbs for 753-bit values), using Dekker's method to recover exact products — exploiting the GPU's abundant FP64 throughput. The library alone speeds up even the BASELINE's design by 1.6×.
The idea
Don’t move the data — move the work assignment. The NTT insight is that shuffling exists only to make thread accesses contiguous, and contiguity can be manufactured without touching global memory. Later NTT batches consist of many small independent groups; GZKP assigns several of them (G ≥ 4) to one thread block, which loads G strided slices and transposes them in shared memory — 48 KB per SM, free to permute. The global array is written back in the SAME order it was read. What the classic algorithm did with a full-array pass through global memory, GZKP does with an on-chip transpose whose cost is invisible. A column-major layout for the big integers finishes the job: limb j of 32 consecutive elements sits at 32 consecutive addresses, so every warp access coalesces — worth 2× by itself at 753 bits.
Spend cheap preprocessing to delete the serial step. Pippenger’s window-reduction exists because bucket b in window t represents b·2^(t·k)⊗P — different windows carry different weights, so their results must be combined by a doubling chain at the end. GZKP’s move: bake the weight into the POINT instead. If the table already holds 2^(t·k)⊗Pᵢ, then window t’s digit can be added directly into a single, window-agnostic bucket set. All windows merge; the doubling chain vanishes; and point-merging — already 90% of the work — becomes a single uniform phase that saturates the GPU. The naive table is too big (over 5 GB at Zcash’s scale), so GZKP stores weighted points only at checkpoints every M windows and pays at most (M−1)·k extra additions to reach intermediate windows — a knob that adapts the same code to a 32 GB V100 or an 11 GB GTX 1080Ti.
Balance the load you can no longer hide. With one giant merged bucket set, the sparse-scalar imbalance that crippled MINA would be fatal — so GZKP treats bucket sizes as a scheduling input. Buckets are grouped into tasks of similar total load, the heaviest tasks launch first, warps are allocated proportional to load, and individual point-additions are internally parallelized across threads via cooperative groups so even one monster bucket spreads across the machine.
Feed it all with floating-point arithmetic. Field elements are represented as 52-bit limbs stored in FP64 mantissas; Dekker’s method splits each multiplication so products are exact. The GPU’s double-precision units — mostly idle in integer-based provers — do the heavy lifting. This library is orthogonal to the algorithms: the paper shows it accelerates even the baseline’s own design by 1.6×.
Walking through one Zcash proof
A Zcash Sprout proof (2²¹ elements, 381-bit BLS12-381 curve) through GZKP
NTT = finite-field FFT (the POLY stage runs seven). MSM = Σ sᵢ⊗Pᵢ over elliptic-curve points (five per proof). Window/bucket = Pippenger's decomposition of scalars into k-bit digits. Warp = 32 GPU threads executing in lockstep. Timings are from the paper's Tables 3, 5, and 7 on an NVIDIA V100.
Step 1 of 7
The job arrives
The Sprout transaction's circuit yields scalar vectors about 2 million entries long. The prover must run seven NTTs (POLY stage) and five MSMs (MSM stage) over 381-bit field elements. bellperson — the best prior GPU prover for this curve — takes 2.93 s; bellman on a CPU takes 13.7 s.
vector size: 2,097,151 bellperson: 0.69 s POLY + 2.24 s MSM
Why it works
- Shuffling was pure overhead that scaled with bit-width → keeping data stationary and transposing small tiles in 48 KB shared memory buys contiguity without global traffic → NTT time drops up to 2.1× from this alone, and total NTT runtime becomes linear in N (the baseline’s wasn’t, because its fixed one-group-per-block division left 30 of 32 threads idle in late batches).
- Window-reduction was serial because windows carried different weights → precomputing the weights into the points makes every window’s contribution commensurable → all additions land in one bucket set, the GPU runs one giant uniform accumulation, and the CPU round-trip for the serial tail disappears.
- The preprocessing table depends only on the circuit’s fixed base points, not the scalars → it is computed once and amortized over every proof the circuit ever generates → the online path pays only the (M−1)·k extra additions, tunable against available memory.
- Load imbalance is measurable before launch (bucket sizes are known after digit-counting) → scheduling can be explicit — similar-load groups, heaviest-first, warps proportional to load → sparse real-world scalars no longer serialize the machine, which is precisely where MINA lost its GPU advantage.
- FP64 units on a V100 vastly outnumber integer-multiply throughput → 52-bit limbs in double mantissas with Dekker’s exact products move field arithmetic onto the abundant resource → 1.6× on the baseline’s own design, compounding with the algorithmic wins.
What it costs
- Preprocessing memory, deliberately spent. The checkpointed weighted-point table makes GZKP’s BLS12-381 memory footprint larger than bellperson’s (though it plateaus past scale 2²² and stays within a V100’s 32 GB). The paper’s framing: bellperson under-uses GPU memory; GZKP converts that headroom into speed.
- Offline preprocessing per circuit. The weighted-point table must be built before the first proof. It is reusable across all proofs of that circuit — the standard zkSNARK setting — but a workload with constantly changing base points would not amortize it.
- Extra additions at checkpoints. Skipping intermediate windows costs up to (M−1)·k point-additions per element — the explicit price of fitting the table into smaller GPUs.
- Per-curve tuning. The window size k comes from profiling each curve/scale combination, and the finite-field library needs limb-count configuration per bit-width (15 limbs at 753 bits).
- Engineering depth. Custom CUDA with cooperative groups, a hand-built floating-point field library, and layout changes (column-major limbs) that permeate every kernel — this is a ground-up prover, not a patch to bellperson.
The evaluation
Experiments run on NVIDIA V100 GPUs (with a GTX 1080Ti repeat to check the results aren’t V100-specific), against the strongest available system per curve: libsnark and bellman on CPU, MINA and bellperson on GPU. Real workloads come from xJsnark applications (AES, SHA-256, RSA operations, Merkle trees, an auction) and the Zcash transaction tool — both produce the sparse scalars that stress load balancing — while module microbenchmarks use dense synthetic data, so the sparse-workload wins are not an artifact of cherry-picked inputs. Honest accounting note: the ablations attribute the gains — in the MSM at scale 2²², bucket partitioning alone gives 3.25×, the floating-point library adds 33%, and load balancing brings the total to 5.6×; in the NTT, the library gives 1.6× and the shuffle-less design another 1.5× on top.
Best CPU vs best prior GPU vs GZKP
| Property | Best CPU (libsnark / bellman) | Best prior GPU (MINA / bellperson) | GZKP |
|---|---|---|---|
| End-to-end zkSNARK proofs, 753-bit curve (6 apps) | |||
| Zcash shielded transaction, 381-bit curve | |||
| Single NTT operation | |||
| Single MSM operation | |||
| Elliptic-curve coverage |
Check yourself
Check your understanding
1. GPU FFT tradition shuffles the array between iteration batches so threads read contiguous memory, and it works beautifully for scientific computing. Predict what happens when the elements grow from 8-byte doubles to 753-bit (~96-byte) field elements.
2. Pippenger's algorithm ends with window-reduction — combining each window's result with a chain of doublings — which is inherently serial. How does GZKP make this step disappear entirely?
3. MINA put the MSM stage — over 70% of CPU proving time — on a GPU, yet barely beat libsnark running on CPUs end to end. Why?
4. GZKP implements 256–753-bit integer arithmetic using double-precision FLOATING-POINT units, packing 52-bit limbs into FP64 mantissas with Dekker's method. Why would exact big-integer math run on float hardware?