Proving inference · Operator atlas · Claims & the backward pass

Claims & the backward pass

how operators compose into one proof

$$\text{a claim is tiny: } \mathrm{Claim}\{\,\text{point},\ \text{eval}\,\}\ \text{asserting}\ \widetilde{f}(\text{point}) = \text{eval} \\[8pt] \text{each layer: } \underbrace{\text{claims about its outputs}}_{\text{last\_claims}}\ \xrightarrow{\ \mathrm{prove}\ }\ \underbrace{\text{claims about its inputs}}_{\text{returned}} \\[6pt] \text{run output-layer} \to \text{input-layer, until only boundary MLE claims remain}$$

The algorithm

The glue. A whole-model proof is not one monolithic circuit — it is a backward pass that threads small claims through the operator DAG. Each layer converts claims about its outputs into claims about its inputs; run from the output layer to the input layer, the entire model collapses to a handful of claims on the boundary polynomials (weights and inputs), which the commitment scheme then opens.

Why it resists a proof

Our docs called these "virtual claims." The code shows they are concrete Claim{point,eval} structs — owned, stored in a register, and (across distributed chunks) backed by polynomial commitments. And the layer role is not one trait but fiveOpInfo, Evaluate, ProveInfo, ProvableOp, VerifiableCtx — with prove(last_claims) → input claims as the load-bearing method. Non-provable layers (Flatten, Reshape) propagate claims unchanged.

Cryptographic toolbox

  • The five-trait layer anatomy
  • A claim register: HashMap
  • flatten_and_merge_claims for fan-out
  • Committed boundary edges across distributed chunks

How each scheme proves it

A claim is one MLE evaluation. The backward pass over the DAG turns each layer's output claims into input claims; across chunk boundaries, claims are threaded by committing the edge MLEs, not passed inline.

  • Prover::prove_chunk walks chunk.subgraph.backward_iter(). Node::Output seeds a register HashMap<NodeInput, Claim>; each provable node reads the claims its successors deposited on its output ports, merges fan-out with flatten_and_merge_claims, calls op.prove(...), and writes the returned input claims back for its predecessors.
  • Non-provable layers don't prove anything — they propagate the claims unchanged, because they don't alter values. The recursion bottoms out at model inputs, where the verifier checks input.into_mle().evaluate(claim.point) == claim.eval directly.
  • Distributed proving cuts the model into ModelChunks; claims crossing a boundary are not passed inline. Each chunk commits its boundary-edge MLEs, keys claims by a SHA256-derived commitment id, and a consistency check verifies an input-edge commitment equals the matching output-edge commitment in the source chunk.
  • The verifier context is stored already reversed, because proving runs last-layer-to-first.
zkml/src/iop/claim.rs · 26-31
pub struct Claim<F> {
    #[serde(with = "dp_crypto::serialization")]
    pub point: Vec<F>,
    #[serde(with = "dp_crypto::serialization")]
    pub eval: F,
}
A claim is just an MLE evaluation: f(point) = eval. Nothing 'virtual' about it.
zkml/src/iop/prover.rs · 764-785
let my_claims = if op.is_provable() {
    op.prove(
        node_id,
        ctx,
        claims_for_prove.iter().collect::<Vec<_>>(),
        section,
        &mut self,
    )
    .with_context(|| format!("proving {}: {}", node_id, op.describe()))?
} else {
    // we only propagate the claims, without changing them, as a non-provable layer
    // shouldn't change the input values
    claims_for_prove
};
The backward pass: provable ops derive input claims via prove(); non-provable layers propagate their claims unchanged.