The Small Fix That Nearly Broke the Build: Visibility, Assumptions, and the Art of C++ Refactoring in CUDA

In the middle of a deep optimization campaign targeting the SUPRASEAL_C2 Groth16 proof generation pipeline for Filecoin's Proof-of-Replication (PoRep), a seemingly trivial message appears — message index 2381 in the conversation. The assistant writes:

I see the problem — I'm calling ntt_msm_h::lg2() and ntt_msm_h::gib but lg2 is private in the class and gib is file-scope (not in the class at all). I need to: 1. Make lg2 public in the class 2. Reference gib directly (it's file-scope, accessible from groth16_cuda.cu since the file is #included)

>

Let me fix the class to make lg2 public:

On its surface, this is a three-line realization followed by a one-line edit. But this message sits at a fascinating intersection of assumptions, codebase archaeology, and the invisible scaffolding that makes large-scale refactoring possible. It is a microcosm of the entire optimization campaign that preceded it — a campaign spanning dozens of messages, multiple subagent sessions, and hundreds of lines of CUDA kernel code.

The Context: Phase 9 PCIe Transfer Optimization

To understand why this message matters, one must understand what the assistant was trying to accomplish. The broader effort was Phase 9: PCIe Transfer Optimization, targeting two root causes of GPU idle gaps identified in the Phase 8 baseline of the cuzk SNARK proving engine.

The first root cause was that approximately 6 GiB of non-pinned a/b/c polynomial data was being uploaded to the GPU inside a mutex lock. This meant that while one GPU worker was transferring data across PCIe, all other workers were blocked waiting for the mutex — and the GPU itself was idle during the transfer. The fix was to "pre-stage" these uploads: pin the host memory with cudaHostRegister, allocate device buffers, and issue asynchronous cudaMemcpyAsync transfers on a dedicated CUDA stream before acquiring the mutex. CUDA events would then synchronize the transfers inside the mutex without blocking.

The second root cause involved per-batch hard sync stalls in the Pippenger MSM, addressed by double-buffered host result buffers.

This was not a trivial change. It required modifying three files: groth16_ntt_h.cu (adding new execute_ntts_prestaged() and execute_ntt_msm_h_prestaged() functions), groth16_cuda.cu (inserting the pre-staging logic and cleanup), and the pippenger.cuh header (for the double-buffering change).

The Mistake: Calling Private Members from Outside the Class

In message 2377, the assistant edited groth16_cuda.cu to add the pre-staging logic. In that edit, it wrote code like:

prestage_lg_domain = ntt_msm_h::lg2(npoints - 1) + 1;

and:

size_t d_a_sz = sizeof(fr_t) << (ntt_msm_h::lg2(points_h.size() - 1) + 1);

The assistant assumed that lg2 — a static utility function that computes the base-2 logarithm — was a public member of the ntt_msm_h class. It also wrote ntt_msm_h::gib, assuming gib was a class member.

Neither assumption was correct.

The Discovery Process

The assistant did not immediately realize the mistake. It first completed the edit (msg 2377), then added cleanup code (msg 2378). Only afterward, in message 2379, did a moment of doubt surface:

I also need to make the lg2 and gib accessible from groth16_cuda.cu. Currently lg2 is a private static in the ntt_msm_h class and gib is a file-scope constant. Since I'm referencing ntt_msm_h::lg2 and ntt_msm_h::gib from the outer file, I need to make them public. Let me check:

The assistant then read the file to verify. Message 2380 confirmed the situation:

I see that gib is a file-scope constant (not inside the class) and lg2 is a private static method. The gib is accessible from groth16_cuda.cu since groth16_ntt_h.cu is #included. But lg2 is private.

The assistant also ran a grep to check whether groth16_cuda.cu had its own lg2 function (it didn't — only the older supraseal and supra_seal directories had local lg2 definitions).

This grep is a critical detail. The assistant was not just fixing the access specifier blindly — it was checking whether there was an alternative lg2 available in the same file, which would have been a simpler fix. Finding none, it concluded that the only path forward was to change the access specifier in the class definition.

The Fix: One Line, Multiple Implications

The fix itself was a single edit: changing private: to public: for the lg2 static method in the ntt_msm_h class. But this seemingly trivial change carries significant weight.

In C++, access specifiers are not just syntactic sugar — they encode design intent. The original author of ntt_msm_h (likely from Supranational LLC, given the copyright header) chose to make lg2 private because it was an internal utility, not part of the public API. Changing it to public is a commitment: future maintainers can now call ntt_msm_h::lg2() from anywhere, and the class's encapsulation boundary has been weakened.

The assistant's reasoning reveals an awareness of this tradeoff. It considered two options:

  1. Make lg2 public (the chosen fix)
  2. Reference gib directly as a file-scope constant (not ntt_msm_h::gib) For gib, the assistant correctly noted that it is a file-scope const size_t defined at line 30 of groth16_ntt_h.cu, outside any class. Since groth16_ntt_h.cu is #included by groth16_cuda.cu (they are compiled as a single translation unit), gib is directly accessible — no class qualification needed. The assistant's code was simply wrong to write ntt_msm_h::gib.

What Knowledge Was Required

To recognize and fix this problem, the assistant needed:

  1. C++ access control semantics: Understanding that private members cannot be accessed from outside the class, even by code in the same translation unit.
  2. CUDA compilation model knowledge: Knowing that groth16_ntt_h.cu is #included into groth16_cuda.cu (a common pattern in CUDA projects to avoid separate compilation overhead), meaning file-scope symbols like gib are directly accessible without qualification.
  3. Codebase archaeology: Knowing where lg2 was defined, what its access specifier was, and whether alternative definitions existed elsewhere.
  4. The distinction between static class members and file-scope constants: gib is a plain const size_t at file scope, not a class member. The assistant had to recognize that ntt_msm_h::gib was syntactically wrong.
  5. The impact on the broader optimization: Understanding that this fix was necessary for the pre-staging logic to compile, and that without it, the entire Phase 9 optimization would fail at build time.

What Knowledge Was Created

This message produced several forms of knowledge:

  1. A corrected class definition: lg2 is now public, enabling the pre-staging code to compile.
  2. Documentation of the gib scope: The assistant confirmed that gib is file-scope and should be referenced without class qualification.
  3. A record of the decision process: The grep results show that the assistant checked for alternative lg2 definitions before modifying the class — a defensive programming practice.
  4. An implicit design decision: By making lg2 public, the assistant implicitly decided that the utility function was general enough to warrant public access, rather than duplicating it or finding another approach.

The Broader Pattern: Iterative Refinement in AI-Assisted Coding

This message exemplifies a pattern that recurs throughout the entire optimization campaign: the assistant makes an assumption, implements code based on that assumption, then discovers the assumption was wrong through verification, and corrects it.

The pattern is:

  1. Assume (write code assuming lg2 is public)
  2. Verify (read the file to check)
  3. Discover (find that lg2 is private)
  4. Diagnose (grep for alternatives, confirm the scope of gib)
  5. Fix (change the access specifier) What makes this pattern interesting is that the assistant does not simply accept its first guess. It proactively reads files, runs grep searches, and cross-references findings before committing to a fix. This is not a "hallucination" in the traditional sense — the assistant is not generating incorrect code and moving on. Rather, it is engaging in a form of exploratory programming, where code is written, tested against the codebase's actual structure, and corrected as discrepancies emerge.

The Human Analogy

A human developer working on this codebase would likely follow a similar path. They might:

  1. Open groth16_cuda.cu and start writing code
  2. Type ntt_msm_h::lg2(...) based on the assumption that it's public
  3. Either notice the access specifier immediately (if they had the class definition open) or discover the error at compile time
  4. Navigate to the class definition, see the private: label, and change it to public: The assistant's process differs in one key respect: it discovered the error before compilation, through proactive file reading. This is possible because the assistant has the ability to read files and grep for patterns without going through a full build cycle. In a sense, the assistant is performing a static analysis of its own code before the compiler gets a chance to reject it.

The Assumptions Made

Several assumptions underpin this message:

  1. That lg2 should be public: The assistant assumes that making lg2 public is the correct fix, rather than, say, adding a public wrapper method or duplicating the function in groth16_cuda.cu. This is a reasonable assumption — lg2 is a pure utility function (it computes floor(log2(x))), and there is no security or encapsulation reason to keep it private.
  2. That no other code depends on lg2 being private: Changing an access specifier from private to public is backward-compatible — existing code that already accesses lg2 (none, since it was private) continues to work, and new code gains access. However, if any code relied on lg2 not being in the public interface (e.g., for API documentation or interface contracts), that assumption is violated. In practice, for an internal CUDA implementation file, this is unlikely to matter.
  3. That the file-scope gib is accessible: The assistant correctly assumes that gib (defined at file scope in groth16_ntt_h.cu) is accessible from groth16_cuda.cu because the former is #included into the latter. This is a standard C++ compilation model assumption.
  4. That no name collision exists: The assistant assumes that gib (GiB = 2^30 bytes) is not defined elsewhere in the translation unit with a different value. Given that it's a const size_t with a clear name, this is safe.

The Mistakes

The primary mistake was writing ntt_msm_h::lg2() and ntt_msm_h::gib without first verifying the access specifiers and scope. This is a classic "write now, verify later" approach that works well when the cost of verification is low (a file read) and the cost of being wrong is moderate (a compile error).

A secondary mistake was using ntt_msm_h::gib at all. Since gib is not a class member, this syntax is doubly wrong — it's both accessing a non-existent class member and implying the wrong scope. The assistant corrected this by noting that gib should be referenced directly.

The Output Knowledge

After this message, the codebase has:

Conclusion

Message 2381 is a small but revealing moment in a much larger optimization story. It shows that even in a highly automated, AI-assisted coding session, the fundamentals of C++ access control still apply. It demonstrates the iterative, verification-driven approach that characterizes effective code modification — especially when working across file boundaries in a complex CUDA codebase. And it reminds us that every line of code, even a one-line access specifier change, carries assumptions about design intent, compilation model, and codebase structure that must be verified against reality.