Skip to content

WebGPU: GPUBuffer.mapAsync() and GPUQueue.onSubmittedWorkDone() always take >= ~10 ms, even with an idle GPU #37042

Open

Description

@JackGillard

GPUBuffer.mapAsync() never resolves in less than about 11 ms on Windows, even when nothing is queued on the GPU, and GPUQueue.onSubmittedWorkDone() takes about 15 ms. For readback-heavy workloads (e.g. running a small neural net once per step and reading the result back) this fixed delay dominates: in our case it was most of the wall time of a single inference loop.

Repro (no dependencies; deno run --unstable-webgpu repro.ts):

const adapter = await navigator.gpu.requestAdapter();
const device = await adapter!.requestDevice();
const dst = device.createBuffer({ size: 256, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST });
const t: number[] = [];
for (let i = 0; i < 200; i++) {
  const s = performance.now();
  await dst.mapAsync(GPUMapMode.READ); // nothing is queued on the GPU
  t.push(performance.now() - s);
  dst.unmap();
}
t.sort((a, b) => a - b);
console.log(`mapAsync p50 ${t[100].toFixed(2)} ms`);

Deno 2.9.0, Windows 11, RTX 4090 (p50 of 200 calls, two runs each):

stock 2.9.0 with the fix below
mapAsync, nothing queued 11.39 / 11.46 ms 0.01 / 0.01 ms
tiny copy + submit + mapAsync 15.53 / 15.52 ms 0.06 / 0.06 ms
empty submit + onSubmittedWorkDone 15.44 / 15.42 ms 0.04 / 0.04 ms

Cause. In ext/webgpu/buffer.rs (map_async) and ext/webgpu/queue.rs (on_submitted_work_done) the polling future is:

while !*done.borrow() {
  self.instance.device_poll(self.device, wgpu_types::PollType::wait_indefinitely()).unwrap();
  tokio::time::sleep(Duration::from_millis(10)).await;
}

device_poll(wait_indefinitely()) blocks until the queue is idle, and the map / work-done callback fires inside that poll, which sends on the oneshot. But the loop then always sleeps 10 ms before it re-checks done (set by the receiver future on the same task), so every call pays at least one full sleep after the GPU is already finished; on Windows the timer rounds that up to about 11-15 ms.

Suggested fix: yield instead of sleeping (PR to follow). wait_indefinitely already blocks until the work is done, so the sleep is not needed for pacing.

Separately, and not addressed by that fix: the blocking wait_indefinitely poll runs on the JS thread inside the op, so the JS thread cannot do other work while the GPU computes. Moving that poll onto a blocking-pool thread let JS keep running in our local tests; that is a larger change, so it is only mentioned here.

This issue was investigated and written with the help of an AI assistant (Claude); the measurements were run on my machine.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions