2.4

The overlap scheduler

The overlap scheduler prepares batch N+1 on the CPU while the GPU runs batch N, and at v0.5.22 it is the default path.

Profile a simple serving loop and you find gaps on the GPU timeline between decode steps. In each gap the GPU waits while Python reads the sampled tokens, checks stop strings, frees memory, and builds the next batch. A decode step on a small model takes a few milliseconds, so a gap of one millisecond is a large share of the step.

When you finish this section, you can explain how SGLang hides the CPU work between steps and say when the overlap is off. You can decide whether --disable-overlap-schedule belongs in your command.

The cost: CPU time between GPU steps

Overlap scheduling: running the CPU work for one batch at the same time as the GPU work of another batch.

The SGLang team described the problem in the v0.4 release post. Batch scheduling, memory allocation, and prefix matching leave the GPU idle between steps. Their answer is a scheduler that runs one batch ahead and prepares all the metadata for the next batch. They report a 1.1x throughput gain from it. That number is their claim, measured on their GPUs.

The obvious fix is a second thread that builds batches while the first one runs the GPU. Python threads share one interpreter, so a scheduler thread and a forward thread take turns. SGLang tried a thread first. Since pull request #11210 in October 2025 it uses two CUDA streams in one thread instead.

How it works on CUDA

The loop launches batch N+1 on the GPU first, and then processes the results of batch N on the CPU. The GPU never waits for the bookkeeping of the step before.

There is a problem. Batch N+1 needs the tokens that batch N sampled, and those tokens are still on the GPU when N+1 is built. SGLang builds N+1 with placeholder token ids. A FutureMap fills in the placeholders on the GPU from the output of N. The CPU never needs to read the tokens before the launch.

Fig. 2.5the same three decode steps without and with overlaptimeline
The same three decode steps without and with overlap Two timelines with a GPU lane and a CPU lane. Without overlap, each GPU step is followed by a hatched gap while the CPU processes its results, and three steps end at 500. With overlap, the GPU runs steps back to back while the CPU processes the previous step under the next one, and both lanes finish three steps at 420. WITHOUT OVERLAP: EVENT_LOOP_NORMAL GPU CPU WITH OVERLAP: EVENT_LOOP_OVERLAP GPU CPU t step 1 step 2 step 3 three steps by 500 the GPU waits for the CPU step 1 step 2 step 3 three steps by 420 the CPU processes step N under step N+1
Without overlap the GPU waits for CPU bookkeeping after every step, and with overlap the CPU processes step N while the GPU already runs step N+1. Read left to right in time. Plum bars are GPU forward passes, blue bars are CPU work, grey hatched bars are idle GPU. The shape is schematic, the loop order is from event_loop_overlap at v0.5.22.

How it works on a Mac

The MLX backend has its own overlap loop, event_loop_overlap_mlx. MLX evaluates lazily, so the loop keeps two decode graphs queued with mx.async_eval. The docstring says that the GPU runs the next step right after the current one, with no scheduling gap on the device. The chain breaks when a prefill arrives, a request finishes, or the waiting queue has new work. Then the loop builds a fresh step.

The scheduler is the same Python scheduler on CUDA and on MLX. The batching, the radix tree bookkeeping, and the log lines in section 2.2 are the same. Only the forward pass and the overlap mechanism differ.

What happened when we turned it off

The Apple silicon page of the SGLang docs lists --disable-overlap-schedule as one of the key parameters for the MLX backend. We added it to our server command and ran the concurrency 8 benchmark. The server answered its own warm-up and the benchmark warm-up. Then the scheduler died on the first batch of the real run.

the scheduler crash with --disable-overlap-schedule on MLXtextFull log
[2026-10-16 14:07:15] Prefill batch, #new-seq: 1, #new-token: 1, #cached-token: 511, token usage: 0.00, #running-req: 0, #queue-req: 0, #pending-token: 0, cuda graph: False, input throughput (token/s): 0.39
[2026-10-16 14:07:16] Scheduler hit an exception: Traceback (most recent call last):
…
    scheduler.event_loop_normal()
…
  File "/private/tmp/sglang-research/sglang-v0.5.22/python/sglang/srt/managers/scheduler.py", line 4746, in run_batch
    self._relay_forward_payload(
  File "/private/tmp/sglang-research/sglang-v0.5.22/python/sglang/srt/managers/scheduler.py", line 4852, in _relay_forward_payload
    self.future_map.stash(future_indices, payload)
  File "/private/tmp/sglang-research/sglang-v0.5.22/python/sglang/srt/managers/overlap_utils.py", line 639, in stash
    self.output_tokens_buf[indices] = payload.bonus_tokens.to(
…
RuntimeError: Expected all tensors to be on the same device, but found at least two devices, mps:0 and cpu!
The traceback is cut between the event loop and run_batch. File paths are from our checkout of v0.5.22.

Read the stack from the bottom. With the overlap off, the dispatcher picks event_loop_normal, the generic loop. That loop calls run_batch, which still relays the sampled tokens through the FutureMap of the overlap machinery. On MLX the buffer lives on the Metal device and the sampled tokens arrive on the CPU, and PyTorch refuses to mix them. The overlap loop for MLX does not take this path, so the default server never reaches it.

The main process then shut everything down. Every one of the 32 benchmark requests failed: 8 were cut off during their response, and 24 could not connect after the server was gone.

the client side of the crashtextFull log
Warmup completed with 1 sequences. Starting main benchmark run...
…
requests.exceptions.ConnectionError: HTTPConnectionPool(host='0.0.0.0', port=30000): Max retries exceeded with url: /server_info (Caused by NewConnectionError("HTTPConnection(host='0.0.0.0', port=30000): Failed to establish a new connection: [Errno 61] Connection refused"))
The first characters of two of the 32 error lines, and the final line. Each error line holds a full traceback.

It failed this way in our one MLX recording. We report it as we found it: a flag that the platform docs recommend crashed the platform at the pinned release.

The same kit configuration ran on CUDA without errors, on the L4 and on the GB10, with 64 of 64 requests served. Each card ran it once with the overlap on and once with it off, at 8 users. The two results disagree:

Card, modelOutput tok/s, overlap on / offMedian TPOT, overlap on / off
L4, Qwen3-8B73.71 / 84.1791.24 / 69.38 ms
GB10, Qwen3-30B-A3B78.92 / 73.5997.30 / 103.21 ms

On the L4 the server without the overlap was 14 percent faster, and on the GB10 it was 7 percent slower. These are single runs without clock records, so a hot card can explain either one. They do not show the 1.1x gain that the SGLang team reports, and they do not disprove it.

A GPU incident with the same root

On CUDA, a user ran with overlap on and hit an allocation failure right after KV cache pool is full at 94 percent token usage. Under overlap, the next batch is planned while the tokens of the previous one are still being computed. Retraction had to reason about requests that would finish one step later. A fix proposed in pull request #15626 closed without merge. At v0.5.22 the scheduler removes requests that will finish by length from the next decode batch. It also skips retraction in a step where finishing requests will free KV.

Both stories have one lesson. Overlap is a second copy of the timeline in the scheduler's head, and every feature has to agree with it.

Our position

Leave the overlap on unless a measurement on your own card says otherwise. It is the default, and it is designed to hide CPU time that you would otherwise pay on every step. Our two single runs on CUDA disagreed, so measure both settings with a clock record before you change it.

Turn it off for one reason: to rule it out while you debug a wrong output or a hang. Expect the off path to be less tested, and test it on your hardware before you depend on it. SGLang itself turns the overlap off where it cannot work, for example on Apple MPS without MLX and for speculative decoding on CPU.

SGLang 0.5.22 · baa9bef · 2026-10-13 · Edition 2026.10