The Bellperson Bridge: Routing GPU Mutex Access Through the Dependency Chain

In the middle of implementing Phase 8 of the cuzk SNARK proving engine—a dual-worker GPU interlock designed to eliminate idle gaps in Groth16 proof generation—the assistant encountered a classic systems-architecture problem: a function existed in the right place but was invisible to the code that needed it. Message [msg 2203] captures the moment of diagnosis and the decision to route through an existing dependency bridge. It is a short message—barely a sentence of reasoning followed by a single file edit—but it encapsulates a critical architectural insight that reveals the structure of the entire proving stack.

The Message

The full text of the subject message is:

Bellperson already depends on supraseal_c2. Let me add alloc/free wrappers to bellperson's supraseal module and re-export them: [edit] /home/theuser/curio/extern/bellperson/src/groth16/prover/supraseal.rs Edit applied successfully.

The Context That Produced This Message

To understand why this message exists, one must trace the implementation thread backward through the preceding messages. The assistant had been systematically building Phase 8: Dual-Worker GPU Interlock across seven files and approximately 195 lines of changes. The core idea was to eliminate GPU idle time by narrowing the scope of a C++ static mutex in generate_groth16_proofs_c so that CPU preprocessing and the b_g2_msm computation could run outside the lock, allowing two GPU workers per device to interleave their work.

The implementation proceeded in a carefully ordered sequence of steps. First, the C++ CUDA kernel was refactored to accept a passed-in mutex pointer with narrowed scope ([msg 2181]). Then FFI plumbing was added in supraseal-c2/src/lib.rs to expose the mutex operations to Rust ([msg 2182]). The Rust bellperson crate was updated to thread the mutex parameter through prove_from_assignments and create_proof_batch_priority_inner. The pipeline module and engine were updated to spawn multiple workers per GPU device, each sharing a per-GPU C++ mutex. Finally, a gpu_workers_per_device configuration option was added.

At message [msg 2200], the assistant entered Step 7: Build and verify. It force-rebuilt the C++ code and ran cargo build --release -p cuzk-daemon. The build output (visible in [msg 2201]) shows warnings about dead code in an unrelated test module, but more importantly, the build appears to have surfaced a compilation error or at least prompted the assistant to investigate the dependency graph more carefully.

The Discovery: A Visibility Problem

Message [msg 2202] reveals the diagnostic moment. The assistant states: "The engine can't directly access supraseal_c2 — it needs to go through bellperson or pipeline." This is the critical realization. The assistant had just created alloc_gpu_mutex and free_gpu_mutex functions in supraseal-c2, but the engine code in cuzk-core does not—and should not—directly depend on supraseal-c2. The dependency chain is layered: cuzk-daemon depends on cuzk-core, which depends on bellperson, which depends on supraseal-c2. The engine lives in cuzk-core and accesses GPU functionality exclusively through the bellperson crate's public API.

This layered architecture is intentional. supraseal-c2 is a low-level C++/CUDA binding crate that handles the raw FFI calls to the CUDA kernels. bellperson is a higher-level Rust library that provides the Groth16 proving abstractions, wrapping the FFI calls in safe Rust interfaces. cuzk-core is the engine that orchestrates the proving pipeline. Each layer has a distinct responsibility, and the dependency direction is strictly one-way: engine → bellperson → supraseal-c2. For the engine to call supraseal_c2::alloc_gpu_mutex() directly would violate this layering and create a tighter coupling than intended.

The Decision: Route Through Bellperson

The assistant's response in [msg 2203] is immediate and decisive. Rather than restructuring the dependency graph, adding a re-export in the pipeline module, or making supraseal-c2 a direct dependency of the engine, the assistant chooses the path of least architectural disruption: add wrapper functions in bellperson's existing supraseal module and re-export them.

The reasoning is explicit in the message's opening statement: "Bellperson already depends on supraseal_c2." This is the key insight. The dependency already exists; the functions just need to be surfaced through it. The bellperson crate already has a module at src/groth16/prover/supraseal.rs that wraps supraseal-c2 functionality—it contains the generate_groth16_proof calls and the SRS parameter handling. Adding alloc_gpu_mutex and free_gpu_mutex wrappers there is consistent with the existing pattern and requires no new dependencies.

The assistant uses the word "wrappers" deliberately. These are not complex transformations; they are thin delegation functions that expose the C++ mutex allocation and deallocation through bellperson's public API. The pattern mirrors what the module already does for generate_groth16_proof: accept parameters, call the FFI function, return the result. In this case, alloc_gpu_mutex returns a raw pointer (wrapped in the SendableGpuMutex newtype for Send/Sync safety), and free_gpu_mutex takes the pointer and destroys the underlying C++ std::mutex.

The Edit and Its Aftermath

The edit itself is applied to /home/theuser/curio/extern/bellperson/src/groth16/prover/supraseal.rs. The message reports "Edit applied successfully" but does not show the diff—the reader must infer the content from context. However, the subsequent message ([msg 2204]) confirms the next step: "Now re-export from bellperson's groth16 mod." This tells us the wrappers were added to the supraseal module and then re-exported through bellperson/src/groth16/mod.rs, making them available as bellperson::groth16::alloc_gpu_mutex() and bellperson::groth16::free_gpu_mutex().

This two-step pattern—add wrappers in the implementation module, then re-export from the parent module—is idiomatic Rust. It keeps the internal structure organized while presenting a clean public API. The engine code can now call bellperson::groth16::alloc_gpu_mutex() without ever knowing that the underlying implementation lives in supraseal-c2.

Architectural Significance

While [msg 2203] is a single edit to a single file, it reveals the architectural philosophy of the entire proving stack. The layered dependency graph is not accidental; it reflects a deliberate separation of concerns:

Assumptions and Corrections

The assistant made an implicit assumption during the initial implementation: that the engine could call supraseal_c2 functions directly. This assumption was reasonable given that the engine already imports from crate::pipeline, which in turn uses bellperson types. But the assumption was corrected when the build revealed (or the assistant's analysis of the codebase revealed) that no direct dependency path existed.

This kind of assumption correction is routine in large-scale refactoring. When working across multiple crates with layered dependencies, it is easy to forget which crate depends on which. The assistant's response was exemplary: rather than fighting the architecture, it worked within it, using the existing dependency chain as the conduit for the new functionality.

Conclusion

Message [msg 2203] is a small but revealing moment in the Phase 8 implementation. It demonstrates that even carefully planned multi-file refactors can encounter unexpected architectural constraints during the build phase. The assistant's response—route through the existing bellperson dependency rather than restructuring the dependency graph—shows architectural discipline and awareness of the existing codebase conventions. The edit itself is trivial (adding wrapper functions), but the reasoning behind it touches on fundamental questions of crate layering, dependency management, and API design in large Rust projects. In the broader context of the cuzk proving engine, this message represents the moment when a theoretical design (the dual-worker interlock) met the practical constraints of the build system and the dependency graph, and emerged with a solution that strengthened rather than compromised the existing architecture.