The Architecture of a SNARK Proving Engine: Designing cuzk — A Pipelined, GPU-Resident Proof Generation Daemon for Filecoin
Introduction
On a cold Linux machine in the Curio codebase repository, an extraordinary act of systems architecture unfolded. The assistant, having spent the preceding nine messages exhaustively mapping the entire Groth16 proof generation pipeline for Filecoin's storage proof system, sat down to design a new piece of infrastructure: a persistent, GPU-resident SNARK proving daemon called cuzk. The resulting message — message 77 in the conversation — is not merely a plan. It is a masterclass in applied systems thinking, drawing inspiration from GPU inference engines, deeply understanding the memory hierarchy of zero-knowledge proofs, and weaving together half a dozen prior optimization proposals into a coherent, phased architecture.
This article examines that single message in microscopic detail. We will explore why it was written, what decisions it encodes, what assumptions underpin it, where it might be wrong, and what knowledge it both consumes and produces. The message is a turning point in the conversation: the moment when analysis crystallizes into design, when understanding the problem becomes planning the solution.
Context: The Conversation That Led to This Moment
To understand message 77, we must first understand what came before it. The conversation spans 77 messages total, with message 77 being the final assistant response in a long chain of investigation. The user's initial request (message 67) was deceptively simple: "look at all proposals, plan a pipelined snark daemon which accepts a pipeline (some rpc) of PoRep/SnapDeals/PoSt snarks (also on various sector sizes), schedules the work, and outputs results."
But this simple request landed on a foundation of extraordinary depth. The conversation's earlier segments (segments 0 through 2) had already produced:
- A comprehensive background document mapping the entire call chain from Curio's Go task layer through Rust FFI into C++/CUDA kernels, with memory accounting showing ~200 GiB peak memory usage
- Nine structural bottlenecks in the existing proof generation pipeline
- Three optimization proposals: Sequential Partition Synthesis (memory reduction), Persistent Prover Daemon (eliminate SRS loading overhead), and Cross-Sector Batching (throughput improvement)
- A fourth proposal with 18 micro-optimizations across GPU kernels, CPU synthesis, and memory transfers
- A fifth proposal exploiting constraint shape knowledge (SHA-256 dominance, boolean witnesses) for precomputation The assistant had already mapped the full call chain from
tasks/seal/task_porep.go:119throughtasks/proofshare/task_prove.go:243, throughffiselectchild process spawning, through Rust FFI intogenerate_groth16_proofs_c, and into the CUDA kernels for NTT, MSM, and batch addition. It had characterized the computational hotpaths at the instruction level. It had identified that the SRS (Structured Reference String) parameters consumed ~47 GiB of pinned host memory for PoRep proofs, that each proof required ~12-16 GiB of working memory, and that the existing architecture loaded these parameters from disk for every proof — a 30-90 second overhead that could be eliminated. But message 77 is not just a summary of prior work. It is a synthesis. The assistant had just completed four parallel investigation tasks (messages 68-76) that explored: 1. The ffiselect child process model for GPU proving — how Curio currently spawns a child process for each GPU operation, communicates via JSON-RPC over stdin/stdout, and tears down the process after each proof 2. The SRS/parameter loading paths across all proof types (PoRep, SnapDeals, WindowPoSt, WinningPoSt) — discovering that PoRep and SnapDeals require enormous ~30-47 GiB SRS files while PoSt SRS files are tiny (~200 MB - 2 GiB) 3. The supraseal C2 C++/CUDA API surface — the fiveextern "C"functions forming the FFI boundary, themax_num_circuitsparameter limiting batch size, and the SRS cache implementation usingcudaHostRegister4. The bellperson supraseal prover internals — thecreate_proof_batch_priority_innerfunction that combines synthesis and proving into a single call 5. GPU inference engine architectures — how vLLM, Triton Inference Server, and TensorRT-LLM manage model loading/unloading, memory hierarchies, and request scheduling The user had also answered clarifying questions about RPC transport (gRPC preferred), C1 output size (confirmed ~50 MB, not GiB), deployment targets (both small and large machines equally), and the initial build approach (Rust binary with library crate). Message 77 is the culmination of all this work. It is the moment when the assistant stops investigating and starts designing.
The Structure of the Message: An Architecture Document in Miniature
The message is structured as a complete architecture document. It contains 13 major sections, each addressing a different aspect of the proposed system:
- Vision & Positioning — The "why" of cuzk, with explicit analogies to inference engines
- Architecture Overview — A high-level diagram and component description
- Proof Types & Circuit Profiles — Resource requirements for each proof type
- gRPC API Surface — The complete protobuf schema
- SRS Memory Manager — Tiered Residency — The hot/warm/cold memory hierarchy
- Scheduler Design — Priority queues, batch accumulation, GPU affinity
- GPU Worker Pipeline — The per-GPU execution loop
- Library / Binary Design — The Rust crate structure
- Phased Implementation Roadmap — 18 weeks across 6 phases
- Configuration — The TOML config file schema
- Test Harness — Golden test data and test modes
- Key Design Decisions & Tradeoffs — A decision matrix
- Open Questions — Four unresolved issues This structure itself reveals the assistant's thinking process. It starts with vision (the highest level of abstraction), drills down into architecture and components, then produces a concrete implementation plan with phases and timelines. It ends with tradeoffs and open questions — acknowledging uncertainty. This is the structure of a seasoned systems architect who knows that the value of a design document lies not in being right about everything, but in being explicit about everything. Let us now examine each section in depth, extracting the reasoning, assumptions, decisions, and knowledge embedded within.
Section 1: Vision & Positioning — "vLLM for Zero-Knowledge Proofs"
The message opens with a bold analogy: "cuzk is a persistent GPU-resident SNARK proving engine — think 'vLLM for zero-knowledge proofs.'" This single sentence reveals the assistant's entire conceptual framework. The assistant has spent significant effort studying GPU inference engine architectures (documented in message 68's task results), and is now explicitly mapping inference concepts onto proof generation concepts.
The table of analogies is worth quoting in full:
| Inference Concept | cuzk Equivalent | |---|---| | Model weights | SRS / Groth16 params (~47 GiB for PoRep, ~1 GiB for PoSt) | | Model loading/swapping | SRS loading/eviction from pinned memory | | Inference request | Proof request (C1 output + proof type + sector ID) | | KV cache / activations | Witness vectors, a/b/c evaluations, NTT intermediates | | Batch scheduling | Cross-sector proof batching (same circuit topology) | | Prefill vs decode | Synthesis (CPU) vs GPU compute (NTT/MSM) | | Model serving | Circuit-type serving (PoRep, WindowPoSt, WinningPoSt, SnapDeals) |
This mapping is not superficial. Each analogy is structurally precise:
- Model weights ↔ SRS: Both are large, immutable data structures loaded into GPU-accessible memory. Both are reused across many computation requests. Both dominate memory budgets. The key insight is that the existing architecture reloads SRS from disk for every proof — analogous to reloading a 47 GiB LLM from disk for every inference request. The inference engine community solved this problem years ago with persistent model loading.
- KV cache ↔ Witness vectors: Both are intermediate representations that are request-specific and memory-intensive. In LLM inference, the KV cache is the bottleneck for long sequences. In Groth16 proving, the witness vectors and NTT intermediates dominate working memory. The assistant is drawing on the inference engine community's techniques for managing these transient but large allocations.
- Prefill vs decode ↔ Synthesis vs GPU compute: This is perhaps the most important analogy. In LLM inference, the prefill phase is compute-bound (processing the prompt) while the decode phase is memory-bandwidth-bound (generating tokens one at a time). In Groth16 proving, synthesis (CPU-bound, constructing the R1CS witness) and GPU compute (NTT/MSM, GPU-bound) have a similar producer-consumer relationship. The inference engine community has developed sophisticated pipelining to overlap these phases — techniques that the assistant is now proposing to apply to proof generation. The "vLLM for ZK" framing is not just marketing. It is a deep structural insight that the proving problem is isomorphic to the inference problem at the level of resource management, scheduling, and pipelining. This insight drives the entire architecture.
The Reasoning Behind the Vision
Why did the assistant reach for inference engines as the mental model? The answer lies in the prior investigation. The assistant had just read about vLLM's sleep mode, Triton's model management, and TensorRT-LLM's batching strategies. It had also mapped the existing proof generation pipeline and identified that the fundamental problem was not computational speed but resource management: SRS loading dominated latency, memory fragmentation limited throughput, and the lack of persistent state meant every proof started from scratch.
The inference engine analogy provided a ready-made solution space. Instead of inventing a new resource management strategy from scratch, the assistant could adapt proven patterns from a mature ecosystem. This is a hallmark of good systems design: recognizing isomorphic problems and transferring solutions across domains.
Section 2: Architecture Overview — The Component Diagram
The architecture diagram in the message shows a layered system:
Curio (Go) → gRPC → cuzk daemon (Rust, tokio)
├── gRPC Server (tonic)
├── Scheduler
│ ├── Priority queues per proof type
│ ├── Batch collector
│ ├── GPU affinity tracking
│ ├── Deadline-aware scheduling
│ └── Memory budget enforcement
├── GPU Workers (one per GPU)
│ ├── SRS Manager (pinned memory)
│ ├── Synthesis (CPU pool)
│ └── NTT+MSM (CUDA)
└── SRS Memory Manager (global)
├── Hot: CUDA pinned host RAM
├── Warm: Regular heap (mmap)
└── Cold: Disk
This diagram encodes several design decisions:
Decision 1: Rust with tokio, not Go. The assistant chooses Rust as the implementation language, with the tokio async runtime. The rationale is stated in the tradeoffs table: "Direct access to bellperson/supraseal, no CGO overhead for hot path, better memory control." This is a significant decision because Curio itself is written in Go, and the existing ffiselect layer is Go. Choosing Rust means the daemon is a separate process, not a Go library. This has implications for deployment, debugging, and the skill set required.
Decision 2: gRPC over Unix socket, not shared memory. The assistant explicitly considered and rejected shared memory for the C1 output transfer. The reasoning is that at ~50 MB per PoRep C1 output, streaming over gRPC on a unix socket takes only ~5ms — well within acceptable latency. This avoids the complexity of shared memory management, reference counting, and lifecycle synchronization.
Decision 3: Separate scheduler and worker components. The scheduler is responsible for queue management, batching, and GPU affinity. The workers are responsible for actual proof execution. This separation allows the scheduler to make decisions based on global state (all queues, all GPUs, all SRS loads) while workers focus on efficient execution.
Decision 4: Global SRS memory manager, not per-GPU. The SRS manager is a global component that tracks all pinned memory across all GPUs. This is necessary because SRS files are large (~47 GiB for PoRep) and may need to be shared across GPUs or evicted when switching proof types. A per-GPU manager would lead to duplication and fragmentation.
Section 3: Proof Types & Circuit Profiles — The Resource Matrix
The message presents a table of resource requirements for each proof type:
| Proof Type | Constraints | FFT Domain | SRS Size | RAM/Proof | GPU VRAM | Partitions | Priority | |---|---|---|---|---|---|---|---| | PoRep C2 (32G) | ~130M | 2^27 | ~47 GiB | ~12-16 GiB | ~12-16 GiB | 10 | Normal | | PoRep C2 (64G) | ~130M | 2^27 | ~47 GiB | ~12-16 GiB | ~12-16 GiB | 10 | Normal | | SnapDeals (32G) | ~81M | 2^27 | ~30 GiB* | ~8-12 GiB | ~8-12 GiB | 16 | Normal | | WindowPoSt (32G) | ~125M | 2^27 | ~1-2 GiB | ~15-20 GiB | ~10-15 GiB | 1/part | High | | WinningPoSt (32G) | ~3.5M | 2^22 | ~200 MB | ~0.5-1 GiB | ~0.5 GiB | 1 | Critical |
This table is the output of the assistant's prior investigation into circuit sizes and resource profiles (messages 68, 70, 75). It encodes several critical insights:
Insight 1: The asymmetry of SRS sizes. PoRep and SnapDeals require ~30-47 GiB of pinned SRS data, while PoSt types require only ~200 MB - 2 GiB. This asymmetry drives the entire memory management strategy. On a machine with 96 GiB of RAM, you can keep one large SRS hot plus all PoSt SRS files simultaneously. On a machine with 256 GiB, you can keep everything hot.
Insight 2: WinningPoSt is tiny but critical. WinningPoSt has only ~3.5M constraints, an SRS of ~200 MB, and requires only ~0.5-1 GiB of RAM. But it has CRITICAL priority because it must complete within a Filecoin epoch (30 seconds) to avoid penalties. This means the scheduler must be able to jump a WinningPoSt proof to the front of the queue, potentially preempting a PoRep proof that has been running for minutes.
Insight 3: WindowPoSt has the highest per-proof RAM requirement. Despite having a small SRS, WindowPoSt requires ~15-20 GiB of working RAM per proof. This is because WindowPoSt proves over many sectors simultaneously, generating a large witness. This means WindowPoSt cannot be batched as aggressively as PoRep.
Insight 4: PoRep 32G and 64G have identical circuit sizes. The assistant notes that both sector sizes use ~130M constraints and the same 2^27 FFT domain. This is a subtle but important point: the circuit size depends on the proof type, not the sector size (for PoRep). This means the SRS is the same for both sector sizes, and batching can mix 32G and 64G proofs.
The asterisk on SnapDeals' SRS size (~30 GiB*) signals uncertainty. The assistant is noting that this is an estimate, not a confirmed value. This is honest about the limits of the investigation.
Section 4: The gRPC API Surface — Designing for Asynchrony
The protobuf schema in the message defines seven RPC methods:
- SubmitProof — Submit a proof request, return immediately with a job ID
- AwaitProof — Block until a submitted proof completes
- Prove — Submit + Await in one call (convenience)
- CancelProof — Cancel a pending or in-progress proof
- GetStatus — Query daemon capabilities and status
- GetMetrics — Prometheus-compatible metrics
- PreloadSRS / EvictSRS — Explicit SRS lifecycle management The Submit/Await split is the most important design decision in this API. It allows Curio to submit multiple proofs asynchronously, then await them independently. This enables the scheduler to reorder and batch proofs without blocking the submitter. The Prove method is a convenience wrapper for simple cases, but the real power is in the split API. The assistant's commentary on the API design reveals the reasoning:
- Submit/Await split: Allows Curio to submit multiple proofs, then await them independently. The scheduler can reorder/batch without blocking the submitter. - Prove (unary): Convenience for simple cases — submit and block until done. - PreloadSRS/EvictSRS: Explicit control for Curio to warm up SRS ahead of proof arrival, or free memory when switching workload types. - Priority levels: WinningPoSt is CRITICAL (preempts other work), WindowPoSt is HIGH (deadline-sensitive), PoRep/SnapDeals are NORMAL. - Vanilla proof inline: At ~50 MB per PoRep C1, streaming over gRPC on a unix socket is fine (~5ms transfer). No need for shared memory complexity.
The PreloadSRS/EvictSRS methods are particularly interesting. They give Curio explicit control over the SRS memory hierarchy. If Curio knows that a WindowPoSt deadline is approaching in 5 minutes, it can call PreloadSRS to ensure the WindowPoSt parameters are hot before the proof request arrives. This is analogous to vLLM's model loading API, where the orchestrator can pre-load models before routing requests to them.
The assistant also includes fields like request_id (for idempotency), sector_number, miner_id, and partition_index — all derived from the existing Curio task layer. This shows attention to how the daemon will integrate with the existing system, not just how it works internally.
Section 5: The SRS Memory Manager — Tiered Residency
This section is the heart of the architecture. The assistant proposes a three-tier memory hierarchy inspired by vLLM's sleep mode:
| Tier | Storage | Access Time | Description | |---|---|---|---| | Hot | CUDA pinned host RAM | Immediate | Ready for GPU DMA. Used during active proving. | | Warm | mmap'd file (page cache) | ~2-10s to re-pin | File is mmap'd, OS page cache may have it. Re-pinning = cudaHostRegister on mmap region. | | Cold | Disk only | 30-90s | Full deserialization from .params file. |
The Rust struct for the SRS manager shows the implementation:
struct SRSManager {
pinned_budget: u64,
hot: HashMap<CircuitId, Arc<SRS>>,
warm: HashMap<CircuitId, MmapHandle>,
ref_counts: HashMap<CircuitId, AtomicU32>,
lru: VecDeque<CircuitId>,
}
The eviction policy is carefully designed:
- Never evict an SRS with ref_count > 0 (in-flight proofs)
- Evict LRU entry with ref_count == 0 from hot → warm (unpin, keep mmap)
- If warm budget exceeded, evict LRU warm → cold (munmap)
- Before starting a proof, ensure the required SRS is hot. If not, promote warm→hot or cold→hot. This policy balances several competing concerns: - Safety: In-flight proofs are never interrupted by SRS eviction - Efficiency: LRU eviction minimizes the cost of future promotions - Predictability: Explicit budget prevents OOM The assistant then maps this strategy to specific machine configurations: Small machine (96-128 GiB RAM): - Pinned budget: ~48 GiB (one PoRep SRS + PoSt SRS) - When proving PoRep, PoSt SRS stays hot (only ~2 GiB) - When switching from PoRep to SnapDeals: evict PoRep SRS to warm, load SnapDeals SRS - Cost of swap: ~30-60s (acceptable if infrequent) Large machine (256+ GiB RAM): - Pinned budget: ~100-140 GiB - Keep PoRep + SnapDeals + all PoSt SRS hot simultaneously - Zero swap overhead for any proof type The assistant also proposes "proactive SRS warming" — the scheduler predicts upcoming SRS needs based on the queue and starts loading SRS data before the proof arrives. This is directly inspired by inference engines that pre-load models based on anticipated request patterns.
The Assumption Behind the Memory Manager
The SRS memory manager makes a critical assumption: that CUDA pinned memory can be allocated and freed without significant fragmentation. The assistant assumes that pinning an mmap'd region with cudaHostRegister and unpinning it with cudaHostUnregister is a fast operation (~2-10s for a 47 GiB region). This assumption is based on the existing supraseal code, which uses cudaHostRegister for its SRS cache. However, the assistant does not verify this assumption experimentally — it's derived from reading the code, not from measurement.
There's also an implicit assumption that the OS page cache will retain mmap'd data after unpinning. This is true under normal circumstances, but if the system is under memory pressure, the kernel may evict pages from the page cache, turning a warm→hot promotion into a cold→hot promotion. The assistant's estimated access times (~2-10s for warm, 30-90s for cold) suggest an awareness of this variability, but the design doesn't include mechanisms to prevent or detect page cache eviction.
Section 6: The Scheduler Design — Priority, Batching, and Affinity
The scheduler design is the most architecturally complex component. It has four layers:
- Admission Control: Enforces memory budget before accepting a proof
- Priority Queues: Three levels (CRITICAL, HIGH, NORMAL) with strict priority ordering
- Batch Collector: Accumulates same-circuit-type jobs into batches
- GPU Affinity Router: Routes proofs to GPUs that already have the right SRS loaded The scheduling rules are:
- CRITICAL preempts everything — WinningPoSt interrupts ongoing PoRep synthesis
- Batch accumulation — same-circuit-type jobs accumulate, flushed when batch full, timeout, or high-priority arrival
- GPU affinity — prefer GPU with matching SRS loaded
- Pipeline overlap — synthesize next batch while GPU computes current batch The assistant also defines two operating modes: Small Machine Mode (1 GPU, 96 GiB): - Batch size: 1 (no batching — insufficient RAM) - Pipeline depth: 2 (synthesize next while GPU proves current) - SRS strategy: one primary type hot, swap on demand - Reorder NORMAL queue to group by circuit type Large Machine Mode (4 GPUs, 512 GiB): - Dedicate GPUs by type: gpu:0,1 for PoRep, gpu:2 for WindowPoSt, gpu:3 floats - Batch size: 3 per GPU for PoRep - Pipeline depth: 3 (synthesis, GPU compute, result assembly overlap) - All SRS hot simultaneously This dual-mode design is a direct response to the user's requirement that the daemon work "both equally" on small and large machines. The assistant doesn't just scale parameters — it changes the scheduling strategy entirely based on available resources.
The Preemption Decision
One of the most carefully considered decisions is about preemption. The assistant states:
Preemption: Queue-level only (no mid-compute). CUDA kernels can't be safely interrupted; GPU phase is bounded (~85s max).
This is a pragmatic choice. True preemption (killing a running CUDA kernel) would require either:
- CUDA's built-in kernel preemption (available on some architectures, but with limitations)
- Running proofs in separate CUDA contexts with time-slicing
- Waiting for the GPU phase to complete before servicing the higher-priority request The assistant chooses the third option, accepting that a WinningPoSt proof might wait up to ~85s if a PoRep GPU phase is in progress. This is acceptable because WinningPoSt has a 30-second epoch window, but the assistant is implicitly assuming that the scheduler can detect the approaching deadline and preempt at the synthesis stage (before the GPU phase starts). If the WinningPoSt arrives during the GPU phase, it waits — which could miss the deadline. This is a potential weakness in the design. The assistant acknowledges it implicitly by noting that the GPU phase is "bounded (~85s max)" — suggesting that the worst-case wait is known and acceptable. But for a truly robust system, the daemon might need to reserve a GPU for WinningPoSt (as suggested in the large machine configuration) or use CUDA's MPS (Multi-Process Service) to share GPUs with finer granularity.
Section 7: The GPU Worker Pipeline — From Sequential to Pipelined
The GPU worker pipeline is described as a loop with seven steps:
- ACQUIRE job from scheduler
- ENSURE SRS is hot (may block on promotion)
- SYNTHESIZE witnesses (CPU thread pool)
- SUBMIT to GPU (generate_groth16_proofs_c)
- COLLECT proof results
- RESPOND to client (gRPC AwaitProof)
- RELEASE SRS ref (decrement ref_count) The critical insight is that steps 3 and 4 can be pipelined across sequential jobs: "Job N GPU compute || Job N+1 synthesis." This is the core pipelining win that Phase 2 enables. The assistant distinguishes between Phase 1 (scaffold, no pipelining) and Phase 2+ (pipelined with split synthesis/prove API). In Phase 1, the worker simply calls the existing
seal_commit_phase2()function — no upstream modifications needed. In Phase 2, after modifying bellperson to expose separate synthesis and prove functions, the worker can overlap these phases. This phased approach is a key architectural strategy: deliver value immediately with no upstream changes, then unlock more performance as modifications are made. The scaffold is not a throwaway prototype — it's a production-quality system that happens to not use pipelining yet.
Section 8: Library / Binary Design — The Crate Structure
The crate structure is:
extern/cuzk/
├── cuzk-core/ # Core library: Engine, Scheduler, SRS Manager, GPU Worker
├── cuzk-proto/ # Protobuf definitions
├── cuzk-server/ # gRPC server (thin wrapper over core)
├── cuzk-daemon/ # Standalone binary
└── cuzk-ffi/ # C ABI for embedding in Go (Curio)
This separation of concerns is standard Rust workspace design, but the inclusion of cuzk-ffi is notable. It provides a C ABI that Curio can link against using CGO, enabling three deployment modes:
- Mode A — exec into daemon: Curio binary includes cuzk-ffi linked in. On startup with
--prove-daemonflag, it exec's into the cuzk daemon mode. - Mode B — spawn as child: Curio runs
curio prove-daemonas a subprocess (like current ffiselect but persistent). - Mode C — external: Operator runs
cuzk-daemonindependently. Curio configured with socket path. All three modes use the same gRPC protocol — the only difference is process lifecycle. This is a clever design that maximizes deployment flexibility without complicating the core protocol. The Go client is a thin wrapper:
type Client struct {
conn *grpc.ClientConn
}
func (c *Client) Prove(ctx context.Context, req *cuzkpb.ProveRequest) (*cuzkpb.ProveResponse, error) {
return c.stub.Prove(ctx, req)
}
This replaces ffiselect.FFISelect.SealCommitPhase2() etc. in the Curio task code. The assistant notes that this is "feature-gated behind config flag" and "falls back to ffiselect if daemon unavailable" — a careful migration strategy that doesn't break existing deployments.
Section 9: The Phased Implementation Roadmap — 18 Weeks to 10x
The roadmap is the most concrete part of the message. It spans 18 weeks across 6 phases, each with specific deliverables, estimated impact, and stopping points.
Phase 0: Scaffold (Weeks 1-3) — "It proves with SRS residency"
The key insight of Phase 0 is that zero upstream modifications are needed to achieve a 25% throughput improvement. The daemon simply:
- Pre-populates
GROTH_PARAM_MEMORY_CACHEat startup by loading SRS once - Keeps the SRS in memory via
Arcreference counting - Calls the existing
seal_commit_phase2()function for each proof The SRS residency is achieved through a clever trick: the existingfilecoin-proofscrate uses alazy_staticHashMap calledGROTH_PARAM_MEMORY_CACHEto cache loaded parameters. By calling the parameter loading function at daemon startup (before any proof requests arrive), the parameters are loaded into this cache and stay there for the daemon's lifetime. Subsequent proof calls hit the cache and skip disk I/O. The assistant estimates this eliminates 30-90s of SRS load per proof, yielding ~25% throughput increase. This is immediate, measurable value with no risk of breaking existing functionality.
Phase 1: Multi-Type Scheduling (Weeks 3-5) — "It handles all proof types smartly"
Phase 1 adds the priority queue scheduler, SRS warm tier, GPU affinity tracking, and support for all proof types. The key deliverable is that WinningPoSt never misses a deadline due to PoRep hogging the GPU.
The SRS swapping strategy is described in detail:
- On a small machine: one large SRS hot at a time, PoSt SRS always hot (tiny)
- Swap = evict old to warm (unpin, keep mmap) + promote new from warm/cold
- Scheduler groups same-type proofs to minimize swaps
Phase 2: Pipelining (Weeks 5-8) — "GPU never idles"
Phase 2 requires the first upstream modification: splitting bellperson's create_proof_batch_priority_inner into separate synthesis and prove functions. The assistant describes this as a "small, low-risk modification" and provides a concrete plan:
- Extract
synthesize_circuits_batch()— already exists as an internal function - Create
prove_from_assignments()— new: takes pre-synthesized ProvingAssignment + SRS, runs GPU The estimated impact is GPU utilization from ~40% to ~70%, yielding ~1.5x throughput over Phase 0.
Phase 3: Batching (Weeks 8-11) — "Multiple sectors per GPU pass"
Phase 3 batches multiple sectors' circuits into a single generate_groth16_proofs_c call. This requires:
- Phase 2's split API (to synthesize multiple sectors before GPU submission)
- Bumping
max_num_circuitsingroth16_srs.cuhfrom 10 to 30+ - A batch collector in the scheduler The estimated impact is 2-3x throughput per GPU with batch=3.
Phase 4: Compute Optimizations (Weeks 11-14) — "Faster per-proof"
Phase 4 cherry-picks the highest-impact micro-optimizations from the earlier Proposal 4:
- SmallVec for LC Indexer: 15-30% synthesis speedup, ~5 LOC
- Pre-size vectors: 5-10% synthesis speedup
- Pin a,b,c with cudaHostRegister: +50% transfer bandwidth
- Parallelize B_G2 MSMs: 50s → 5s, one-line change
- batch_addition occupancy: 5-12% MSM speedup, one-line change Estimated impact: 30-40% faster per-proof on top of batching gains.
Phase 5: PCE — Pre-Compiled Constraint Evaluator (Weeks 14-18) — "Skip synthesis"
Phase 5 is the most ambitious: eliminating circuit synthesis overhead by pre-compiling R1CS constraint matrices. This is described as "the biggest single optimization" with 3-5x synthesis speedup.
The deliverables include:
- RecordingCS to extract fixed R1CS matrices into CSR format
- WitnessCS-based witness generation (skip enforce(), only run alloc())
- Sparse MatVec evaluator for a = A·w, b = B·w, c = C·w
- Coefficient-specialized MatVec (±1 skip multiply, boolean witness fast-path)
- Pre-sorted SRS topology for split MSM
The Cumulative Impact Table
The message includes a stopping-points table showing cumulative throughput at each phase:
| After Phase | Throughput vs Baseline | Minimum RAM | Key Win | |---|---|---|---| | 0 | 1.3x | 256 GiB (no change) | SRS residency | | 1 | 1.3x (scheduling) | 256 GiB | Mixed workload correctness | | 2 | 1.8x | 128 GiB | GPU pipelining | | 3 | 4-5x | 128 GiB | Batching | | 4 | 6-7x | 96 GiB | Compute wins | | 5 | 10x+ | 96 GiB | PCE |
The decreasing minimum RAM requirement is notable: as optimizations reduce per-proof memory, the daemon becomes viable on smaller machines. This is a deliberate design goal, responding to the user's requirement for both small and large machine support.
Section 10: Configuration — The TOML Schema
The configuration file schema shows the assistant's attention to operational details:
[daemon]
listen = "unix:///run/curio/cuzk.sock"
[memory]
pinned_budget = "50GiB"
working_memory_budget = "80GiB"
[gpus]
devices = []
# affinity: 0 = "porep-32g", 1 = "any"
[scheduler]
max_batch_size = 3
max_batch_wait_ms = 10000
sort_by_type = true
[synthesis]
threads = 0 # auto
[srs]
param_cache = "/var/tmp/filecoin-proof-parameters"
preload = ["porep-32g"]
[logging]
level = "info"
Every configuration parameter has a rationale. The pinned_budget of 50 GiB on a 96 GiB machine leaves 46 GiB for synthesis working memory and OS overhead. The max_batch_wait_ms of 10000 prevents proofs from waiting too long for a batch to fill. The sort_by_type option reduces SRS swaps on small machines. The preload array lets operators decide which SRS files to warm at startup.
The assistant also includes commented-out examples for GPU affinity, showing how to pin specific circuit types to specific GPUs. This is a power-user feature that requires understanding of the workload patterns.
Section 11: Test Harness — Golden Data and Test Modes
The test harness design shows the assistant thinking about testing strategy:
extern/cuzk/testdata/
├── porep-32g-c1.json
├── porep-32g-proof.json
├── snap-32g-vanilla.json
├── wpost-32g-vanilla.json
└── winning-32g-vanilla.json
Three test modes are proposed:
- Integration test: Load real SRS, prove with golden input, verify proof against expected output
- Mock test: Mock the supraseal FFI layer, test scheduling/SRS management/gRPC without GPU
- Benchmark: Repeated proofs with timing, memory tracking, GPU utilization The mock test mode is particularly important for development. It allows testing the scheduler, SRS manager, and gRPC API without requiring a GPU — enabling rapid iteration and CI/CD integration. The assistant is thinking about the development workflow, not just the final product.
Section 12: Key Design Decisions & Tradeoffs
The decision matrix summarizes the major architectural choices:
| Decision | Choice | Rationale | |---|---|---| | Language | Rust (tokio) | Direct access to bellperson/supraseal, no CGO overhead, better memory control | | RPC | gRPC (tonic) | Strongly typed, streaming, mature Rust+Go ecosystem | | SRS management | Tiered (hot/warm/cold) | Inference engine pattern, works for both small and large machines | | Batching granularity | Per-circuit-type | All circuits in a batch must share the same R1CS structure and SRS | | Preemption | Queue-level only | CUDA kernels can't be safely interrupted; GPU phase is bounded | | Library vs binary | Library with exec mode | Maximum flexibility: embed, standalone, or child process | | Upstream modifications | Phase 0: zero; Phase 2+: fork | Scaffold is immediately useful; deeper optimizations require controlled fork | | Error handling | Retry at daemon level | Daemon detects GPU OOM/fault, retries on different GPU or after SRS eviction |
Each decision is justified with a concise rationale. The assistant is not making arbitrary choices — each decision is traced back to a constraint or requirement discovered during the investigation phase.
Section 13: Open Questions — Honest Uncertainty
The message ends with four open questions:
- SnapDeals partition count: SnapDeals has 16 partitions vs PoRep's 10. Does the current supraseal code handle
num_circuits > 10? Need to verifymax_num_circuitshandling. - Non-supraseal path for PoSt: In default Curio builds, PoSt uses bellperson's native prover (not supraseal). Should cuzk support both prover backends? Or require
cuda-suprasealfor all proof types? - SnarkPack aggregation: Should cuzk also handle proof aggregation (CPU-only, no GPU)? Or leave that to Curio?
- Remote proving: The gRPC API supports TCP listeners for remote proving (proofshare marketplace). Should this be in scope for Phase 0, or deferred? These open questions are not weaknesses — they are evidence of intellectual honesty. The assistant knows the limits of its investigation and explicitly marks areas that need further exploration. This is a hallmark of good engineering: knowing what you don't know.
Assumptions Embedded in the Design
Let us now examine the assumptions that underpin this architecture. Some are explicit, some are implicit, and some may be incorrect.
Explicit Assumptions
- C1 output is ~50 MB, fits over gRPC: Verified by checking the actual file (message 72). Correct.
- SRS loading takes 30-90s: Based on the prior investigation of the existing pipeline. Reasonable but not experimentally verified for all proof types.
- CUDA pinned memory can be allocated/freed without fragmentation: Based on reading the supraseal code. Needs experimental validation.
- GPU phase is bounded at ~85s max: Based on the prior analysis of NTT/MSM timing. Reasonable for current hardware.
- The inference engine analogy is structurally valid: This is the core architectural insight. It is well-supported by the mapping table.
Implicit Assumptions
- The OS page cache will retain mmap'd SRS data after unpinning: This is true under normal conditions but can fail under memory pressure. The design doesn't include mechanisms to detect or recover from page cache eviction.
- The scheduler can predict SRS needs from the queue: This assumes that proof requests arrive with enough lead time to pre-load SRS. For burst workloads (many proofs arriving simultaneously), this may not hold.
- WinningPoSt can tolerate up to ~85s of waiting: The Filecoin epoch is 30 seconds. If a WinningPoSt proof arrives during the GPU phase of a PoRep proof, it will miss the deadline. The assistant assumes this is rare enough to be acceptable, but doesn't quantify the risk.
- The bellperson modification for Phase 2 is "small, low-risk": This is an engineering judgment that may prove optimistic when actually implementing.
- All proof types can use the supraseal CUDA backend: The assistant notes that PoSt currently uses bellperson's native prover in default builds. The design assumes this can be changed.
Potentially Incorrect Assumptions
- The SnapDeals SRS size is ~30 GiB (estimated): Marked with an asterisk, this is acknowledged as uncertain. If the actual size is significantly different, the memory budget calculations may be off.
max_num_circuitscan be bumped from 10 to 30+: This depends on GPU memory capacity and the CUDA kernel's internal assumptions. The assistant hasn't verified this experimentally.- The tiered memory hierarchy access times: The estimated times (immediate for hot, ~2-10s for warm, 30-90s for cold) are plausible but not measured. The warm→hot promotion time depends on
cudaHostRegisterperformance, which may vary by GPU and driver version. - The scheduler can reorder NORMAL-priority proofs without side effects: Reordering proofs changes the order in which sectors are sealed, which could affect Curio's task tracking and deadline calculations.
Knowledge Inputs and Outputs
Input Knowledge Required to Understand This Message
To fully understand message 77, one needs:
- Groth16 proof generation: Understanding of R1CS, witness synthesis, NTT, MSM, and the structure of a SNARK proof
- Filecoin proof types: PoRep (Proof of Replication), SnapDeals, WindowPoSt, WinningPoSt — what each proves and when it's needed
- CUDA memory model: Pinned host memory, device memory, DMA transfers, kernel launches
- GPU inference engine architecture: vLLM, Triton, TensorRT-LLM — model loading, scheduling, batching
- Rust async programming: tokio, tonic (gRPC), async task management
- Curio architecture: The ffiselect child process model, task layer, proof sharing
- The prior optimization proposals: The nine bottlenecks, 18 micro-optimizations, PCE design
Output Knowledge Created by This Message
This message creates:
- A complete architecture for a SNARK proving daemon: The first documented design for a persistent, GPU-resident proof generation system in the Filecoin ecosystem
- A phased implementation roadmap: 18 weeks, 6 phases, with clear deliverables and stopping points
- A resource requirement matrix: For all proof types, enabling capacity planning
- A gRPC API specification: The protobuf schema that defines the interface between Curio and the daemon
- A memory management strategy: The tiered hot/warm/cold hierarchy with eviction policies
- A scheduler design: Priority queues, batch accumulation, GPU affinity, deadline awareness
- A deployment model: Three modes (exec, spawn, external) with the same protocol
- A test strategy: Golden data, integration/mock/benchmark modes
- A configuration schema: TOML-based with sensible defaults
- A set of open questions: Areas requiring further investigation
The Thinking Process: From Investigation to Design
The most remarkable aspect of message 77 is not the architecture itself — it's the thinking process that produced it. Let us trace this process through the conversation.
Step 1: Understanding the Problem Space (Messages 68-76)
The assistant began by reading all seven prior proposals and background documents. This gave it a comprehensive understanding of the existing system: the call chain, the memory footprint, the bottlenecks, and the optimization opportunities.
Then it systematically explored every layer of the system:
- The ffiselect child process model (how Curio currently manages GPU proving)
- The SRS/parameter loading paths (how different proof types load their parameters)
- The supraseal C2 C++/CUDA API (the FFI boundary)
- The bellperson supraseal prover internals (the Rust-side implementation)
- The circuit sizes and resource profiles for each proof type This exploration was exhaustive. The assistant didn't just read documentation — it read source code, traced function calls, and built a mental model of the entire system.
Step 2: Finding the Right Analogy (Message 68)
The assistant then studied GPU inference engine architectures. This was a deliberate search for patterns — the assistant was looking for existing solutions to the resource management problem it had identified. The inference engine analogy provided:
- A vocabulary for describing the problem (model loading, request scheduling, memory hierarchy)
- A set of proven solutions (vLLM sleep mode, Triton model management, TensorRT-LLM batching)
- A framework for evaluating design choices
Step 3: Verifying Key Facts (Messages 72-75)
Before designing, the assistant verified critical assumptions:
- The C1 output size (checked the actual file: 50 MB, not GiB)
- The crate structure (checked the existing
extern/cuzk/directory) - The SRS construction path (traced through bellperson and filecoin-proofs) This verification prevented the assistant from designing based on incorrect assumptions. The original assumption was that C1 output was "~2-4 GiB" — the assistant corrected this to ~50 MB, which changed the gRPC transfer design.
Step 4: Synthesizing the Architecture (Message 77)
With all the investigation complete, the assistant synthesized the architecture. The synthesis process involved:
- Identifying the core value proposition: SRS residency (eliminate per-proof loading)
- Finding the minimal viable system: Phase 0 with zero upstream modifications
- Layering on complexity: Each phase adds one new capability
- Drawing from the analogy: The inference engine patterns provided templates for the scheduler, memory manager, and worker pipeline
- Accounting for constraints: Small vs large machines, different proof types, priority levels
- Acknowledging uncertainty: Open questions and estimated values
Step 5: Writing the Document
The final message is not just a design — it's a communication document. It's structured to be read by different audiences:
- Executives can read the vision and roadmap
- Architects can read the component design and API
- Developers can read the crate structure and implementation phases
- Operators can read the configuration and deployment modes The document uses tables, diagrams, code snippets, and bullet points to convey information efficiently. It's 13 sections of dense, well-organized content.
Mistakes and Potential Weaknesses
No design is perfect. Let us examine potential weaknesses in the cuzk architecture.
Weakness 1: The Preemption Gap
The most significant potential weakness is the preemption strategy. WinningPoSt proofs must complete within a Filecoin epoch (30 seconds). If a WinningPoSt arrives during the GPU phase of a PoRep proof (~85s), it will miss the deadline. The assistant's mitigation is to preempt at the synthesis stage (before GPU submission), but this doesn't cover the case where the GPU is already busy.
A more robust approach might be:
- Reserve one GPU exclusively for WinningPoSt (as suggested in the large machine configuration)
- Use CUDA MPS to share GPUs with finer granularity
- Implement true CUDA kernel preemption using CUDA streams and priorities The assistant acknowledges this implicitly by noting that the GPU phase is "bounded (~85s max)" and that the scheduler should be "deadline-aware." But the design doesn't fully solve the problem for single-GPU deployments.
Weakness 2: The Warm Tier Assumption
The warm tier assumes that mmap'd SRS data remains in the OS page cache after unpinning. This is a reasonable assumption under normal conditions, but it's not guaranteed. Under memory pressure, the kernel may evict pages, turning a warm→hot promotion into a cold→hot promotion (30-90s instead of 2-10s).
The design doesn't include mechanisms to:
- Detect page cache eviction
- Pre-emptively re-pin warm data before it's needed
- Fall back gracefully if a warm promotion takes longer than expected
Weakness 3: Batch Size Limits
The assistant assumes that max_num_circuits can be bumped from 10 to 30+. This is based on reading the CUDA kernel header file, but the actual limit depends on GPU memory capacity and the kernel's internal data structures. If the limit is hard-coded in multiple places (not just the header), changing it may require more extensive modifications.
Additionally, the assistant doesn't consider the interaction between batch size and GPU memory fragmentation. Larger batches require more GPU memory, which may increase fragmentation and reduce the effective batch size over time.
Weakness 4: The Bellperson Modification Risk
Phase 2 requires modifying bellperson to split synthesis and proving into separate functions. The assistant describes this as "small, low-risk," but any modification to a cryptographic library carries risk:
- The split might introduce subtle bugs in the proof generation logic
- The new API might not match the existing internal structure cleanly
- The modification might need to be maintained across bellperson versions The assistant mitigates this by proposing a fork (via Cargo
[patch]), but forks add maintenance burden.
Weakness 5: Single-Point-of-Failure Architecture
The daemon is a single process that manages all GPUs, all SRS memory, and all proof scheduling. If it crashes, all in-flight proofs are lost. The design doesn't include:
- Persistence of in-flight proof state
- Hot standby or failover
- Graceful degradation if a GPU fails For a production system that handles critical WinningPoSt proofs, this is a significant concern. The assistant may be assuming that Curio's existing retry logic handles this, but the daemon's crash would lose all queued proofs, not just the one being processed.
The Broader Significance: A New Category of Infrastructure
Message 77 is significant beyond its immediate context. It represents the design of a new category of infrastructure: a SNARK proving engine that treats proof generation as a service rather than a function call.
The existing architecture calls seal_commit_phase2() as a blocking function — load SRS, synthesize witnesses, compute proof, return result, free everything. Each call is independent, stateless, and starts from scratch. The cuzk architecture transforms this into a persistent service with stateful resource management, request scheduling, and pipeline parallelism.
This shift is analogous to the evolution of machine learning inference:
- Phase 1: Load model, run inference, free model (per-request)
- Phase 2: Keep model loaded, run multiple inferences (persistent)
- Phase 3: Schedule requests, batch for efficiency, manage memory (service) The Filecoin proof generation ecosystem is currently in Phase 1. Message 77 proposes jumping to Phase 3, drawing directly from the ML inference community's experience. This is not just an optimization — it's a re-architecture of how proof generation is done. The implications include: 1. New deployment topologies: Proving can be centralized in a daemon process, separate from the Curio task layer 2. New resource management strategies: SRS can be shared across proofs, swapped proactively, and managed with explicit budgets 3. New scheduling capabilities: Proofs can be prioritized, batched, and routed to specific GPUs 4. New failure modes: The daemon can retry on different GPUs, handle GPU OOM gracefully, and report detailed metrics
Conclusion: The Art of Systems Architecture
Message 77 is a masterclass in systems architecture. It demonstrates:
- Deep domain understanding: The assistant spent hours reading source code, tracing call chains, and understanding memory layouts. The architecture is grounded in this understanding.
- Cross-domain pattern transfer: The inference engine analogy is not superficial — it's structurally precise and drives concrete design decisions.
- Pragmatic incrementalism: Phase 0 delivers immediate value with zero risk. Each subsequent phase adds complexity only when the foundation is stable.
- Honest uncertainty: The open questions, estimated values, and asterisks show that the assistant knows the limits of its knowledge.
- Communication design: The document is structured for multiple audiences, with tables, diagrams, and code snippets that convey information efficiently.
- Tradeoff awareness: Every major decision is accompanied by a rationale and, in many cases, an alternative considered and rejected. The message is not the end of the conversation — it's the beginning of implementation. But it's a crucial milestone: the moment when understanding crystallizes into a plan. The cuzk architecture, as designed in message 77, represents a new way of thinking about SNARK proof generation — not as a function call, but as a service. Whether the implementation succeeds or fails, the architecture document stands as a model of how to design complex systems: investigate thoroughly, find the right analogies, design incrementally, and be honest about uncertainty. That is the lesson of message 77.