The Throttle Check: A Single Edit That Bridges C++ and Rust Memory Bandwidth Coordination

Subject Message (msg 2791): [assistant] Now add the throttle check in \spmv_parallel\: [edit] /home/theuser/curio/extern/cuzk/cuzk-pce/src/eval.rs Edit applied successfully.

Introduction

At first glance, message 2791 appears to be one of the most mundane entries in a coding session: a single edit command, a file path, and a confirmation. Yet this message is the keystone of a delicate cross-language coordination mechanism — the final piece of Intervention 3 in a months-long optimization campaign targeting the SUPRASEAL_C2 Groth16 proof generation pipeline for Filecoin's Proof-of-Replication (PoRep). The edit it describes adds a throttle check inside the spmv_parallel function in Rust, completing a global atomic signaling pathway that allows C++ code running on GPU worker threads to tell Rust code running on rayon threads to yield the CPU when they are competing for scarce DDR5 memory bandwidth. This message is the moment where a carefully reasoned design becomes executable reality.

Context: The Memory Bandwidth Crisis

To understand why message 2791 exists, one must understand the bottleneck it addresses. The optimization journey documented in this session had already identified DDR5 memory bandwidth contention as the primary performance limiter for the cuzk SNARK proving engine. Phase 9 had achieved a baseline of 38.0 seconds per proof at configuration c=20 j=15 (20 concurrent proofs, 15 synthesis jobs). Phase 10's ambitious two-lock GPU interlock had been abandoned after discovering fundamental CUDA device-global synchronization conflicts. Phase 11 was designed as a more targeted response: three interventions to reduce memory bandwidth contention.

Intervention 1 serialized the async_dealloc calls with a static mutex to reduce TLB shootdowns. Intervention 2 reduced the groth16_pool thread count from 192 to 32, cutting L3 cache thrashing. Both had been implemented and benchmarked by the time we reach message 2791. Intervention 2 alone had delivered a 3.4% improvement, bringing throughput to 36.7 seconds per proof. But the analysis revealed a deeper problem: the b_g2_msm computation — a multi-scalar multiplication on the G2 curve that runs on the CPU during proof generation — was competing with the synthesis pipeline's sparse matrix-vector multiplication (SpMV) for memory bandwidth.

The key insight, documented in the Phase 11 design specification, was that b_g2_msm and SpMV both perform bandwidth-intensive random-access reads on large data structures. When they run concurrently — as they inevitably do in a dual-worker GPU configuration — they thrash the L3 cache and saturate the DDR5 memory bus, causing both to slow down. The solution was Intervention 3: a global atomic throttle flag that the C++ code sets around b_g2_msm, which the Rust SpMV code checks periodically and yields its CPU time when the flag is raised.

The Design Journey: From Spec to Implementation

Message 2791 is the culmination of a design process that began in the Phase 11 specification document (c2-optimization-proposal-11.md). The spec had proposed a mechanism where Rust would alias a C++ atomic variable through an unsafe pointer. But as the assistant worked through the implementation in message 2790, it reconsidered this approach:

"The design in the spec has Rust aliasing a C++ atomic through unsafe, which is fragile. Let me reconsider."

This moment of reflection is crucial. The assistant recognized that the spec's approach — sharing a C++ AtomicI32 across the FFI boundary — was unnecessarily risky. The two code paths (C++ b_g2_msm and Rust spmv_parallel) run in the same process but on different thread pools. They don't need to share memory; they just need a signaling mechanism. The assistant's revised design was simpler and safer: a Rust-side static AtomicI32 in the cuzk-pce crate, with a C-callable FFI function to set and clear it.

Message 2790 implemented the foundation: the global MEMBW_THROTTLE static atomic and the set_membw_throttle FFI function. Message 2791 adds the consumer side — the actual check inside spmv_parallel that reads the flag and yields when it's set. Message 2792 (immediately following) adds the producer side in C++: calls to set_membw_throttle(1) before b_g2_msm and set_membw_throttle(0) after.

What the Edit Actually Does

The edit described in message 2791 modifies the spmv_parallel function in /home/theuser/curio/extern/cuzk/cuzk-pce/src/eval.rs. This function is the heart of the PCE (Pre-Compiled Constraint Evaluator) — it performs row-parallel sparse matrix-vector multiplication to compute the a, b, and c vectors for each partition of the Groth16 proof. The SpMV is parallelized across rayon threads, and each thread processes a range of rows independently.

The throttle check works as follows: at the beginning of each row iteration (or periodically within the iteration), the code reads MEMBW_THROTTLE.load(). If the value is non-zero, indicating that b_g2_msm is running on another thread, the current thread calls std::thread::yield_now() to voluntarily yield its CPU time slice. This allows the memory controller to service b_g2_msm's requests without contention from the SpMV threads.

The design is intentionally lightweight. It doesn't use a spinlock or busy-wait — just a yield_now hint to the scheduler. The assumption is that b_g2_msm runs for about 1.7 seconds (as measured with 32 threads), and during that window, the SpMV threads can afford to slow down slightly if it means the overall system throughput improves. The check itself is a single atomic load with Ordering::Relaxed, which on x86 is essentially free (a compiler barrier but no actual memory fence instruction).

Assumptions and Trade-offs

The throttle mechanism embodies several assumptions that deserve scrutiny:

First, it assumes that b_g2_msm and SpMV genuinely compete for the same memory bandwidth resource. This was supported by the Phase 11 waterfall timing analysis, which showed that GPU utilization dips correlated with CPU-side memory-intensive operations. But the assumption is probabilistic — the contention depends on which cache lines each thread is accessing, the NUMA topology, and the memory controller's arbitration policy. The throttle is a heuristic, not a guarantee.

Second, it assumes that yielding the CPU is the right response. An alternative would be to pin threads to specific CCDs (core complexes on AMD EPYC processors) to physically separate the memory traffic. The assistant had considered this in earlier analysis but chose the atomic flag approach because it's simpler and doesn't require OS-level affinity management.

Third, it assumes that the overhead of checking the atomic on every row iteration is negligible. For a 32-thread SpMV processing millions of rows, each check adds an atomic load and a conditional branch. The assistant's reasoning was that a relaxed atomic load is essentially free on x86, and the branch predictor will correctly predict the "not throttled" path most of the time. But if b_g2_msm runs for 1.7 seconds out of a ~57 second total prove time, the throttle is active only ~3% of the time, so the misprediction penalty is minimal.

Fourth, the design assumes that the FFI function set_membw_throttle is safe to call from C++ code. The function is defined in Rust with extern "C" linkage and uses unsafe internally to access the static atomic. The C++ code in groth16_cuda.cu calls it without any synchronization — it's a simple store to a global variable. This is safe because the atomic provides the necessary memory ordering guarantees.

Input Knowledge Required

To understand message 2791, one must be familiar with several layers of the system:

  1. The Groth16 proving pipeline: How proof generation is split into partitions (10 per proof), each requiring synthesis (constraint evaluation via SpMV) and GPU kernel execution (NTT, MSM).
  2. The PCE (Pre-Compiled Constraint Evaluator): A mechanism that pre-compiles the fixed R1CS constraint matrices into a format that allows fast row-parallel SpMV, avoiding redundant LinearCombination construction.
  3. The dual-worker GPU interlock: Phase 8's architecture where two GPU workers share a single GPU mutex, allowing CPU preprocessing to overlap with GPU kernel execution.
  4. The memory bandwidth contention problem: How DDR5 bandwidth is a shared resource that, when saturated, causes all threads on the same socket to slow down.
  5. Rust's atomic types and FFI: How AtomicI32 with Relaxed ordering works, how extern "C" functions are defined and called across language boundaries.
  6. Rayon parallelism: How the SpMV uses rayon's parallel iteration, and how yield_now interacts with rayon's work-stealing scheduler.

Output Knowledge Created

Message 2791 creates the consumer half of a cross-language signaling mechanism. The output is a modification to eval.rs that:

  1. Reads the MEMBW_THROTTLE static atomic at the start of each SpMV row iteration.
  2. Calls std::thread::yield_now() when the throttle is active.
  3. Integrates seamlessly with the existing row-parallel iteration structure. This edit, combined with the FFI setter added in message 2790 and the C++ calls added in message 2792, completes the throttle mechanism. The system now has a feedback path from the GPU worker thread (running b_g2_msm in C++) to the synthesis threads (running SpMV in Rust).

The Thinking Process

The assistant's reasoning is visible in the surrounding messages. In message 2790, the assistant walks through the design:

"The simplest approach is a global static atomic in the cuzk-pce crate that: 1. C++ sets via an FFI function before b_g2_msm, clears after 2. Rust's spmv_parallel checks periodically"

This reveals a pragmatic engineering mindset. The assistant considered and rejected the spec's more complex approach (aliasing C++ memory through unsafe pointers) in favor of a simpler design. The reasoning is explicit: "The gpu_resources pointer flows through engine.rs to gpu_prove() in pipeline.rs. But synthesis runs on a different thread pool and different code path. We'd need to make the throttle pointer globally accessible."

The assistant then considered two alternatives: (a) making the throttle a static AtomicI32 in C++ and exposing it via an FFI getter, or (b) using a Rust-side static AtomicI32. It chose (b) because it's "even simpler" — the Rust code that checks the flag is in the same crate as the flag, so there's no FFI overhead on the read side.

Why This Message Matters

Message 2791 is easy to overlook because it's so brief. But it represents the moment when a carefully reasoned design crosses the threshold from theory to practice. The edit it describes is the last piece of Intervention 3 — the throttle check that makes the entire mechanism useful. Without this check, the MEMBW_THROTTLE flag would be set and cleared by C++ code with no effect. The Rust SpMV threads would continue competing for memory bandwidth, and the 3.4% improvement from Intervention 2 would be the final gain.

The message also illustrates a recurring pattern in this optimization work: the assistant operates in a tight feedback loop of design, implementation, measurement, and refinement. Each intervention is small and targeted, with a clear hypothesis about what it will achieve. The throttle check is no exception — it's a minimal change (a few lines of code) that implements a specific idea (yield during b_g2_msm) derived from a specific analysis (waterfall timing showing memory bandwidth contention).

Conclusion

Message 2791 is a single edit that completes a cross-language, cross-thread-pool memory bandwidth coordination mechanism. It is the implementation of the consumer side of Intervention 3's throttle flag, adding a yield check to the SpMV function in Rust's PCE evaluator. The message is brief, but it sits at the intersection of deep system knowledge: CUDA kernel execution, CPU memory architecture, Rust FFI, atomic operations, and parallel algorithm design. It embodies the assistant's methodology of iterative, hypothesis-driven optimization, where each small change is grounded in careful analysis of the previous round's measurements. The throttle check may seem like a minor addition, but it is the culmination of a design process that considered multiple alternatives, rejected fragile approaches, and settled on a simple, safe mechanism that bridges the gap between C++ GPU worker threads and Rust synthesis threads.