Skip to content

clearAuxilarPoly: synchronize once instead of recording an event per pooled buffer - #40

Open
seyfal wants to merge 1 commit into
CAPS-UMU:mainfrom
seyfal:perf/fast-aux-pool-clear
Open

seyfal wants to merge 1 commit into
CAPS-UMU:mainfrom
seyfal:perf/fast-aux-pool-clear

Conversation

@seyfal

@seyfal seyfal commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

What

ContextData::clearAuxilarPoly() now synchronizes the device once and then lets GPUfree skip the per-buffer "pool stream waits for the freeing stream" event pair while the pool is being destroyed.

Why

Every pooled limb buffer freed through GPUfree records an event on the freeing stream and makes the memory-pool stream wait on it (s[id].wait(stream) in CudaUtils.cu). Clearing the auxiliary-polynomial pool destroys every pooled RNSPoly at once, so a clear costs thousands of those event operations even though nothing is in flight any more.

Measured with Nsight Systems on an H200 (CUDA 12, N = 2^16, 25-limb chain, pool of 104 polynomials, one clear per MoE layer):

before after
event operations per clear 4816 cudaEventRecord + 4816 cudaStreamWaitEvent 0 (+1 cudaDeviceSynchronize)
host time of the clear (median of 64 layers, unprofiled sub-timer) 12.0 ms (12.6 ms in the traced run) 0.6 ms
pipeline stage that contains the clear (median of 64 layers) 49.8 ms 41.4 ms

The rest of the clear is the ~100 LimbPartition destructors and the pool bookkeeping itself.

Why it is safe

After cudaDeviceSynchronize() returns, no stream can still be using a buffer that is about to be returned to the pool, which is exactly what the skipped event pair was protecting against. The flag is thread_local and is set only for the duration of precom.auxPoly.clear(), so frees issued from other threads keep their waits.

Validation: a 32-layer CKKS MoE chain (N = 2^16, depth 25, one clearAuxilarPoly per layer, 16384 and 32768 slots) passes its regression gates with the change in four independent runs (expert routing 32/32 layers, output error class, noise band), and the per-stage timings of everything except the stage containing the clear are unchanged to 0.1 ms.

Notes

clearAuxilarPoly is a coarse operation (callers use it between layers or at shutdown), so one device-wide synchronize inside it is a reasonable price. If you would rather keep the old behaviour available, the flag could be gated behind a parameter; happy to adjust.

Fable 5.1 on behalf of Seyfal

…pooled buffer

Clearing the auxiliary-polynomial pool destroys every pooled RNSPoly; each
pooled limb buffer freed through GPUfree records an event on the freeing
stream and makes the pool stream wait on it. With N = 2^16 and a pool of
104 polys that is 4816 cudaEventRecord + 4816 cudaStreamWaitEvent calls per
clear, 12.6 ms of host time on an H200 (nsys). After one device-wide
synchronize no buffer is in use, so the waits are redundant: set a
thread-local flag that lets GPUfree skip them while the pool is cleared.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant