The sumcheck protocol
the workhorse under every page
The algorithm
Every operator page says "run a sumcheck." A sumcheck reduces a claim about a sum over the boolean hypercube ($2^n$ terms) to a claim about the summand at one random point, in $n$ rounds of $O(1)$-size messages. This is why matmul is cheap: the product is never committed, only evaluated at one point.
Why it resists a proof
Two facts from the code. The round loop is not in zkml; the engine (IOPProverState, IOPVerifierState, IOPProof) lives in dp_crypto, and zkml only builds a virtual polynomial whose hypercube sum equals the claim. And a sumcheck opens nothing: its output is a deferred claim $f(\text{point})=e$, discharged later against a commitment.
Cryptographic toolbox
- Virtual polynomial builder (the summand as an expression tree)
- Fiat–Shamir challenges via the transcript
- Random-linear-combination batching of many claims into one
- Deferred claims, opened later by the PCS
How each scheme proves it
GKR for the arithmetic relations, Lasso for the lookups — and circuit squeeze, an optimization aimed squarely at the parallelism GKR + Fiat–Shamir normally loses.
- Arithmetic relations are proven by GKR (ML-friendly, layer-by-layer sumcheck); range and exp relations by Lasso. The advice design keeps the arithmetic circuit shallow.
- Circuit squeeze. GKR under Fiat–Shamir parallelises poorly — each layer's sumcheck is small, and FS forces synchronization per layer, so threads idle. zkGPT's insight: because results are supplied as advice, the subcircuits checking different constraints have no topological dependence, so they can be squeezed into the same layers — fewer, wider layers, more parallelism.
- Matmuls (proven by Thaler's protocol, not GKR) get a companion protocol that batches several into one sumcheck, so they parallelise too. Net effect: GPT-2 in <25 s on a 16-core CPU.
The base protocol, GPU-first. zkLLM's contribution is making sumcheck-based lookups (tlookup) and attention (zkAttn) fully parallel, and adding a zero-knowledge variant.
- Arithmetic tensor ops (matmul, convolution) are verified by sumcheck over multilinear extensions. Preserving the tensor structure is what enables CUDA parallelism.
- A zero-knowledge sumcheck hides the tensors, at negligible extra cost. This is the piece DeepProve does not have and Jolt Atlas rebuilds with BlindFold.
zkml builds a virtual polynomial and calls the dp_crypto engine. The clearest instance is same_poly: it folds many claims about one committed polynomial into a single reduced claim.
- same_poly. One committed polynomial $f$ has accumulated claims $f(r_i)=y_i$ from downstream layers. Sample one coefficient $a_i$ per claim; build $f\cdot(\sum_i a_i\,\beta_{r_i})$. Its hypercube sum is $\sum_i a_i y_i$, because $\sum_x f(x)\beta_{r_i}(x)=f(r_i)$.
- The verifier cannot open $f$. It computes the $\beta$ factor itself ($\sum_i a_i\,\mathrm{identity\_eval}(r_i,\text{point})$), multiplies by the prover's $f(\text{point})$, and checks the product against
expected_evaluation. The survivingClaim{point,eval}is returned unopened. - Batching has two layers: a random linear combination folds many claims into one sumcheck, and a
ChunkProofis a sequence of per-layer sumcheck proofs, output layer to input.
let mut expr_builder = VirtualPolynomialsBuilder::<F>::new(num_threads, num_vars);
let poly_expr = expr_builder.lift(Either::Left(&self.poly));
let sum_expr =
beta_evals
.iter()
.enumerate()
.fold(Expression::Constant(F::ZERO), |acc, (i, p)| {
acc + Expression::Challenge(i as u16, 1, F::ONE, F::ZERO)
* expr_builder.lift(Either::Left(p))
});
let virtual_poly = expr_builder.to_virtual_polys(&[poly_expr * sum_expr], &challenges);
let (sumcheck_proof, state) = IOPProverState::<F>::prove(virtual_poly, t);pub struct ChunkProof<F: PrimeField, PCS: CommitmentScheme> {
/// The successive sumchecks proofs. From output layer to input.
steps: HashMap<NodeId, LayerProof<F, PCS>>,