--- tags: PSE --- # Folding Circom circuits: a ZKML case study ## Previous work from other groups * [Zator: Verified inference of a 512-layer neural network using recursive SNARKs 🐊](https://github.com/lyronctk/zator) ## Folding model architecture vs inference * Folding model architecture * Pros: get much more compression ratio as we wish per inference * Cons: require models to be specifically designed for folding, which might not be practical for the current web2 industry * Folding model inference * Pros: do not need to modify current models * Cons: performance still limited by the model depth and complexity ## Let's try to fold model inference! In order to use [Nova-Scotia](https://github.com/nalinbhardwaj/Nova-Scotia) to fold Circom circuits: > ![](https://hackmd.io/_uploads/H17dtjz43.png) > We write a circuit that takes a list of public inputs (these must be named step_in for the Nova-Scotia interface) and outputs the same number of public outputs (named step_out). These public outputs will then be routed to the next step of recursion as step_in, and this will continue until we reach the end of the recursion iterations. Within a step circuit, besides the public inputs, Circom circuits can input additional private inputs (with any name/JSON structure Circom will accept). Let's try to write out the `step_in`, `step_out`, and private inputs at each step! ### Scenario 1: Public Data, Private Model #### Attempt 1: naive approach | signals | Step 1 | Step 2 | ... | Step N-1 | Step N | | -------------- | ------------------------------------------------------ | ---------------------------------------------------------- | --- | ------------------------------------------------------------------ | ---------------------------------------------------------------------- | | $step_{in}$ | $[modelHash,data_1,data_2,...,data_N,0,0,...,0,0]$ | $step_{out,1}$ | ... | $step_{out,N-2}$ | $step_{out,N-1}$ | | $step_{out}$ | $[modelHash,data_1,data_2,...,data_N,out_1,0,...,0,0]$ | $[modelHash,data_1,data_2,...,data_N,out_1,out_2,...,0,0]$ | ... | $[modelHash,data_1,data_2,...,data_N,out_1,out_2,...,out_{N-1},0]$ | $[modelHash,data_1,data_2,...,data_N,out_1,out_2,...,out_{N-1},out_N]$ | | private inputs | model weights | model weights | ... | model weights | model weights | Issues: * the size of the public outputs (hence the size of the proof) grows linearly * a lot of switchers need to be used to insert at the correct positions #### Attempt 2: Merkle root? | signals | Step 1 | Step 2 | ... | Step N-1 | Step N | | -------------- | ------------------------------------------------------------ | ----------------------------------------------------------- | ---------------- | --------------------------------------------------------------------- | ----------------------------------------------------------- | | $step_{in}$ | $[modelHash, zeroRoot, zeroRoot]$ | ... | $step_{out,N-2}$ | $step_{out,N-1}$ | | | $step_{out}$ | $[modelHash, dataMerkleRoot_1, outputMerkleRoot_1]$ | $[modelHash, dataMerkleRoot_2, outputMerkleRoot_2]$ | ... | $[modelHash, dataMerkleRoot_{N-1}, outputMerkleRoot_{N-1}]$ | $[modelHash, dataMerkleRoot_N, outputMerkleRoot_N]$ | | private inputs | $[modelWeights, dataLeaf_0, merklePaths_0]$ | $[modelWeights, dataLeaf_1, merklePaths_1]$ | ... | $[modelWeights, dataLeaf_{N-2}, merklePaths_{N-2}]$ | $[modelWeights, dataLeaf_{N-1}, merklePaths_{N-1}]$ | Improvements: * less public inputs/outputs Issues: * order of data matters! * relatively cheaper (than Attempt 3) if try to verify the entire tree on chain * $2\log N$ number of hashes per step * need to publish Merkle leaves elsewhere #### Attempt 3: Recursive hashing? | signals | Step 1 | Step 2 | ... | Step N-1 | Step N | | -------------- | ---------------------------------------- | ----------------------------------------------------------- | ---------------- | -------------------------------------------------- | ------------------------------------------------ | | $step_{in}$ | $[modelHash, 0, 0]$ | ... | $step_{out,N-2}$ | $step_{out,N-1}$ | | | $step_{out}$ | $[modelHash, H(0, data_1), H(0, out_1)]$ | $[modelHash, H(H(0, data_1),data_2), H(H(0, out_1),out_2)]$ | ... | $[modelHash, H(...,data_{N-1}), H(...,out_{N-1})]$ | $[modelHash, H(..., data_1), H(..., out_{N-1})]$ | | private inputs | $[modelWeights, data_1]$ | $[modelWeights, data_2]$ | ... | $[modelWeights, data_{N-1}]$ | $[modelWeights, data_N]$ | Improvements: * only 2 hashes per step * less signals to pass around Issues: * order of data still matters * very expensive to verify on chain, but luckily we can precompute and commit the final hash * need to publish raw inputs elsewhere ### Scenario 2: Private Data, Public Model #### Attempt 1: naive approach | signals | Step 1 | Step 2 | ... | Step N-1 | Step N | | -------------- | ----------------------------------------------------- | ------------------------------------------------------------------ | --- | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | $step_{in}$ | $[modelWeights,0,0,...,0,0,0,0,...,0,0]$ | $step_{out,1}$ | ... | $step_{out,N-2}$ | $step_{out,N-1}$ | | $step_{out}$ | $[modelWeights,dataHash_1,0,...,0,0,out_1,0,...,0,0]$ | $[modelWeights,dataHash_1,dataHash_2,...,0,0,out_1,out_2,...,0,0]$ | ... | $[modelWeights,dataHash_1,dataHash_2,...,dataHash_{N-1},0,out_1,out_2,...,out_{N-1},0]$ | $[modelWeights,dataHash_1,dataHash_2,...,dataHash_{N-1},dataHash_N,out_1,out_2,...,out_{N-1},out_N]$ | | private inputs | $data_1$ | $data_2$ | ... | $data_{N-1}$ | $data_N$ | Issues: same as Scenario 1 Attempt 1 #### Solution: see Scenario 1 Attempt 3 Issues: * we will need to publish the model weights elsewhere