Overlapping CPU bookkeeping with GPU math bought Moondream 35% more throughput on a B200
The GPU wasn't waiting for more math. It was waiting for the CPU to tell it what to compute next. Moondream's engineers rebuilt the decode loop so the two never wait on each other.
In LLM token generation the GPU often sits idle, not for lack of work but because the CPU hasn't finished housekeeping for the next token. Moondream calls that gap a GPU bubble and rebuilt its Photon engine to hide it, overlapping the next forward pass with the sampling of the previous one.
The payoff scales with the machine. On an RTX 3090 the trick adds about 6.5%; on a B200, where CPU sync is a bigger slice of a tiny 2.4ms forward pass, it adds 35.4%. An inference engineer pushed back: the win looks this big only because the model is small.
The GPU was idle, and the CPU was the reason

Moondream's engineering post opens on a counterintuitive measurement: during token generation the GPU frequently does nothing, and it isn't starved for math. It's waiting for the CPU to finish the bookkeeping for the next token. They call that idle stretch a GPU bubble (a gap in the compute pipeline, not a financial one), and the whole post is about closing it inside Photon, their inference engine, to hit near-realtime vision-language inference of roughly 33ms on an NVIDIA B200.
The naive decode loop is strictly sequential: the CPU prepares a step, launches the forward pass, waits for it, samples a token, then starts over. Every handoff between CPU and GPU is a moment where one of them stalls.
The top of that timeline is the problem: CPU work and GPU work take turns. The bottom is the goal: while the GPU chews on step t+1, the CPU is already committing and sampling step t. Nothing waits.
Two slots, a copy that runs behind the GPU's back
To overlap steps you need somewhere to put step t+1's inputs and outputs without clobbering step t's, which the CPU is still reading. Photon uses ping-pong slots: two alternating DecodeSlot bundles, each holding input staging, logit output, sampled tokens, and KV-cache bookkeeping. Odd and even steps use different slots, so the GPU can write one while the CPU reads the other.
The second trick is the deferred copy. Reading the sampled token back from GPU to CPU normally forces a sync. Photon puts that device-to-host copy on a separate CUDA stream, an ordered queue the GPU drains in order where work on separate streams can overlap, so the token copy runs while the GPU is already busy with the next forward.
Forward now, sample later, and the sequences that refuse to die
The scheduler runs each cycle as an advance tick with three phases: launch the forward for step t+1, commit step t, then finalize sampling for t+1. The ordering matters because constrained-decoding masks depend only on already-committed state, never on results still in flight on the GPU.
That pipelining creates a subtle bug class. A sequence can hit its end-of-sequence token at step t while it's already batched into step t+1 on the GPU. Killing it immediately would corrupt the in-flight batch. Photon keeps it alive as a zombie: a finalized flag marks it done after EOS or the length cap, while an inflight_refs counter tracks how many in-flight steps still reference it. The slot is released only when that count hits zero.
The numbers: the payoff grows with the GPU
The measured per-step cost splits into a forward pass and a sampling step. The faster the GPU, the more the fixed CPU overhead dominates the period:
| Hardware | Config | Forward (ms) | Sampling (ms) | Period (ms) |
|---|---|---|---|---|
| 3090 | 1 stream | 4.87 | 0.20 | 5.10 |
| 3090 | 8 streams | 6.66 | 0.27 | 6.97 |
| 3090 | 32 streams | 10.24 | 0.26 | 10.52 |
| B200 | 1 stream | 2.45 | 0.14 | 2.63 |
| B200 | 8 streams | 3.12 | 0.14 | 3.30 |
| B200 | 32 streams | 3.80 | 0.14 | 3.98 |
Against a blocking baseline, the pipelined loop's advantage climbs from single digits on a 3090 to over a third on a B200:
| Hardware | Config | Blocking (ms) | Pipelined (ms) | Predicted | Observed |
|---|---|---|---|---|---|
| 3090 | 1 stream | 5.44 | 5.10 | +5.7% | +6.5% |
| 3090 | 8 streams | 7.52 | 6.97 | +7.6% | +7.8% |
| 3090 | 32 streams | 11.74 | 10.52 | +11.1% | +11.6% |
| B200 | 1 stream | 3.11 | 2.63 | +17.2% | +17.6% |
| B200 | 8 streams | 4.04 | 3.30 | +22.2% | +21.9% |
| B200 | 32 streams | 5.55 | 3.98 | +39.1% | +35.4% |
The measured B200 timeline confirms the intent: with pipelining the GPU is busy for essentially the entire period, the bubble squeezed out.
The thread: a great trick, or a small-model artifact?
The sharpest response came from someone inside the discipline, who read the same tables and drew the opposite lesson.
"the blog is nice but it has a lot of CODEX fingerprints on it, and it's also very specific to the size of the model in question… 2.4ms FW-pass is tiny, and 9B params sit on a single GPU. Large models are closer to 30-40ms. The CPU-GPU sync is 1-2ms… much less important than for example scheduling of computation/communication or kernel optimization." — an engineer who works in the field
That is the load-bearing caveat: a 1–2ms CPU sync is 35% of a 2.4ms forward pass and barely 5% of a 40ms one, which is exactly why the B200 column looks so much better than the 3090 column. The bigger the model, the smaller this bubble as a share of the whole.
Most of the thread, though, snagged on the title. A former ML-company founder put the confusion plainly:
"This is true, but I've never heard anyone refer to this as a GPU bubble before… most people hear 'GPU bubble' and think of a financial bubble of some kind." — a former ML-company founder and startup CTO
Another reader was blunter, calling the naming a deliberate bait for a term that means a market crash, and suggesting GPU-CPU pipeline stall instead. One commenter offered a rescue analogy: propeller cavitation on a ship, where water-influx rate and blade design decide how much efficiency the bubbles cost — the GPU is the propeller, the work is the influx.
The takeaway for a reader shipping inference: this technique earns its keep when your forward pass is small enough that CPU overhead is a real fraction of it. On a large MoE model, the thread's insider is right that you'd spend your effort on communication overlap and kernels first.
Get breakdowns like this every day
The top Hacker News stories, unpacked — by email, Telegram, or RSS.
The US labor share hit a post-war low. The NY Fed's verdict: nothing new
Workers' cut of national income is at its lowest since WWII, down 1.6 points from before the pandemic. The New York Fed studied whether this time is different and answered its own headline: no.