engine-gpu: double-buffered submission so the GPU never drains between batches #5
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part of #1. The proper fix for the same bubble the workers-per-device issue papers over. Applies to origin's wgpu engine, which stays our fallback on any host where
engine-cudais unavailable and is the engine on non-NVIDIA hardware.Current loop
Per batch in
run_single_batch(crates/engine-gpu/src/lib.rs):queue.write_buffercalls (dispatch config, start nonce, midstate, results reset)queue.submitmap_asyncon staging,device.poll(Wait)until the callback firesThe GPU is idle from the end of step 2's execution until the next iteration's step 2 reaches the hardware. With a 2.7 ms kernel that gap is the loss.
Design
Two resource sets per worker (
GpuResourcesA and B), each with its own results and staging buffer. The loop becomes:map_asyncfor A is issued right after A's submit, so by the time we wait, it is usually already complete.Where the code goes
This one has to touch origin's
engine-gpu; there is no way around it since the loop is the thing. Keep the diff mergeable:crates/engine-gpu/src/pipelined.rs(ours) containing the double-bufferedsearch_range.GpuEngine::search_rangedispatches to it behind apipelined: boolset from a new flag--gpu-pipelined(default on once measured, off until then). Origin'srun_single_batchstays byte-identical so origin fixes to it merge clean; we call it with per-set resources.GpuResourcesgains a constructor for a second set.create_resourcesis already a method onGpuContext, so this is a second call, not a change.Estimated diff in origin-owned code: the dispatch branch and the resources call, marked
// lair:.Measure
Harness: pipelined off vs on, one 5090, at 1M and 16M batch, workers-per-device = 1. Then pipelined on with workers-per-device = 2 to confirm they do not stack (they should not; if they do, the bubble was bigger than modelled and both stay).
Origin coupling
Moderate. If origin restructures
search_rangethe merge conflicts in the one branch point. If origin ships its own pipelining, prefer theirs and deletepipelined.rs.Measured ceiling from #9's batch phase histogram on benjy (4090, 1M batch): the host side of a batch is 0.13 ms against 6.79 ms on the GPU, 1.9% of batch time. That is the most double-buffering can recover on this card at the default batch size; about 5% on a 5090 where batches are 2.7 ms. Same ceiling applies to #4. #6 captures most of it by itself. Priority of this issue drops accordingly; it stays worth doing only as part of the wgpu fallback engine's tidy-up, after #3.
Closing as superseded. The bubble this was meant to hide was measured at 1.9% of batch time on the 4090 and about 5% on a 5090, and #26 recovered what was recoverable on the engine that actually runs: batches are now whole grids, two per launch, which took beast from 2146 to 2205 MH/s (+2.8%) and benjy +1%; going to four or eight nonces per thread measured neutral to -1.2%, so the remaining gap between launches is not worth a second stream on the CUDA engine either. The quanpool kernel does run two streams, and its profile (#27) puts its advantage in instruction count and register footprint, not in launch overlap.
For the wgpu fallback this remains true in principle, but that engine now only runs on NVIDIA hosts without a CUDA driver, and touching origin's
search_rangefor a fallback path is exactly the coupling #1 says to avoid.