Allow more than one worker thread per GPU device #4

Closed
opened 2026-09-03 09:08:44 +00:00 by grenade · 1 comment
Owner

Part of #1. The cheapest change on the list: hides the per-batch readback bubble on the origin wgpu engine with no kernel work.

Problem

run_single_batch in crates/engine-gpu/src/lib.rs is synchronous: write buffers, submit, block on the fence, map, read, unmap, then the worker computes the next midstate and submits again. One worker per card means the card is idle for the whole turnaround. At the default 1M batch a 5090 finishes the kernel in about 2.7 ms, so a few hundred microseconds of turnaround is a double-digit loss.

Why it is cheap

The engine already supports it by accident. Worker threads get a device by device_counter % contexts.len() (thread-local ASSIGNED_GPU_DEVICE), and each thread owns its own GpuResources (thread-local WORKER_RESOURCES, its own buffers and bind group). Two threads on one wgpu::Device and Queue are legal; wgpu serialises at the queue and the GPU executes the two submissions back to back. So while thread A is in readback, thread B's dispatch is already queued and the card stays busy.

The only thing preventing it is the count check in resolve_gpu_configuration (crates/miner-service/src/lib.rs, "Requested N GPU devices but only M available").

Change

  • New flag --gpu-workers-per-device <N>, default 1 (origin behaviour). Total GPU workers = devices x N.
  • resolve_gpu_configuration validates requested_devices <= available as today, then multiplies. WorkerPool::new is unchanged; it just gets a bigger gpu_devices count.
  • miner_gpu_devices metric keeps reporting devices, not workers, so the fleet dashboard reads the same.
  • Each worker still picks its own random 512-bit start nonce, so two workers on one card search disjoint space at no coordination cost, exactly like two cards do.

About 20 lines, all in the two files #1 already allows a standing diff in.

Measure

Harness runs at N = 1, 2, 3 on one 5090 at the 1M default and at 16M. Expect N = 2 to recover most of the bubble at 1M and matter less at 16M. If N = 3 is not better than N = 2, cap the flag at 2 in the docs.

Interaction with other workstreams

Superseded on the wgpu engine by proper double-buffering, and irrelevant to engine-cuda which pipelines internally. Worth doing first anyway: it ships in an afternoon, and its number is the baseline the double-buffering issue has to beat.

Origin coupling

None beyond the flag. If origin later adds its own pipelining, the flag becomes a no-op and can be dropped in that merge.

Part of #1. The cheapest change on the list: hides the per-batch readback bubble on the origin wgpu engine with no kernel work. ## Problem `run_single_batch` in `crates/engine-gpu/src/lib.rs` is synchronous: write buffers, submit, block on the fence, map, read, unmap, then the worker computes the next midstate and submits again. One worker per card means the card is idle for the whole turnaround. At the default 1M batch a 5090 finishes the kernel in about 2.7 ms, so a few hundred microseconds of turnaround is a double-digit loss. ## Why it is cheap The engine already supports it by accident. Worker threads get a device by `device_counter % contexts.len()` (thread-local `ASSIGNED_GPU_DEVICE`), and each thread owns its own `GpuResources` (thread-local `WORKER_RESOURCES`, its own buffers and bind group). Two threads on one `wgpu::Device` and `Queue` are legal; wgpu serialises at the queue and the GPU executes the two submissions back to back. So while thread A is in readback, thread B's dispatch is already queued and the card stays busy. The only thing preventing it is the count check in `resolve_gpu_configuration` (`crates/miner-service/src/lib.rs`, "Requested N GPU devices but only M available"). ## Change - New flag `--gpu-workers-per-device <N>`, default 1 (origin behaviour). Total GPU workers = devices x N. - `resolve_gpu_configuration` validates `requested_devices <= available` as today, then multiplies. `WorkerPool::new` is unchanged; it just gets a bigger `gpu_devices` count. - `miner_gpu_devices` metric keeps reporting devices, not workers, so the fleet dashboard reads the same. - Each worker still picks its own random 512-bit start nonce, so two workers on one card search disjoint space at no coordination cost, exactly like two cards do. About 20 lines, all in the two files #1 already allows a standing diff in. ## Measure Harness runs at N = 1, 2, 3 on one 5090 at the 1M default and at 16M. Expect N = 2 to recover most of the bubble at 1M and matter less at 16M. If N = 3 is not better than N = 2, cap the flag at 2 in the docs. ## Interaction with other workstreams Superseded on the wgpu engine by proper double-buffering, and irrelevant to `engine-cuda` which pipelines internally. Worth doing first anyway: it ships in an afternoon, and its number is the baseline the double-buffering issue has to beat. ## Origin coupling None beyond the flag. If origin later adds its own pipelining, the flag becomes a no-op and can be dropped in that merge.
grenade added the needs-benchmarkperf labels 2026-09-03 09:10:16 +00:00
Author
Owner

Closing as superseded by measurement. The bubble this flag would hide is 1.9% of a batch on the 4090 (miner_gpu_batch_seconds, #9) and the CUDA engine's batches are shorter still. The harness already supports oversubscribing a card (--workers above the card count, #11) for anyone who wants to measure it; a miner flag for under 2% is not worth the surface. If pipelining is ever revisited it is #5.

Closing as superseded by measurement. The bubble this flag would hide is 1.9% of a batch on the 4090 (`miner_gpu_batch_seconds`, #9) and the CUDA engine's batches are shorter still. The harness already supports oversubscribing a card (`--workers` above the card count, #11) for anyone who wants to measure it; a miner flag for under 2% is not worth the surface. If pipelining is ever revisited it is #5.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: blackbeard/miner#4