Lookup arguments & range checks
how non-linearity is discharged
The algorithm
The primitive that discharges everything a field cannot compute cheaply — exp, GeLU, right-shifts, sign, "is this zero," "is this in range." The prover shows the multiset of looked-up rows is contained in a fixed table $T$ by proving the LogUp fractional-sum identity. Range checks are not a separate mechanism: a lookup whose table is exactly the interval $[\min,\max]$ is a range check, which is why our old separate pages are merged here.
Why it resists a proof
DeepProve computes each side of the identity with a GKR fraction tree, not one big sumcheck: a binary tree of $a/b + c/d = (ad+bc)/(bd)$ nodes halving the width each layer until one fraction remains. Two subtleties the paper glosses: each denominator entry is a random linear combination of all columns of its row (tables are genuinely multi-column), and the two sides use different numerators — the table side carries multiplicities, the lookup side hard-codes every numerator to $-1$.
Cryptographic toolbox
- LogUp fractional-sum identity
- GKR fraction tree (Fraction add nodes)
- Multi-column denominators via a column-separation challenge
- Multiplicities Mᵢ = count(lookups)/count(table rows), by field inverse
- Decomposable tables: split a wide value into k-bit chunks
How each scheme proves it
A different lookup family — Lasso, not LogUp. zkGPT uses Lasso for both its range relations and its exponentiation table, which is what makes result-as-advice cheap.
- Where DeepProve/zkLLM use LogUp (a fractional-sum identity), zkGPT uses Lasso. Its range-check table is structured (satisfies Lasso's structured-table requirement); its exponentiation table is small and unstructured, which Lasso still handles well.
- This is the engine behind result-as-advice — the prover supplies a division/sqrt/exp result as a witness, and the check is a single Lasso range lookup or exp lookup rather than an arithmetic simulation of the operation.
- So all three indexed sum-check systems reach non-linearity through lookups, but by three different arguments: LogUp-GKR (DeepProve), tlookup (zkLLM), Lasso (zkGPT).
tlookup — the lookup argument DeepProve's descends from. Same LogUp identity, but run as one parallel sumcheck instead of a GKR tree, so it maps onto a GPU.
tlookup, a parallelized lookup argument designed for non-arithmetic tensor operations in deep learning, offering a solution with no asymptotic overhead.
zkLLM · Abstract
- tlookup proves set inclusion $S \subseteq T$ via the LogUp identity $\sum_i 1/(\beta+S_i) = \sum_i m_i/(\beta+T_i)$, checked at a random $\beta$. Multiplicities $m$ come from counting, in $O(D)$.
- The point is parallelism. Prior lookup arguments were sequential (univariate polynomials); tlookup runs the identity as one sumcheck over tensors, so it uses a GPU. No asymptotic overhead in time or memory.
- It is the foundation for zkAttn (softmax) and every activation. DeepProve keeps the identity but computes it with a GKR fraction tree and batches many table sizes into one proof.
LogUp-GKR with three shared 16-bit auxiliary tables and one op-specific value table. Exactly eight table types exist — and there is no 'clamp table' or 'argmax table'; clamping is a selector folded into the sumcheck.
- The acceptance condition is that the combined numerator over all instances is zero and the combined denominator non-zero. The lookup side sets every numerator to $-1$; the table side carries multiplicities $M_i=\mathrm{count(lookups)}/\mathrm{count(table\ rows)}$ computed by a field inverse, so non-power-of-two tables padded with repeated rows still satisfy the identity.
- There are exactly eight
TableTypes —Exp, GeLU, ReLU, SiLU, Requantise, ShiftCheck, SignedZeroCheck, ZeroCheck— generated on demand as an input range, never materialised as a stored array. Three of them are shared, fixed 16-bit auxiliary tables (ShiftCheck = pure range, ZeroCheck = "is this chunk zero," SignedZeroCheck = sign), independent of the 12-bit quantization width. - A wide accumulator (~62 bits) is range-checked by decomposition: split into a low value chunk (hits the op table) plus 16-bit shifted chunks (the requant shift, range-checked) and 16-bit zeroing chunks (the high bits, forced to zero). Clamping is not a table at all — it is the product of the zeroing-chunk outputs acting as a selector, multiplied into the value inside the lookup expression.
- Instances of one proof type at different sizes are batched into a single GKR, required in decreasing-
num_varsorder and rolled in at the round where their height matches.
impl<F: PrimeField, T: Borrow<Fraction<F>>> AddAssign<T> for Fraction<F> {
fn add_assign(&mut self, rhs: T) {
let rhs: &Fraction<F> = rhs.borrow();
let numerator = (self.numerator * rhs.denominator) + (self.denominator * rhs.numerator);
let denominator = self.denominator * rhs.denominator;
*self = Fraction { numerator, denominator };
}
}/// The bit size for the zero check table. pub const ZERO_CHECK_TABLE_BIT_SIZE: usize = 16; /// The bit size for the shift check table. pub const SHIFT_CHECK_TABLE_BIT_SIZE: usize = 16;
The heart of the paper, and a genuinely different instantiation: the lookup argument is built from ZK proofs of read-only memory, not from LogUp, Lasso or a polynomial commitment. Amortised cost is 2 multiplications per lookup.
considering N ≫ T , the amortized computation complexity per lookup is 2 multiplications.
Hao et al. · §3.5
We re-run the source code of Caulk [61], and the results show that the ZK lookup protocol of Caulk costs 177.451ms for each amortized access on a table of size 2¹², while our protocol from ZK-ROM only requires 0.069ms (about 2571× better) for the same setting.
Hao et al. · §2.1
- For $N$ lookups into a public size-$T$ table the cost is $T + 2N$ multiplications. Because the table is public, its $T$ tuples are appended to the ROM's write set in plaintext, saving the $T$ multiplications the generic ZK-ROM would spend committing them.
- Range checks are the degenerate case, exactly as in the sum-check systems: a range check is a table lookup whose output is empty. Same unification DeepProve arrives at with its ShiftCheck / ZeroCheck tables.
- They benchmarked the folklore choice instead of inheriting it. Caulk was re-run and found far slower per amortised access at the same table size; Lasso was rejected because it wants decomposable or structured tables. Almost nobody else in this literature justifies their lookup argument empirically.
- The amortisation is load-bearing and honestly stated: the cost is $O(1)$ per lookup given $N \gg T$. With a $2^{12}$-entry table and the default batch of $10^5$ instances that holds comfortably -- and the paper defends the regime by noting a single ResNet-50 layer contains on the order of 800K ReLUs -- but it is an assumption, and $T$ grows exponentially in the digit width.