scroll-tech / ceno

Accelerate Zero-knowledge Virtual Machine by Non-uniform Prover Based on GKR Protocol
Apache License 2.0
31 stars 4 forks source link

investigate efficiency of memory finalized circuit #378

Open hero78119 opened 3 weeks ago

hero78119 commented 3 weeks ago

Per discussion here https://github.com/scroll-tech/ceno/pull/251#discussion_r1797717452

Currently in memory consistency check it's relied on max_memory as circuit constant parameter, which obviously bring up a possible gap between real in-used memory since max_memory need to cover the worst case memory consumption.

One solution, as non-uniform circuit and sumcheck based design, it's naturally possible treating memory table as special case of dynamic table, such that "memory size" can be auxiliary input from prover so the size can be determined for different proof and prover just need to "pay-as-you-go". Extra cost of this design is we need to prove address column are well-formed: uniquely and sorted

kunxian-xia commented 3 weeks ago

Here I want to describe how to prove a set { addr_i } does not have duplicated elements (aka. every element has multiplicity one) by applying "Protocol 1" proposed in sec 4 of OLB24 in the multivariate setting (aka. using sumcheck). This can be used in memory init / finalize circuit.

Recap protocol 1 in OLB24

The prover encodes the set { addr_i: 0 < i < N } as a univariate polynomial $z(X) = \prod (X - a_i)$. It's easy to see that this is equivalent to the formal derivative $z'(X)$ is co-prime to $z(X)$ in the polynomial ring $F[X]$. By bezout's identity, we know there must exists two polynomials $s(X), t(X)$ of degree less than $N-1$ and $N$ respectively s.t.

$$z(X) s(X) + z'(X) t(X) = 1. $$

According to Schwartz-Zippel lemma, this is equivalent to checking $z(\alpha) s(\alpha) + z'(\alpha) t(\alpha) = 1$ for a random challenge $\alpha \in E$ with negligible soundness error.

Prove protocol 1 using sumcheck

It's easy to see

  1. $z(\alpha) = \prod (\alpha - a_i)$ can be easily proved by our product tower prover if we encodes { a_i } as a multilinear polynomial $Z(\vec{X}) = \sum_{\vec{i}} \textrm{eq}(\vec{X}, \vec{i}) \cdot a_i$ where $\vec{i}$ is the binary decomposition of $i$.
  2. Since $z'(\alpha) = z(\alpha) * \sum \frac{1}{\alpha - a_i}$, and the fraction sum $\sum \frac{1}{\alpha - a_i}$ can also be easily proved by our tower prover.

The problem is reduced to

how to evaluate $s(\alpha)$ and $t(\alpha)$.

evaluates $s(\alpha)$ using tower tree

Note that $s(\alpha) = s_0 + s_1 \alpha + s_2 \alpha^2 + \cdots = s{odd}(\alpha^2) * \alpha + s{even}(\alpha^2)$ where

We also encodes { s_i } as a multilinear polynomial $S(\vec{X}) = \sum_{\vec{i}} \textrm{eq}(\vec{X}, \vec{i}) \cdot s_i$ . Assuming $N = 2^k$ is a power of two, let's show how to compute $s(\alpha)$ using layered circuit which can then be proved by our tower prover. We call this poly evaluate tower prover.

Notation: $B_j$ is the boolean hypercube with $j$ variables.

Let's take $k = 3$ as an example.

Protocol 1 in multivariate setting

We can use tower prover to the reduce the correctness of $s(\alpha)$ to evaluation of $S(\vec{X})$ at a random point $\vec{r} \in E^k$. We can do the same for $t(\alpha)$.

Therefore protocol 1 in the multivariate setting works as follows:

  1. The prover commits to $Z(X), S(X), T(X)$ and sends three commitments $c_Z, c_S, c_T$ to verifier;
  2. The verifier samples a random challenge $\alpha \in E$;
  3. The prover runs product tower prover for $z(\alpha)$ to get product tower proof $\pi_1$ and point evaluation $Z(\vec{r})$;
  4. The prover runs fraction sum tower prover for $\sum \frac{1}{\alpha - a_i}$ to get fraction sum tower proof $\pi_2$ and point evaluation $Z(\vec{u})$ ;
  5. The prover runs poly evaluate tower prover for $s(\alpha)$ to get poly evaluate tower proof $\pi_3$ and point evaluation $S(\vec{\omega})$;
  6. The prover runs poly evaluate tower prover for $t(\alpha)$ to get poly evaluate tower proof $\pi_4$ and point evaluation $T(\vec{\zeta})$;
  7. The prover sends mpcs opening proof $\pi$ for these evaluations to verifier.
  8. The verifier checks the mpcs opening proof $\pi$ and all these 4 tower proofs. And then verifier checks if $z(\alpha) s(\alpha) + t(\alpha) z(\alpha) * \sum \frac{1}{\alpha - a_i} = 1$.
hero78119 commented 2 weeks ago

@kunxian-xia this idea works! but it will introduce an overhead in prover to to interpolate and derive coefficients format for s(x) and t(x).

But I just reviewed again for our original question and suddenly a idea flash into my mind: the way to make us abandon to prove address vector are well format [0, 1, 2, 3, 4, 5, .. 2^b - 1] is because we haven't figure out how to convert into sumcheck PIOP, but... what if we just pre-commit few address vectors 16: [0, 1, ... 2^16-1] 17: [0, 1, ... 2^17-1] ... 28: [0, 1, ... 2^30-1] And we allow prover to auxiliary the index and choose which pre-commit poly to use?

Few benefits for these design

Wdyt :) ?

kunxian-xia commented 2 weeks ago

I want to explain why the memory entries that accessed by a guest program have holes (equivalently, the addresses are sparse).

The memory allocator used in RISC0 / SP1 looks like this

/// https://github.com/risc0/risc0/blob/main/risc0/zkvm/platform/src/heap/bump.rs
use crate::syscall::sys_alloc_aligned;
use core::alloc::{GlobalAlloc, Layout};

#[global_allocator]
pub static HEAP: BumpPointerAlloc = BumpPointerAlloc;

pub struct BumpPointerAlloc;

unsafe impl GlobalAlloc for BumpPointerAlloc {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        sys_alloc_aligned(layout.size(), layout.align())
    }

    unsafe fn dealloc(&self, _: *mut u8, _: Layout) {
        // this allocator never deallocates memory
    }

    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
        // NOTE: This is safe to avoid zeroing allocated bytes, as the bump allocator does not
        //       re-use memory and the zkVM memory is zero-initialized.
        self.alloc(layout)
    }
}

As you can see the above allocator is too simple and it never deallocates memory. This can cause big problem that never happen with modern malloc libraries like jemalloc.

Let's assume your guest program has code snippets like this:

fn foo() {
  {
     let mut v = vec![];
     for i in (0..n) {
        v.push(i);
     }
  }

  // ... later
  {
     let mut v = vec![];
     for i in (0..n) {
        v.push(i);
     }
  }
}

There are two issues in the above code snippets.

  1. With allocator that frees memory, two occurrences of v may have same memory address; while with the BumpPointerAlloc allocator, 2nd occurrence of v must have larger memory address than the 1st even though the scope of the two for-loops are disjoint.
  2. For containers like Vec and HashMap, if we don't reserve the "right" capacity before we do the insertions, the utilized ratio may be around 50% because each time the old space is not enough it will grows by twice (ref Vector in rust std library).

Then there will holes in the memory addresses accessed by your guest program.

kunxian-xia commented 2 weeks ago

Like i mentioned in earlier (comment):

hero78119 commented 1 week ago

Elaborate succintly address form which verifier can evaluate it without any SNARK, and leveraging a multi-variate math fact (credited from @spherel 🔥 )

image

With above fact, verifier can evaluate addr directly giving challenge vector $\vec{r}$ with just simple field arithmetics, without prover help. The non-uniform prover design from the fact that prover can witness n = ceil(log(max_memory)) per proof, and verifier take it to evaluate address.

This method also expose a powerful fact => we can add constant offset and scalar into the memory, so it end up that we can construct any consecutive memory sub-region starting on arbitrary offset.

Giving 2 assumptions for riscv zkVM access pattern:

One imperfect is prover still need to pay the cost [max_address, next_pow2]. I would claim this gap might be negligible. Since opcode proof might be bottleneck, and table proof with such non-uniform design already solve the issue nicely. It is like 2/8 principle: 20% effort solve 80% problem. We can left those 80% effort as future work

PR WIP #457