"The API can juggle a hundred requests. The GPU can run exactly one. Those are two different numbers, and the design has to honor both."
Two Kinds of Concurrency, Often Confused
An async server is built to accept many requests at once — that's what keeps it responsive. The trap is assuming that 'accept many' should mean 'execute many.' For most web work it does. For GPU inference it absolutely must not: the API layer should stay happily concurrent while the actual GPU work runs strictly one job at a time. Conflating these two is how a responsive server turns into a crashed one under load.
Why Two Inferences at Once Is the OOM Again
Recall the previous lesson: a single diffusion inference has a real peak memory cost. Run two simultaneously and you double that peak. On a finite device, doubling peak memory is the exact road back to the out-of-memory crash you just fixed. The inference_mode fix bounded one job's memory; running two jobs at once unbounds it again. Concurrency at the GPU undoes the memory discipline everywhere else.
The Semaphore of One
The mechanism is small: a concurrency gate that permits exactly one holder at a time, wrapped around the GPU section of the adapter. The job queue still accepts everything and runs all the non-GPU work concurrently — validation, queueing, response streaming. But when a job reaches the actual inference, it must acquire the single permit. The second job waits at that gate until the first releases it. The API never blocks; only the GPU section serializes.
Why Not Just a Bigger Machine?
You could buy more memory and run more concurrent inferences — but that just moves the ceiling, it doesn't remove it. However much memory you have, some number of concurrent inferences exceeds it, and without a gate you'll find that number the hard way, in production. The semaphore makes the engine correct at any memory size: it never tries to run more GPU work than the device can hold, regardless of how big the device is. Correctness beats capacity.