Feb 25, 2026·8 min read·Cysic

zkVM Trilogy (Part I): Go Graph

Who this post is for: ZKP engineers, proving-system architects, and anyone evaluating how to structure a zkVM backend for performance and…

zkVM Trilogy (Part I): Go Graph

zkVM Trilogy (Part I): Go Graph

Feb 25, 2026

Who this post is for: ZKP engineers, proving-system architects, and anyone evaluating how to structure a zkVM backend for performance and auditability. Familiarity with GPU kernel programming and basic IOP structure is helpful but not required. This post focuses on architectural philosophy and design rationale — concrete benchmark data appears in the upcoming zkVM Trilogy (Part II)

The Philosophy (Why Graph?)

In the current landscape where we increasingly prioritize the speed of ZKP proving systems, almost all mainstream proving systems offer GPU acceleration support (such as CUDA or Metal). When we “offload” compute kernels to devices other than the CPU, we typically have two choices:

  1. The HAL (Hardware Abstraction Layer) Mode: This wraps different computing devices (CPU, GPU, or others) into a set of identical calling interfaces. It supports running the same proving code on different hardware platforms through different dispatching of function calls with the same name.
  2. The Graph Mode: This encodes the entire computation process (such as the proving process) into a pure data structure, typically in the form of a computational graph. Such a graph, after optimization, can be uniformly translated into implementations for specific platforms (for example, mapping to a CUDA Graph on a GPU).

Today, the choices made by the ZKP field and the AI field on this issue are almost exactly opposite. The AI field has almost invariably chosen the graph mode, while proving systems in the ZKP field (such as Risc0, etc.) have chosen the HAL mode. The current state of this choice in the ZKP field may stem from the historical development process: for example, most proving systems started from pure CPU implementations. Therefore, when adding GPU support, the first mode (HAL mode) is compatible with the vast majority of existing code (requiring only a relatively simple “isolate” and “replace” process).

However, under the HAL mode, it is not easy to encode the ZKP proving process in an algebraic way and dump it for downstream optimization (for performance) and audit analysis (for security). This is exactly what the Graph mode can provide. We believe that in the long run, the latter (Graph mode) will provide better performance, cross-platform support capabilities, and more importantly: better security and auditability. These reasons prompt us to consider building an experimental proving system based on the second mode.

Review of the Status Quo

Existing ZKP systems (taking several zkVM implementations as examples, such as RISC0, SP1, Hypercube, etc.) perform the backbone of their proving on the CPU: allocating buffers, initializing witness data, executing the proving process, managing the fiat-shamir transcript, etc. (the so-called “prove” function). To support running on different platforms with the same code, their typical pattern is to implement the “prove” function as a generic function over a “backend” (this generic design can be called HAL). If using Rust, the approximate “recipe” is as follows:

  • Step 1: Provide a Trait named Backend, which provides cross-platform types (such as Buffer-related type definitions) and interfaces for core ZKP kernels.
  • Step 2: Implement different Backends (such as CPU, GPU) and implement all necessary Kernels in an enumerated manner.
  • Step 3: Implement a prove function generic over the Backend.
trait Backend<ClassTag> {
    // BufferRef: supports copyFrom/To Host methods
    type BufferRef;

    fn allocBuffer(&self, /* ... */) -> Self::BufferRef;
    fn commit(&self, /* ... */) -> /* ... */;
    fn evaluateConstraints(&self, /* ... */) -> /* ... */;

    // ...
}

// Step 2: Implement different Backends
struct CpuBackend;
struct GpuBackend;

impl Backend<CpuBackend> for CpuBackend {
    type BufferRef = /* ... */;
    fn allocBuffer(&self, /* ... */) -> Self::BufferRef { /* ... malloc ... */ }
    // ...
}

impl Backend<GpuBackend> for GpuBackend {
    type BufferRef = /* ... */;
    fn allocBuffer(&self, /* ... */) -> Self::BufferRef { /* cudaMallocDevice... */ }
    // ...
}

// Step 3: Implement a generic prove function
fn prove<B, ClassTag>(backend: &B /* ... */) -> /* ... */
where
    B: Backend<ClassTag>,
{
    let buf = backend.allocBuffer(/* ... */);
    // ...
    backend.commit(/* ... */);
    backend.evaluateConstraints(/* ... */);
    // ...
}

As mentioned earlier, such a design is the simplest path for modifying a system that already exists based on a pure CPU implementation. However, we can immediately realize several problems.

HAL Defect 1: Difficulty in achieving global optimization across kernel calls. Since in HAL mode, each Kernel is notified to be called only when it is about to be needed, we cannot complete the following types of optimizations:

  • On-demand movement of data between Host and Device between Kernel calls.
  • Packing all Kernel calls into a CUDA Graph to save synchronization overhead between GPU and CPU.
  • Data-level multi-GPU parallelism (scheduling different kernels to run on different GPUs based on the number of GPUs on the machine and GPU memory capacity). Note: Most multi-GPU support in existing proving systems is based on task-level parallelism.

For example, when surveying and profiling several mainstream zkVMs, we found the following phenomena:

  • With overly fragmented GPU Kernels, the GPU-CPU synchronization overhead reduces hardware utilization. This problem is particularly serious when using Multilinear polynomial encoding, because every round of Sumcheck requires global synchronization between GPU and CPU.
  • The semantics encapsulated by some kernels in the Backend, in pursuit of cleanliness, default to copying data back to the Host after each kernel call, while in some cases this movement is unnecessary.

These problems can usually be alleviated through graph-level optimization — that is, we need to encode the computation introduced in the entire ZKP proving process into a data format that can be read by a program. However, in the HAL mode, this graph structure is never explicitly constructed. We only have a black box named “prove function”, so we cannot perform such optimizations.

HAL Defect 2: Hindrance to security analysis and auditing. Unless the correctness of the ZKP/ZKVM itself is trusted, all its proving and verification will become meaningless self-performance. To be sure that the proofs passed by the Verifier are “correct”, the ideal way is to encode all details of the interaction with the fiat-shamir transcript during the proving process in plaintext, and simultaneously dump the proving obligations satisfied and newly added before and after each “IOP reduction” process. A “computational graph” encoding the proof completion flow is an ideal carrier for annotating such information. Theoretically, we can write a separate checker program to check that the entire computational graph (and the reduction relationship of the proving obligations behind it) is legitimate itself. However, in the HAL mode, we cannot do this; for example, the interaction with the fiat-shamir transcript is largely hidden deep in the code of the prove function, which can only be identified by manually reading the code.

Get Cysic’s stories in your inbox

Join Medium for free to get updates from this writer.

Remember me for faster sign in

HAL Defect 3: Hindrance to the use of proprietary hardware. Although mainstream computing platforms are currently CPU+GPU, proprietary accelerators may appear in the future that can natively support some operations originally in the prove function (for example, two most direct possibilities are: 1) supporting fiat-shamir directly on the device to save synchronization with the Host CPU; 2) supporting risc-v emulation and execution trace generation directly at the device level). HAL mode relies on an unparsable prove function, which makes it harder to compile and optimize for proprietary hardware.

The core difference between HAL mode and Graph mode can be viewed from a larger perspective: when we need to model a piece of “computation”, do we adopt Shallow Embedding (writing the computation directly in the host language) or Deep Embedding (encoding the computation into a data structure)?

From Shallow to Deep, our options include:

  1. Direct Implementation: Directly implement this piece of computation in the host language (such as Rust).
  2. Embedded DSL: Simulate the behavior of a DSL using language features (such as polymorphic functions) in the host language.
  3. Redesigning a specialized language to encode this computation: Reinvent a brand new language to encode this computation.

In Zero-Knowledge Proofs, for the computation “to be proved”, merely knowing how to evaluate it (like a circuit) is not enough; we also need to know how to translate this computation into constraints. Therefore, most proving systems have already made choices between 2 and 3. For example:

  1. Circom, Pil/Pil2: They implemented specialized “constraint description languages” and provided separate compilers.
  2. Plonky2: Requires each class of Custom Gate to separately provide functions like “how to compute auxiliary witness”, “how to compute evaluation”, “how to compute residual polynomial”, etc.
  3. Plonky3: More advanced than Plonky2, it cleverly uses the AirBuilder trait to achieve a "multi-functional" computation construction capability. By using different template types, the same code can implement computation on elements on the real witness, calculate residuals as a verifier, and can also be used to dump constraint polynomials (thereby used to generate CUDA Kernels or generate constraint expressions for Lean4 (SP1 Hypercube)). In a sense, in terms of expressing AIR constraints, it achieves the effect of Deep Embedding with the complexity of Shallow Embedding (no new language invented).

Among current mainstream zkVMs, SP1, Hypercube, OpenVM, etc., have all adopted Plonky3, while only Zisk is based on Pil2.

What Makes Our Graph Mode Different

Although there are conceptual similarities, existing attempts to explicitly encode “computation” (such as Pil/Pil2) have significant differences from our desire to encode the proving process into a computational graph, and these differences should not be confused:

The “Computational Object” encoded is different: What Pil/Pil2 wants to encode with data structures is the “computation to be proved” or “constraints”. What we want to encode is the “proving process”.

If we divide the ZKP proving system into three logically relatively independent components:

  • Circuit Representation Layer: (Polynomial declaration, constraint definition, circuit “evaluation rules” and hint generation rules (both jointly provide the witness)) (The core is to provide convenience for end users).
  • Cryptographic IOP Reduction Layer: (The core is to ensure the correctness of the reduction process itself).
  • High-Performance Computing Layer: (How to complete the prove computation as much as possible on the hardware platform).

Then, the mission completed by languages like Pil/Pil2 or Circom is to separate the “Circuit Representation Layer” from the rest. (But the latter two are still mixed together). Our “Graph Mode” can separate the “Cryptographic IOP Reduction Layer” and the “High-Performance Computing Layer”. This allows the second layer to care about correctness at the cryptographic level as cleanly as possible, while the High-Performance Computing Layer only needs to care about computational performance as cleanly as possible.

Conclusion

The Graph Mode is not a cosmetic refactor of the HAL approach. It is a systems decision that impacts performance, security auditability, and hardware portability. By encoding the proving process as an explicit computational graph:

  • We enable global optimization across kernel calls, including cudaGraph capture, intelligent data placement, and multi-GPU scheduling.
  • We create a path to machine-checkable security auditing, where the IOP reduction structure is inspectable as data rather than inferred from source control-flow alone.
  • We establish a hardware-portable execution contract that can survive the transition from GPU to FPGA to ASIC.

In subsequent posts, we will present our preliminary performance results from Venus (our graph-first implementation built on ZisK) and our roadmap toward encoding cryptographic security arguments into the graph itself.

PreviouszkVM Trilogy (Part II): Accelerating zkVM: GPU, FPGA and ASIC — A RevisitNextAnnouncing CyRunners Ambassador Program