← back to the corkboard
Field guide · October 24, 2026

Inside SGLang

What happens between your prompt and the first token: a field guide to SGLang, the open-source engine that puts a model on GPUs and answers thousands of requests at once, written from its source code, with small simulations you can poke.

SGLang is an open-source inference engine: the server software that takes a model's weights, puts them on GPUs, and answers thousands of requests at once. I wrote this guide to walk through how it works, using the real code at github.com/sgl-project/sglang, with small simulations you can poke.

Most chapters take 5-10 minutes (chapter 5, the scheduler, takes longer). Chapters 1-3 cover ideas every engine shares, vLLM included. Chapters 4-8 open up SGLang itself. Chapters 9-13 are for when you want to run it or read the source.

What you already know
  • Prompts go in, tokens come out, and you pay per token
  • Streaming, temperature, context windows
  • Output tokens cost more than input tokens
  • Some APIs discount "cached" input tokens
What this guide adds
  • Why those prices and limits are shaped the way they are
  • How one GPU serves hundreds of users at once
  • What SGLang's RadixAttention caches, and when it helps
  • Enough vocabulary to read the code, the docs and the logs

Why inference engines exist

In one line

Running a model for one person is a short Python script. Running it for a thousand people at once, without wasting a GPU that rents for a few dollars an hour, is a scheduling and memory problem. That is the problem inference engines solve.

If you download an open model from Hugging Face, you can generate text in about five lines with the transformers library: load the weights, call model.generate(), print the result. That works, and it gets you roughly 1-5% of what the same GPU can do when it serves many users.

Three things go wrong when that script becomes a service:

  1. One request at a time wastes the GPU. Generating a token for one user moves all the model's weights through the chip. Generating a token for 64 users in the same pass moves the same weights once. Batching is close to free throughput, and the engine's job is to keep the batch full.
  2. Users arrive and leave at random. One person wants 3 tokens ("Yes."), another wants 3,000. A batch that waits for its slowest member wastes most of its seats. Engines add and remove requests between every step.
  3. Memory runs out before compute does. Every token in every conversation leaves behind a block of saved numbers on the GPU (the KV cache). How many users you can serve is mostly a question of how well you manage that memory.

An answer is written one token per decode step. In each step the GPU reads every weight of the model once and makes the next token for every request in the batch. Below is that trip for Llama 3.1 8B (its weights fill 16 GB) on one H100 GPU.

Each request also keeps its own KV cache (the saved numbers from problem 3), and every step reads those too. So the length of each conversation matters as well as how many there are. All the caches have to fit in the memory the weights leave free, about 55 GB on this GPU.

One trip for the weights
Simulation
GPU memoryHBM · 80 GBweights · 16 GB, read every stepKV caches · 8.6 of 55 GB3.35 TB/s25 GB per stepCompute coreslayer 15 of 32+48 more ride along

64 requests share one trip through the weights. The step grows from 4.83 ms to 7.36 ms and now makes 64 tokens instead of 1, so the GPU’s total output is 42 times higher.

Step time7.36 ms
Tokens/s for each request136
Tokens/s for the whole GPU8,700
GPU cost per 1M output tokens$0.096assuming $3 per GPU-hour
weights, 1 square = 1 GBKV cachesrequests in the batch, one square eachnew token

Throughput vs batch size · both axes log scale

weights set the paceKV reads1001K10K1248163264128256512requests in the batchwhole GPUeach request
tokens/s, whole GPUtokens/s, each requestKV reads take longer than weight readsKV memory full, extra requests wait

Simplified model: a step lasts as long as the slower of reading memory and doing the math. It assumes Llama 3.1 8B in BF16 on one H100 SXM: 16.06 GB of weights, 131,072 bytes of KV cache per token, 3.35 TB/s of memory bandwidth, 60% of 989 TFLOP/s, about 55 GB for KV caches (as in chapter 3) and $3 per GPU-hour. In this model memory sets the pace at every batch size; real kernels can’t overlap reads and math perfectly, so math starts to matter at around 200 requests.

In this model, batching takes the GPU cost of a million output tokens from about $4 to under 5 cents, which is why an engine works so hard to keep the batch full. SGLang is built for the right half of that chart. In practice KV memory fills first, as the chart shows, which is why chapter 3 is all about that memory.

SGLang, vLLM, TensorRT-LLM and others are different answers to the same three problems. SGLang came out of LMSYS (the group behind Chatbot Arena) in 2024. Its best-known idea is RadixAttention: noticing that many requests start with the same text, and computing that text once. Today it serves production traffic at many AI labs and clouds, and it is a common engine for generating rollouts for reinforcement learning (RL).

Analogy

Think of a restaurant kitchen. The model is the stove. A good expediter keeps every burner busy, starts the next order the moment a burner frees, and never preps the same sauce twice. SGLang is mostly an expediter.

What "SGLang" stands for

Structured Generation Language. The original 2024 project had two halves: a Python mini-language for writing multi-step LLM programs (gen, select, fork), and a fast runtime underneath it. The runtime won. The language lives on in python/sglang/lang/, but the package README marks it as deprecated and no longer maintained. When people say SGLang today, they mean the runtime, called SRT (SGLang Runtime) in the code: python/sglang/srt/.

check yourself

Why is serving 64 users in one batch much cheaper per token than serving them one by one?

Next: Prefill and decode →

Prefill and decode: the two halves of every request

In one line

The prompt is read in one big parallel pass (prefill), then the answer is written one token per pass (decode). Prefill sets your time to first token. Decode sets your tokens per second.

A transformer predicts one next token from everything before it. To answer a 1,000-token prompt with a 200-token reply, the engine does this:

  1. Prefill: push all 1,000 prompt tokens through the model at once. The GPU is good at this, since it is one huge matrix multiplication. The output is the first reply token.
  2. Decode: feed that one new token back in, get the next one, and repeat 199 more times. Each pass handles a single token per request, so the GPU spends most of its time waiting for weights to arrive from memory rather than doing math.

That difference explains two things you have seen on API pricing pages. Input tokens are cheaper than output tokens, because prefill processes them in parallel and decode processes output one at a time. And a streamed answer starts after a pause and then flows at a steady rhythm, because the pause, time to first token (TTFT), is mostly prefill and the rhythm after it is decode.

Watch one request generate
Interactive
Forward passes so far4
The·dinner·cost·$90·for·3·people,·so·each·person·owes·$30.·Add
16 tokens of K/V saved

Decode step 3. Only the newest token goes through the model. It looks back at the 16 saved keys and values instead of recomputing them.

being computed this passalready computed, K/V savednew token

In every layer, the newest token compares itself with every token so far, itself included, and pulls in information from the ones that match best. That step is called attention, and it is the only place where tokens see each other.

Attention needs two vectors from every earlier token, a key and a value. Step through the same dinner bill below, and switch the cache off to see what saving them buys.

What attention reads, and why the KV cache can save it
Interactive
Forward pass5 of 15
Q

Query. What the new token is looking for.

K

Key. What each earlier token offers to be matched on.

V

Value. What a token passes along when it matches.

Pass 5, decode. Only the new token “Add” computes a query, key and value. Its query is matched against its own new key and the 16 saved ones (strongest: “90” 16%, “3” 13%, “30” 10%, “each” 10%), and the blend of their values predicts “a”.

K/V computed this pass1
Total so far, with cache17
Total so far, without cache75
0100200300151015forward pass (1 = prefill)without cachewith cache
Total K/V computations after each pass. Without the cache they grow with the square of the length. With it, they grow by one per token. Reading the saved keys and values still grows with the length, which slows decode on long prompts; drag the next widget’s prompt slider far to the right to see it.

Simplified model: one attention head and made-up weights. Llama 3.1 8B does this in all 32 layers, with many heads at once, and keeps a key and value per token for every layer.

new token, computing its queryK/V computed this passK/V saved in the cachearc thickness = attention weight (the loop is the new token’s own key)

This is the whole reason a KV cache works. A token's key and value depend only on that token and the ones before it, so adding new tokens never changes them. The prompt's keys and values are computed once, during prefill, and every later decode step only adds one more pair per layer.

Here is the same split as you feel it in a chat window, on a GPU you share with other users.

What you feel: time to first token and tokens per second
Simulation
You

Here is the receipt and our chat so far. How much does each of us owe with a 20% tip? 2,000-token prompt

Model
The dinner cost $90 for 3 people, so each person owes $30. Add a 20% tip and it is $36 each, or $108 for the table. If someone only had a salad, splitting by what each person ordered is fairer. Add up each order, add 20% to it, and that is what each person pays. The three amounts still add up to $108. To settle up, one person can pay the whole
Decoding: token 90 of 200602 ms of 1.20 s · real time
first token at 114 ms
0 s1.20 s

Your first token arrives after 114 ms, which is 111 ms of prefill for 2,000 prompt tokens plus 2.7 ms waiting for the current step to finish. After that, decode adds one token every 5.5 ms (about 183 per second), the streaming speed you see. Sharing each step with 7 others costs you little (5.5 ms per token against 4.9 ms alone), because each step reads the 16 GB of weights once for everyone.

Time to first token114 ms
Time per output token5.5 ms
Tokens per second, for you183
Total time for the answer1.20 s
Tokens per second, whole GPU1,468
Simplified model: Llama 3.1 8B on one H100. How the numbers are made
  • Other users. Each of them sends a 2,000-token prompt and gets a 200-token answer. Only your own request follows the prompt and answer sliders.
  • Queue. The KV cache holds about 422,000 tokens (the chapter 3 calculator at its default setting). Simplified: every request reserves room for its whole prompt and answer, so about 191 of the others fit at once and the rest wait in rounds. The real scheduler bets that answers stop early (chapter 5), so it fits a few more. You arrive last and join the last round if it has room. When everyone fits, you only wait for the current step to end.
  • Prefill. prompt ÷ 18,000 tokens per second. On an H100, SGLang reads a long prompt in chunks of 8,192 tokens, drawn as lines in the bar. Real prefill slows down on very long prompts, because attention grows with the square of the length.
  • Decode. Each step reads the 16 GB of weights once, plus the saved keys and values of every running conversation (131 KB per token), at 3.35 TB/s. On average a conversation holds its prompt plus half its answer, so 2,100 tokens for each of the others. It can never beat the math, 16 GFLOP per token at 60% of 989 TFLOP/s.

time per token = max( (16.06 GB + (others in your round × 2,100 + your prompt + your answer ÷ 2) × 131 KB) ÷ 3.35 TB/s , everyone in your round × 16 GFLOP ÷ (60% × 989 TFLOP/s) )

Left out: other users’ prompts also need prefill, and by default that pauses everyone’s stream (chapter 5). A real busy server is slower than this.

waiting to startprefill (reading the prompt)one decoded tokennow

Time to first token also includes queueing, so it grows with how busy the server is as well as with your prompt. SGLang's benchmark reports both numbers, computing time per output token as (total time - time to first token) ÷ (output tokens - 1), and a server started with --enable-metrics exports them as Prometheus histograms.

The engine's name for these

In SGLang's code, a prefill batch is called EXTEND, because it extends a sequence by many tokens at once (and sometimes the start of the sequence is already cached, so it only extends the rest). A decode batch is DECODE. These are values of ForwardMode, and you will see them everywhere in the scheduler.

check yourself
You paste a 50-page document into a chat. Which number gets noticeably worse?

The KV cache is the budget

In one line

For every token in every live conversation, the GPU keeps a small block of numbers called keys and values. Those blocks fill GPU memory long before the math runs out, so how many users fit depends on that memory.

Chapter 2 showed that attention needs a key and a value vector for every earlier token in every layer, and that the engine computes them once and saves them. That saved set is the KV cache.

The cost per token is fixed by the model's shape:

bytes per token = 2 (K and V) × layers × KV heads × head size × bytes per number

For Llama 3.1 8B that is 2 × 32 × 8 × 128 × 2 bytes = 131 KB per token. A single 100,000-token conversation needs about 13 GB of GPU memory just for its cache, on top of 16 GB of weights. That is why long context windows are expensive, and why providers limit them.

SGLang decides the split at startup. It loads the weights, measures what is left, and turns a fraction of the GPU into one big pool of KV slots. The flag --mem-fraction-static sets how much of the GPU goes to weights plus that pool. The rest is scratch space for intermediate results. If you leave it unset, SGLang picks a value from your GPU's size (about 0.83 on an 80 GB H100).

How many tokens fit on your GPU?
Calculator

The memory slider jumps to roughly what SGLang would choose whenever you change the GPU or GPU count.

model weightsKV cache poolscratch (activations, CUDA graphs)
2 (K and V) × 32 layers × 8 KV heads × 128 dims × 2 bytes = 131 KB per token
KV cache pool per GPU55.4 GB
Tokens the pool holds422K
Requests at 8K tokens each51

Things to try: switch Llama 3.1 70B onto one H100 and watch it fail to fit, then give it 4 GPUs, which splits every layer across them (tensor parallelism, chapter 9). Change the KV cache to FP8 and the token count doubles. Pick DeepSeek-V3, which uses a compressed cache design called MLA (multi-head latent attention): its per-token cost is about half of Llama 8B's despite being 80 times bigger.

The calculator uses the formula from SGLang's pool sizing code and the published shapes of each model. The real server measures free memory after loading, so expect its number to land within a few percent of this one.

Two tables, not one big array

Here is a detail that matters later. SGLang does not give each conversation its own contiguous chunk of memory. It keeps two lookup tables:

  • ReqToTokenPool: one row per live request. Row r, column i says which KV slot holds token i of request r.
  • TokenToKVPool (the KVCache classes): the actual memory. For each layer, a big tensor of shape [slots, kv_heads, head_dim] for keys and another for values.

A free-slot allocator hands out slot numbers like a coat check. Because a request's tokens can live in any slots, two requests can point at the same slots for a shared beginning. That one fact is what makes RadixAttention possible. vLLM's PagedAttention is the same idea with fixed-size blocks of 16 tokens. On most GPUs SGLang's default "page size" is 1 token, which lets it share prefixes down to the exact token. Some attention kernels need pages of 16-128 tokens, and on Blackwell GPUs such as the B200 the default kernel is one of them, so the default page size there is 64.

Below, one stream of requests runs through two ways of handing out a pool of 48 KV slots. In the first (the way serving systems worked before PagedAttention), a request books one unbroken block for its prompt plus its whole max_tokens limit the moment it arrives. The second is SGLang's way. A request takes one slot per token from the front of the free list, the queue of unused slot numbers.

Two ways to hand out KV slots
Simulation

Step 7 · next to arrive: R9, a 4-token prompt with max_tokens 4, so it would book 8 slots up front

Reserve up front

Books prompt + max_tokens slots in one block on arrival.

Free runs: 13-22 (10), 44-47 (4). Longest: 10 of 14 free. R9 will need 8 in a row.

R2R3R8
Running3
Real tokens17
Wasted slots16
Turned away4

Slot allocator (SGLang)

Takes one slot per token from the front of the free list.

Free list, front first (14): 345814313925262728374432 The next step takes the first 6, one per running request.

R2R3R4R5R7R8
Running6
Real tokens33
Wasted slots0
Turned away0

Mid-run, after 7 steps. Reserve up front runs 3 requests and has turned away 4. Its 14 free slots sit in runs of 10 and 4, so a request that books more than 10 cannot get in. The slot allocator runs 6 requests in 33 slots. The line traces R2. It stays solid through one block in the first grid and turns dashed wherever it jumps between scattered slots in the second. Row 6 of the table lists those slots in order.

ReqToTokenPool, SGLang side: row = req_pool_idx, column = token position, cell = KV slot number

Hover or tap a row, or a slot, to trace that request through both grids. Row 0 is padding for CUDA graphs (chapter 7). New requests take free rows from the end of the free-row list.

ReqToTokenPool: for each row, the KV slot that holds each token position of that row’s request
row01234567891011
0
293038451
2 free
222324364315
202135429
17181934417
101112131633406
7 free
46472
slot holding a token (one color per request)booked but still empty: wastedfree slotfree slot the next step takesslot 0 and row 0, kept for paddingtoken order, on to the next slottoken order, a jump to a slot elsewhere

Simplified model: 48 slots and one token per step; requests that do not fit are dropped instead of queued, and finished requests free their slots instead of keeping them in the radix cache (chapter 6).

Because SGLang takes each slot right before it writes a token there, a request never holds an empty slot. With --page-size 16 it hands out 16-slot pages instead, so a request can leave up to 15 slots of its last page empty, which is still far less than booking max_tokens ahead. Real SGLang also estimates, before admitting a request, whether its answer and the answers already running will fit, without booking slots for any of them (chapter 5).

check yourself
You launch with --kv-cache-dtype fp8_e4m3. What happens?
Next: Life of a request →

Life of a request

In one line

SGLang is three cooperating programs. One talks HTTP and tokenizes, one schedules and drives the GPU, and one turns token ids back into text. They pass messages over fast local sockets (ZeroMQ).

When you run sglang serve, you get more than one process. The split exists because Python can run only one thread of Python code at a time in a process. If tokenizing a big prompt or formatting JSON ran in the same process as the scheduler, the GPU would sit idle while that work happened. So the work that must never stall (keeping the GPU fed) gets a process of its own.

Here is one chat request passing through those three processes, with the token ids a Qwen model really sees. Step through it hop by hop and then token by token, or play the whole request to watch the answer come back one token per forward pass. The message panel shows what crosses each hop, using SGLang's own class and field names.

Follow one chat request through the server
Step through
1 / 8
Main processDetokenizer processScheduler process · one per GPUHTTPZMQZMQZMQYour appOpenAI SDKHTTP serverFastAPI routesTokenizerManagerids out, text backDetokenizerManagerids → textSchedulerqueue, batches, cacheModelRunnerruns the modelGPUKV pool

1 · You send a request

Your code calls POST /v1/chat/completions, exactly as it would call OpenAI. The body is plain JSON, and "stream": true asks for the answer as server-sent events, one small chunk per token. SGLang also speaks the Anthropic Messages API, Ollama’s chat API and its own native /generate.

On the wireJSON bodyHTTP POST · your app → HTTP server
POST /v1/chat/completions
{
  "model": "Qwen/Qwen2.5-7B-Instruct",
  "messages": [
    {"role": "system",
     "content": "You are terse."},
    {"role": "user", "content": "Hi"}
  ],
  "temperature": 0.7,
  "max_tokens": 64,
  "stream": true
}
Your app prints

Nothing yet. Text first reaches your app at step 8.

This request’s tokens Not tokens yet. The request is still JSON text.

<|im_start|>system\nYou are terse.<|im_end|>\n<|im_start|>user\nHi<|im_end|>\n<|im_start|>assistant\n

Simplified model: each message shows a handful of its real fields, and … marks the ones left out. The token ids are real Qwen ids. The request id and KV slot numbers are made up.

message in flightseparate OS processbeing computed this passalready computed, K/V savednew tokenGPU running a pass

In the default single-machine setup, each ZeroMQ hop is a PUSH socket feeding a PULL socket over an ipc:// address, which is a Unix socket file rather than a network port. The messages are the Python objects above, serialized on one side and rebuilt on the other.

With more than one GPU

With --tp-size 4, which splits every layer of the model across four GPUs (tensor parallelism, chapter 9), SGLang starts four scheduler processes, one per GPU. Only the first of them, called rank 0, reads from the socket. It broadcasts each batch of new requests to the other three, and all four then make identical scheduling decisions, so their batches line up exactly when the model's layers talk to each other across GPUs. Only rank 0 sends results back.

check yourself

Why does SGLang put the scheduler in its own process instead of a thread inside the web server?

Next: The scheduler →

The scheduler: continuous batching

In one line

Once per forward pass, the scheduler decides who rides. New requests get admitted the moment there is room, finished ones leave immediately, and the GPU never waits for the slowest request in a group.

This loop is the core of the engine. Trimmed down, here is what event_loop_normal in scheduler.py does, forever:

# python/sglang/srt/managers/scheduler.py (trimmed)
while True:
    self.ingest_requests()                    # pull new requests off the socket
    plan = self.get_next_batch_to_run(...)    # prefill new arrivals, or decode the running batch
    batch = plan.batch_to_run
    if batch:
        result = self.run_batch(batch)        # one forward pass on the GPU
        self.process_batch_result(batch, result)  # append tokens, check stop, stream out
    else:
        self.on_idle()
    self.last_batch = batch

The interesting part is get_next_batch_to_run. Each turn it:

  1. Merges the requests that were just prefilled into the running batch, the set of requests currently decoding.
  2. Drops finished requests from the running batch.
  3. Tries to build a prefill batch from the waiting queue. If it can, that runs this turn. Prefill gets priority, so new users see their first token quickly.
  4. Otherwise, it runs one decode step for everyone in the running batch.

That is continuous batching (sometimes called in-flight batching). Compare it with the obvious approach below.

Static vs continuous batching
Simulation

Press Play to watch both schedulers run pass by pass. Each column is one forward pass.

Ten requests, with how many tokens each will generate
R1 2 tokR2 13 tokR3 5 tokR4 4 tokR5 11 tokR6 8 tokR7 6 tokR8 11 tokR9 11 tokR10 9 tok

Static batching. Fill the seats, run until the longest finishes, then refill.

PDDDDDDDDDDDDPDDDDDDDDDDPDDDDDDDDDDPPPPPPPPPP
Forward passes35
Seats doing useful decode work55%
Average finish time, in passes18.0

Continuous batching. SGLang’s loop: a free seat is refilled with a prefill pass right away.

PDPDDPDPDDDDDPDPDPDDDDDDDDDPPPPPPPPPP
Forward passes27
Seats doing useful decode work88%
Average finish time, in passes15.7
P = prefill pass for this requestdecode steppaused while others prefillempty seat

Simplified model: every forward pass is one column of the same width. Real prefill passes take longer than decode passes, so the pauses in the continuous grid cost more time than they show.

Look at the faded cells in the continuous grid. When a prefill pass runs, every request that is already decoding pauses for that pass. With a short prompt that costs little. With a 30,000-token prompt it stops everyone's stream for a noticeable moment.

Chunked prefill

SGLang cuts a long prompt into chunks of --chunked-prefill-size tokens and prefills one chunk per pass. That caps how much scratch memory any single pass needs. The default chunk size grows with GPU memory (8,192 tokens on an H100), because bigger GPUs are usually faster and need bigger passes to stay busy. The chunk-size menu below lists the default for each class of GPU. A half-done prompt carries over between passes as self.chunked_req and is admitted first on the next pass.

Chunking alone does not restart the other streams, because prefill keeps priority and the chunks run back to back. Two flags change that. --enable-mixed-chunk puts the decoding requests' next tokens into each chunk's pass (a MIXED batch), and --prefill-decode-interval N runs N decode passes after each chunk. The full fix is to prefill on other GPUs (chapter 9).

Below, four users are partway through their answers when a fifth request arrives with a 24,000-token prompt (say, a long contract to summarize). Pick how the engine handles that prompt, then watch the four streams and the new request's wait for its first token.

A long prompt lands on a busy GPU
Simulation

Server flags none needed, 8,192 is the default chunk size on 60–160 GB GPUs

This GPUpaused 1,233 msUser AUser BUser CUser DNew requestfirst token after 1,225 ms05001,0001,5002,000 ms8,1928,1927,6168,1928,1927,616640 ms
User A28 up to Graça and watch the sunset from the viewpoint. On Sunday go to Belémpaused 489 ms
User B<= b[j]: out.append(a[i]) i += 1 else: out.append(b[j]) j += 1 out.extend(a[i:]) out.extend(b[j:]) return out defpaused 489 ms
User Csecond set of reactions uses them to build glucose from carbon dioxide taken from the air.paused 489 ms
User Dwith thirty-one percent more active accounts than in June, and the share of new users whopaused 489 ms
Newpass 2 of 3: tokens 8,193-16,384waiting 490 ms

Chunked prefill, SGLang’s default. The prompt goes through in 3 passes of at most 8,192 tokens. Prefill keeps priority, so the chunks run back to back and the streams still stop for 1,233 ms. Chunking caps the size, and the scratch memory, of every pass.

Longest pause in users A–D’s streams1,233 ms
New request’s time to first token1,225 ms
Passes to read the prompt3
Largest pass on this GPU, tokens8,192
decode pass: one token for each userprefill pass reading the promptchunk pass carrying decode tokensstream pausednew request waiting for its first token

Simplified model, illustrative numbers: a pass takes 8 ms, plus 1 second per 20,000 prompt tokens, plus 0.1 ms per decode token, and copying the KV cache between machines takes 40 ms. Real pass times depend on the model, the GPU and how long the context already is. Playback runs 4× slower than the clock.

Admission: betting on how long answers will be

The scheduler cannot know how many tokens a request will generate. If it admits too many, they run out of KV memory mid-answer. If it admits too few, the GPU idles. The PrefillAdder class handles this: for each running request it reserves room for its remaining max_tokens times a new-token ratio. The ratio starts at 0.7 (scaled by --schedule-conservativeness) and slowly decays toward about 0.1 while things go well, since most answers stop long before their limit.

When the bet loses and the pool fills up, the scheduler retracts: it pauses the requests with the fewest generated tokens, frees their memory, and puts them back in the queue to resume later. You will see KV cache pool is full. Retract requests. in the log. A few retractions are normal. Frequent ones mean you should raise --schedule-conservativeness or lower --max-running-requests.

Who goes first

--schedule-policy picks the order of the waiting queue. The default is fcfs (first come, first served). The cache-aware option lpm (longest prefix match) sorts waiting requests by how much of their prompt is already cached, which raises cache hits when many requests share prefixes. Sorting costs CPU time, so LPM quietly falls back to FCFS when more than 128 requests are waiting. Other options include dfs-weight, lof (longest output first), hrrn and random.

The control room below runs all of these rules together on a deliberately small KV pool, so the pressure shows. Each tick is one trip around the loop: new arrivals join the queue, then the scheduler runs a prefill if it can admit someone, or one decode step for everyone running. Each card shows a request's max_new, the max_tokens it was sent with (SGLang stores it as max_new_tokens).

Yellow in the pool is cache, the KV that finished requests and shared system prompts left behind. A later prompt that starts the same way reuses it, and the scheduler evicts it when it needs room (chapter 6).

Scheduler control room
Simulation

More traffic than the server can serve, and answers that run close to their max_new. Server flags: --max-total-tokens 6000 --schedule-conservativeness 1.0 --schedule-policy fcfs --chunked-prefill-size 2048. Changing a flag restarts the server and replays the same arrivals.

Pass 120 retract, then decode35 prefill and 81 decode passes so far

Pass 120: the pool ran out. The decode step needed 44 free slots and only 3 were left, even after evicting cache. SGLang retracted R45 (fewest tokens written) and sent it to the back of the queue. Retracted requests keep their output and recompute their KV when readmitted. The ratio was re-estimated from the requests still running: 0.52.

Waiting queue 15

fcfs: arrival order. Retracted requests rejoin at the back.

  1. R46292 tokmax_new 160system prompt B · 250 cached
    next in line · needs 202, no room
  2. R4791 tokmax_new 128no shared prefix
  3. R4836 tokmax_new 128no shared prefix
  4. R4976 tokmax_new 96no shared prefix
  5. R5083 tokmax_new 96no shared prefix
  6. R51434 tokmax_new 96system prompt A · 400 cached
  7. +8 more waiting, then the back of the queue:
  8. R45494 tokmax_new 128retracted, keeps 24 tokens · system prompt A · 400 cached

Running batch 43

R182/128
R282/160
R480/160
R580/160
R679/160
R777/160
R877/96
R973/128
R1066/96
R1165/96
R1265/160
R1365/160
R1462/160
R1562/96
R1660/128
R1760/96
R1860/160
R1958/128
R2048/160
R2148/96
R2246/128
R2340/96
R2440/128
R2540/160
R2640/128
R2739/160
R2838/128
R2938/128
R3036/160
R3136/96
R3235/96
R3335/128
R3435/128
R3531/160
R3631/128
R3731/128
R3830/128
R3929/128
R4029/96
R4128/96
+4 more

KV pool and the admission check

Pool

in use 5,947cached 0free 53of 6,000 tokens

Admission

in usereserved for running requestsnext request’s need, green if it fits, red if it waitsroom (free plus cached)past the end of the pool

room = free 53 + cached 0 − reserved 1,839 = -1,786 tokens
reserved = ratio 0.52 × 3,526 tokens the running requests may still write (each counted up to 4,096)
next: R46 needs 42 uncached + 160 max_new = 202 → waits

New-token ratio 0.52

Starts at 0.70 (0.7 × conservativeness), drops a little after every decode pass toward the floor 0.10, is re-estimated after a retraction (red line), and goes back to the start when the server is idle. Prefill passes (small tick at the bottom) leave it alone.

Pass 120 retract, then decode

Scheduler log, in SGLang’s own format
[pass 14] Prefill batch, #new-seq: 1, #new-token: 38, #cached-token: 400, token usage: 0.18, #running-req: 6, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 1370.85
[pass 15] Prefill batch, #new-seq: 1, #new-token: 36, #cached-token: 400, token usage: 0.18, #running-req: 7, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 4411.76
[pass 20] Prefill batch, #new-seq: 1, #new-token: 54, #cached-token: 250, token usage: 0.20, #running-req: 8, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 1100.24
[pass 21] Decode batch, #running-req: 9, #token: 1184, token usage: 0.20, cuda graph: True, gen throughput (token/s): 299.86, #queue-req: 0
[pass 28] Prefill batch, #new-seq: 1, #new-token: 65, #cached-token: 0, token usage: 0.22, #running-req: 9, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 807.85
[pass 30] Prefill batch, #new-seq: 2, #new-token: 138, #cached-token: 400, token usage: 0.24, #running-req: 10, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 5637.25
[pass 31] Prefill batch, #new-seq: 1, #new-token: 79, #cached-token: 250, token usage: 0.26, #running-req: 12, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 7355.68
[pass 34] Decode batch, #running-req: 13, #token: 1569, token usage: 0.26, cuda graph: True, gen throughput (token/s): 750.29, #queue-req: 0
[pass 35] Prefill batch, #new-seq: 1, #new-token: 97, #cached-token: 400, token usage: 0.28, #running-req: 13, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 2229.89
[pass 36] Prefill batch, #new-seq: 1, #new-token: 116, #cached-token: 250, token usage: 0.30, #running-req: 14, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 8950.62
[pass 39] Prefill batch, #new-seq: 1, #new-token: 104, #cached-token: 250, token usage: 0.32, #running-req: 15, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 3073.29
[pass 40] Prefill batch, #new-seq: 2, #new-token: 129, #cached-token: 400, token usage: 0.34, #running-req: 16, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 9388.65
[pass 43] Prefill batch, #new-seq: 1, #new-token: 106, #cached-token: 0, token usage: 0.36, #running-req: 18, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 3056.52
[pass 49] Decode batch, #running-req: 19, #token: 2301, token usage: 0.38, cuda graph: True, gen throughput (token/s): 1030.22, #queue-req: 0
[pass 54] Prefill batch, #new-seq: 1, #new-token: 101, #cached-token: 250, token usage: 0.41, #running-req: 19, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 808.91
[pass 55] Prefill batch, #new-seq: 1, #new-token: 82, #cached-token: 400, token usage: 0.43, #running-req: 20, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 7509.16
[pass 58] Prefill batch, #new-seq: 1, #new-token: 80, #cached-token: 400, token usage: 0.45, #running-req: 21, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 2364.07
[pass 62] Decode batch, #running-req: 22, #token: 2770, token usage: 0.46, cuda graph: True, gen throughput (token/s): 1387.21, #queue-req: 0
[pass 65] Prefill batch, #new-seq: 3, #new-token: 289, #cached-token: 800, token usage: 0.52, #running-req: 22, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 3101.52
[pass 66] Prefill batch, #new-seq: 1, #new-token: 85, #cached-token: 0, token usage: 0.53, #running-req: 25, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 7657.66
[pass 68] Prefill batch, #new-seq: 1, #new-token: 56, #cached-token: 400, token usage: 0.55, #running-req: 26, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 2607.08
[pass 70] Prefill batch, #new-seq: 1, #new-token: 62, #cached-token: 400, token usage: 0.56, #running-req: 27, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 2823.32
[pass 71] Prefill batch, #new-seq: 1, #new-token: 119, #cached-token: 0, token usage: 0.58, #running-req: 28, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 9056.32
[pass 74] Prefill batch, #new-seq: 2, #new-token: 93, #cached-token: 250, token usage: 0.60, #running-req: 29, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 2545.16
[pass 76] Prefill batch, #new-seq: 2, #new-token: 210, #cached-token: 400, token usage: 0.65, #running-req: 31, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 6704.98
[pass 77] Prefill batch, #new-seq: 1, #new-token: 73, #cached-token: 0, token usage: 0.66, #running-req: 33, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 7032.76
[pass 80] Decode batch, #running-req: 34, #token: 4045, token usage: 0.67, cuda graph: True, gen throughput (token/s): 1242.56, #queue-req: 0
[pass 82] Prefill batch, #new-seq: 1, #new-token: 40, #cached-token: 400, token usage: 0.69, #running-req: 34, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 658.76
[pass 83] Prefill batch, #new-seq: 2, #new-token: 71, #cached-token: 400, token usage: 0.70, #running-req: 35, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 6920.08
[pass 85] Prefill batch, #new-seq: 1, #new-token: 80, #cached-token: 250, token usage: 0.72, #running-req: 37, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 3300.33
[pass 87] Prefill batch, #new-seq: 1, #new-token: 95, #cached-token: 250, token usage: 0.74, #running-req: 38, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 3760.89
[pass 88] Prefill batch, #new-seq: 1, #new-token: 113, #cached-token: 0, token usage: 0.76, #running-req: 39, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 8841.94
[pass 90] Prefill batch, #new-seq: 2, #new-token: 178, #cached-token: 800, token usage: 0.80, #running-req: 40, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 5839.90
[pass 92] Prefill batch, #new-seq: 1, #new-token: 77, #cached-token: 400, token usage: 0.81, #running-req: 42, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 3122.47
[pass 93] Prefill batch, #new-seq: 1, #new-token: 76, #cached-token: 400, token usage: 0.83, #running-req: 43, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 7196.97
[pass 96] Prefill batch, #new-seq: 1, #new-token: 70, #cached-token: 400, token usage: 0.85, #running-req: 44, #queue-req: 1, #pending-token: 292, cuda graph: True, input throughput (token/s): 1805.99
[pass 99] Decode batch, #running-req: 45, #token: 5259, token usage: 0.88, cuda graph: True, gen throughput (token/s): 1713.01, #queue-req: 2
[pass 109] Decode batch, #running-req: 44, #token: 5557, token usage: 0.93, cuda graph: True, gen throughput (token/s): 3120.66, #queue-req: 9
[pass 119] Decode batch, #running-req: 44, #token: 5997, token usage: 1.00, cuda graph: True, gen throughput (token/s): 3081.23, #queue-req: 14
[pass 120] KV cache pool is full. Retract requests. #retracted_reqs: 1, #new_tokens_gained: 93, #new_token_ratio: 0.0980 -> 0.5216
Average wait for first token, in passes4.5includes requests still waiting
Longest wait for first token, in passes25
Output tokens per pass19.2
Requests retracted1
Cache hit rate71%
Finished1
KV in use by requestscached, evictablefreetile: tokens written, then the reserved betfinished this passretracted this pass

Simplified model: every pass is one tick here, though real passes take different amounts of time. The ratio decays over 60 decode passes here instead of SGLang’s 600, decode lines print every 10 decode passes instead of 40, and the throughput figures assume an illustrative time per pass.

Things to try: play Rush hour to about pass 300, then raise conservativeness to 1.6. The server replays the same arrivals and retracts fewer requests. Drop it to 0.3 and it retracts more. In Shared system prompt, play to about pass 400, then switch the policy from lpm to fcfs. lpm serves requests whose prefix is already cached first, so it can leave others waiting. Under lpm the cache hit rate is a point or two higher and the finished counts stay close, but the longest wait for a first token is much longer.

The overlap scheduler

Scheduling takes CPU time: matching prefixes, allocating memory, building tensors. In a naive loop the GPU waits while the CPU does that between passes. SGLang's default loop, event_loop_overlap, launches pass N on the GPU and then, while it runs, processes pass N-1's results and prepares pass N+1. The catch is that pass N+1 needs the tokens pass N has not produced yet. The second half of the simulation below shows how a GPU buffer called FutureMap gets around that. Turn overlap off with --disable-overlap-schedule when debugging.

The CPU and the GPU, pass by pass
Simulation

Server flags --disable-overlap-schedule

CPUGPU1wait2wait3wait4wait5wait6wait010203040mspass 1pass 2pass 3pass 4pass 5pass 6pass 1pass 2pass 3pass 4pass 5pass 628.6 ms

28.6 ms · CPU: waiting for pass 4 · GPU: running pass 4

Overlap off. Each pass, the CPU plans the batch, the GPU runs it while the CPU waits, and then the CPU processes the results while the GPU waits. The GPU idles 2.5 ms of every 8.5 ms, so it is busy 71% of the time.

GPU busy71%on: 100%
Passes per second118on: 167
Time per pass8.5 mson: 6 ms
CPU plans pass NCPU processes pass N’s resultsGPU runs pass Nidle or waitingone color per pass

How pass 5 can start before pass 4’s tokens exist FutureMap

Step 1 of 4
GPU · pass 4running
Buffer in GPU memory, one slot per request row
row 3
not written yet
row 7
not written yet
row 9
not written yet
GPU · pass 5being planned by the CPU

CPU planning pass 5 for rows 3, 7 and 9

Pass 4 is running on the GPU. Each running request owns a numbered row in a table on the GPU, and these three requests are rows 3, 7 and 9. The CPU is already planning pass 5 for them. Pass 5’s input is each request’s next token, and pass 4 is still computing those.

In SGLang, pass N writes its tokens into this buffer in _relay_forward_payload, and pass N+1 reads them back in resolve_forward_inputs.

Simplified model, illustrative numbers: planning a pass takes the CPU 1.5 ms and processing its results takes 1 ms, whatever the GPU time. Real costs depend on batch size, model and hardware. The timeline shows the first passes after the loop starts.

Planning one pass ahead has a side effect. A request that emits its stop token in pass N is already in pass N+1, because the CPU planned that pass before it saw the stop. The GPU computes one extra token for it, and SGLang skips that token while processing results, so it is never sent. A request that reaches its max_tokens limit is predictable, so the overlap loop leaves it out of the next pass instead.

check yourself
Your log shows KV cache pool is full. Retract requests. What just happened?
Next: RadixAttention →

RadixAttention: never compute the same prompt twice

In one line

SGLang keeps the KV cache of finished requests in a tree keyed by their tokens. A new request walks the tree, reuses whatever beginning it shares with earlier requests, and only computes the rest.

Real traffic repeats itself constantly. Every request to a chatbot starts with the same system prompt. Every turn of a conversation resends all previous turns. An agent re-reads its tool list and history on every step. Few-shot prompts share their examples across thousands of calls. Without a cache, the engine prefills all of that again each time.

The "cached input tokens" discount on API pricing pages is this same idea, run by the provider. With SGLang, you run it yourself, and it is on by default.

The data structure is a radix tree (a compressed trie). Each edge holds a run of tokens. Each node remembers which KV slots hold those tokens. Requests that share a beginning share a path from the root. The tree stores slot numbers, not the keys and values themselves, so sharing costs nothing: two requests simply point at the same slots.

Radix cache playground
Interactive · words stand in for tokens

Every request starts with the same instructions. Only the question differs.

Request queue
  1. SYSTEM: You are a friendly assistant for splitting bills . Answer in one sentence . USER: How do I split $ 90 three ways ?
  2. SYSTEM: You are a friendly assistant for splitting bills . Answer in one sentence . USER: Should tip be split evenly ?
  3. SYSTEM: You are a friendly assistant for splitting bills . Answer in one sentence . USER: How do I split $ 90 with tax ?
  4. SYSTEM: You are a friendly assistant for splitting bills . Answer in one sentence . USER: Who pays for shared appetizers ?
The tree in GPU memory
How do I split $ 90 three ways ? (9 tokens)How do I split $ 90 t…9 tokensShould tip be split evenly ? (6 tokens)Should tip be split e…6 tokensSYSTEM : You are a friendly assistant for splitting bills . Answer in one sentence . USER : (18 tokens)SYSTEM : You are a fr…18 tokensroot
Last request: reused vs computed
SYSTEM : You are a friendly assistant for splitting bills . Answer in one sentence . USER : Should tip be split evenly ?
Prompt tokens sent51
Served from cache18
Cache hit rate35%
Tokens held in tree33 / 120
  • 18 of 24 tokens reused
  • 0 of 27 tokens reused
matched by the last request (reused)added by the last request (computed)evicted (least recently used, unlocked)locked by the running request (lock_ref above 0), so it cannot be evicted

Try the multi-turn conversation preset: each turn reuses almost everything. Then try unrelated prompts: nothing helps. Shrink the memory slider and watch the least recently used branches get evicted.

When it helps, and when it does not

WorkloadTypical reuseWhy
Multi-turn chatVery highEach turn resends the whole conversation
Agents and tool loopsVery highTool list and history repeat every step
Shared system promptHighLong fixed instructions, short questions
Few-shot or batch classificationHighExamples repeat across thousands of calls
Sampling n answers to one promptHighSame prompt, different continuations
One-off summarization of unique documentsLowLittle shared text beyond the instructions

Two practical tips follow. Put the fixed parts of your prompts first and the variable parts last, because only the beginning can match. And avoid putting a timestamp or request id at the top of the system prompt, which makes every prompt unique from the first token. Launch with --enable-cache-report and every OpenAI-style response reports usage.prompt_tokens_details.cached_tokens, so you can measure your hit rate.

What the real code does, step by step

  1. Match. When a request is considered for admission, match_prefix walks the tree from the root and returns the slots of the longest matching beginning. If the match ends halfway along an edge, the node is split in two so the shared half becomes its own node. Splitting copies no KV data; it only slices the lists of slot numbers.
  2. Lock. The matched path gets its lock_ref raised, so those slots cannot be evicted while this request depends on them.
  3. Allocate the rest. Only the unmatched tail gets new KV slots and gets prefilled. The log's #cached-token counts the reused part.
  4. Insert early. Right after prefill, the prompt is inserted into the tree, so other requests can share it while this one is still writing its answer.
  5. Insert again on finish. When the request finishes, the full sequence, answer included, goes into the tree. That is why the next turn of a chat hits: its prompt is the previous prompt plus the previous answer.
  6. Unlock and evict lazily. The lock is released. Nothing is deleted yet. Only when the allocator runs short does the cache evict, picking unlocked leaves by --radix-eviction-policy (default lru; also lfu, slru, priority, tlru). Removing a leaf may turn its parent into a leaf, so eviction eats branches from the tips inward and the shared trunk stays longest.

A note on the code you will find: mem_cache/radix_cache.py is the clearest version to read, and the playground above mirrors it. Production now runs UnifiedRadixCache, with the tree's core written in Rust by default, which applies the same ideas to newer model types (sliding-window and Mamba-style layers).

Beyond one GPU's memory: HiCache

GPU memory is small, so popular prefixes get evicted. --enable-hierarchical-cache adds two more tiers: evicted KV is kept in CPU memory (by default twice the size of the GPU pool), and optionally in a storage backend shared across servers (Mooncake, 3FS, NIXL, local files). A request that matches an entry in CPU memory copies it back layer by layer, overlapped with compute, which is much faster than recomputing a long prompt.

Across many servers: cache-aware routing

With several SGLang replicas, a prefix cached on server A is useless if the next turn lands on server B. SGLang's main router, the SGLang Model Gateway (written in Rust, in sgl-model-gateway/), keeps an approximate radix tree of recent request text for each server and sends each request to the one with the longest match. When load gets lopsided it switches to the shortest queue, so no server falls far behind. cache_aware is its default policy. The repo also holds a slimmer router in sgl-router/, whose default policy is power_of_two.

Below, one router spreads eight chats across three SGLang servers. Every turn resends the whole chat so far, so a turn is cheap only on a server that still holds the turns before it. All three policies route the same requests, so you can switch between them at any point and compare.

One router, three servers, eight chats
Simulation
Request 22 of 55: chat 7, turn 3 (992 prompt tokens). Load is balanced and the router’s tree matches 74% of the prompt on C, above the 0.3 threshold, so it goes there. C reused 943 tokens and computed 49.
Arriving next (33 left)
  1. 1·3
  2. 2·4
  3. 3·3
  4. 6·4
  5. 7·4
  6. 8·4
  7. 1·4
Router · cache_aware
7·3chat 7, turn 3
992 prompt tokens

Load balanced · best match 74% > 0.3 → C

Server A

0 running

tree match 0%

2,622 / 5,000 tokens

Cached, last used first
  1. chat 61,099
  2. chat 3747
  3. chat 5776

6 requests51% hit

Server B

0 running

tree match 0%

3,546 / 5,000 tokens

Cached, last used first
  1. chat 41,383
  2. chat 21,185
  3. chat 1978

8 requests69% hit

Server C

3 running

tree match 74%

3,569 / 5,000 tokens

Cached, last used first
  1. chat 71,076
  2. chat 51,239
  3. chat 81,254

8 requests67% hit

Requests routed22 / 55
Overall cache hit rate64%
Prompt tokens computed6,188
Cached tokens evicted0
The same requests under each policy: overall cache hit rate so far
  • Round robin20%
  • Random18%
  • Cache-aware64%
one color per chat; chips read chat · turnprefix reused from the cachecomputed freshrequest runningevicted, least recently used firstfree cachestill in the router’s tree, evicted on the server

Simplified model: each server holds 5,000 tokens of KV cache, keeps a chat as a chain of turns, and evicts from the least recently used chat, newest turn first. A request that computes more tokens runs longer. Thresholds used: cache threshold 0.3 and relative balance threshold 1.5, the gateway’s command-line defaults; absolute balance threshold 2 instead of the default 64, scaled down because only a few requests run at once here.

Things to try: switch to round robin. Each new turn usually lands on a server without the turns before it, so it recomputes the whole chat and pushes other chats out of that server's cache. The hit rate stays at about a third or less. Back on cache-aware, press Step once to see the exception. Server C has three requests running and the others have none, so the router skips its tree for this one request and picks the shortest queue. A and B tie at zero, and the gateway breaks a tie at random without looking at its tree, so chat 1 lands on A, which holds none of it, while B holds its earlier turns. Near the end of a cache-aware run, dashed rows show chats the router still expects on a server that has already evicted them.

check yourself

Your app's system prompt begins with "Current time: 14:02:37", then 2,000 tokens of fixed instructions. What is your cache hit rate likely to be?

Next: On the GPU →

On the GPU: kernels, CUDA graphs and sampling

In one line

The model code is ordinary PyTorch. Speed comes from three swaps underneath it: specialized attention kernels that read the paged KV cache, CUDA graphs that replay a whole decode step in one launch, and a fused sampler.

From batch to tensors

The scheduler's ScheduleBatch is Python bookkeeping. Right before the GPU runs, it becomes a ForwardBatch: a handful of tensors that describe the whole batch at once, such as the token ids to run, their positions, each request's row in the slot table, and the KV slots this pass writes to. Step 5 of the request diagram in chapter 4 shows one.

A model file, briefly

Each supported architecture has a file in python/sglang/srt/models/ (there are hundreds). They read like the Hugging Face versions, except that linear layers are tensor-parallel and attention goes through RadixAttention. Here is the attention block from llama.py:

# python/sglang/srt/models/llama.py · LlamaAttention.forward (trimmed)
qkv, _ = self.qkv_proj(hidden_states)               # one matmul makes Q, K and V
q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
q, k = self.rotary_emb(positions, q, k)             # encode positions (RoPE)
attn_output = self.attn(q, k, v, forward_batch)     # RadixAttention → attention backend
output, _ = self.o_proj(attn_output)

Despite its name, RadixAttention has no tree logic in it. It is a thin layer that hands q, k and v to whichever attention backend is active. The backend writes the new k and v into the KV pool at the slots chosen for this pass (out_cache_loc), then runs a kernel that gathers every earlier token's keys and values through the slot table. Prefix sharing is invisible at this level: the kernel reads slots and does not care who else points at them.

Attention backends

Attention over a paged cache is the one operation PyTorch cannot do well by itself, so SGLang plugs in kernels from several projects and picks one for your hardware. Override with --attention-backend, or separately with --prefill-attention-backend and --decode-attention-backend.

HardwareDefault for Llama/Qwen-style modelsNotes
H100, H200 (Hopper)fa3FlashAttention 3
B200, B300 (Blackwell)trtllm_mhaNVIDIA's TensorRT-LLM kernels; fa4 also available
A100, L40S, RTX cardsflashinferFlashInfer, a kernel library built for serving
AMD InstinctaiterAMD's kernel library
AnywheretritonPortable fallback written in Triton

DeepSeek-style MLA models have their own kernels (flashmla, trtllm_mla, cutedsl_mla), and so do sparse-attention models.

CUDA graphs

A decode step for a small batch is hundreds of tiny GPU kernels, each finishing in microseconds. Launching each one from Python costs more time than running it. A CUDA graph records the whole sequence of launches once and replays it with a single call.

The catch: a graph is recorded for one exact batch size. So at startup SGLang captures decode graphs for a list of sizes (1, 2, 4, 8, 12, 16, 24, 32, and so on up to 256 on an H100) and pads each real batch up to the nearest captured size. That is why startup takes a minute, and why the log line says cuda graph: True on most decode steps. Recent versions also capture prefill in pieces, with attention run normally between them.

One decode step, launched or replayed
Simulation

1 Where the time goes in a small decode step

Step done at 204 µs. The GPU sat idle for 90 µs of it.

Eager. The CPU spends 5 µs launching each kernel, and with 4 requests most kernels finish faster than that, so the GPU waits for the next launch. It sits idle 90 µs of this 204 µs step.

Step time204 µsgraph: 119 µs
GPU busy56%graph: 96%
Launch calls from the CPU40graph: 1
CPU launches a kernelCPU replays the graphGPU runs a kernelGPU idle, waiting for work

Simplified model, illustrative timings: 40 kernels stand in for the hundreds in a real step, each launch costs the CPU 5 µs, and each kernel takes 2–14 µs, growing with the batch. Preparing the batch’s inputs is left out.

2 Which captured graph a batch uses

Server flags

none for this size. 256 is the default on one 80 GB GPU.

36 decode graphs captured at startup, one per batch size:

1248121624324048566472808896104112120128136144152160168176184192200208216224232240248256

13 real rows + 3 dummy = the size-16 graph

13 requests replay the size-16 graph. SGLang finds it with a binary search (bisect) over the sorted sizes, fills the 3 spare rows with dummy inputs, and drops their outputs after the replay.

Graph replayedsize 16
Dummy rows3
Rows of work thrown away19%
real request rowdummy row, output thrown awaycaptured size that runsrow run eagerly, without a graph

With 4 or more GPUs in tensor parallel, the default on an 80 GB card rises to 512. Setting --cuda-graph-max-bs-decode higher captures more graphs, which costs startup time and some GPU memory for the graphs' buffers.

From logits to a token

The model's last layer produces a score (logit) for every token in the vocabulary, about 150,000 of them for Qwen. The sampler then applies, in order:

  1. Repetition, frequency and presence penalties
  2. The grammar mask, if the request asked for structured output (next chapter)
  3. logit_bias
  4. Temperature, then top-k, top-p and min-p filtering, then a random draw

Temperature 0 becomes plain argmax (greedy). When every request in a batch is greedy, the whole sampling step is a single argmax. These are the same knobs you set in an API call; here you can see where each one acts.

Sampling, one stage at a time
Interactive

PromptSplit the bill and leave a generous

Divides logits by 0.8, so gaps widen and the top token gains.

Off (top_k = -1), so there is no limit on how many stay.

Keeps top tokens until they add up to 90%. That is 4 tokens here.

Off at 0. Nothing is dropped.

tip tip review review note note generous tip tip tip generous review review note tip tip tip tip review generous

Top-p 0.9 is doing the work here. Temperature 0.8 sharpens the softmax, then top-p keeps the 4 most likely tokens that together reach 90%, and SGLang rescales them to sum to 100% before drawing. Move any knob and watch the change flow left to right.

Tokens still in the running4 of 12
Chance of “tip” after the filters64%
Different tokens in 20 draws4
probability after softmaxafter filtering and renormalizingdropped by this filterkept by this filterfilter offexpected count in 20 drawslatest draw

Simplified model: 12 made-up candidates stand in for a vocabulary of about 150,000 tokens. Penalties, the grammar mask and logit_bias act on the logits before stage 1; none are set here. The 20 draws reuse the same random numbers until you press Sample again, so moving a knob shows the same luck landing on the new distribution.

check yourself
37 requests are decoding. What does a CUDA-graph decode step actually run?
Next: Structure and speed →

Structured output and speculative decoding

In one line

A grammar mask makes invalid tokens impossible, so JSON output always parses unless it is cut off by max_tokens. Speculative decoding lets a small fast model guess several tokens ahead and the big model check them all in one pass.

Structured output: grammar masks

If you have used response_format with a JSON schema on the OpenAI API, you have used this. The engine compiles your schema into a grammar. Before every sampling step, the grammar computes which tokens are legal next, and every other token's logit is set to -∞. The model can only pick among what the schema allows, so the output parses as long as max_tokens does not cut it off (check for finish_reason: "length").

Watch the mask work
Step through · schema: {"item": string, "price": number}
Output so far
Model’s top candidates: probability before → after the mask
  • Sure41% → 0%
  • Here22% → 0%
  • ```14% → 0%
  • {12% → 100%
  • The11% → 0%

The schema says the answer must be a JSON object, so the only legal first token is an opening brace. The model would rather chat, but those tokens are masked to probability zero.

Step 1 of 5

SGLang's default grammar engine is xgrammar (--grammar-backend also accepts outlines and llguidance). You can constrain with a JSON schema, a regular expression or an EBNF grammar, one per request. Through the OpenAI API, use response_format for JSON and extra_body={"regex": ...} or {"ebnf": ...} for the others. For reasoning models launched with --reasoning-parser, the constraint switches on only after the model finishes thinking, so the thinking text stays free-form.

History note: the 2024 SGLang paper also introduced "jump-forward" decoding, which skipped ahead when only one continuation was legal. The functions still exist in constrained/, but nothing in the current scheduler calls them.

Speculative decoding: guess, then check

Decode is slow because each token needs a full pass through the big model, and that pass is mostly spent waiting on memory. Checking 5 tokens costs the big model about the same as generating 1, because it reads its weights once either way. So:

  1. A small, cheap draft model guesses the next few tokens.
  2. The big target model checks all the guesses in a single pass.
  3. Guesses are kept up to the first one the target disagrees with. At that point the target's own token is used instead, so every pass gains at least one token.

With greedy sampling, the output is exactly what the big model would have produced alone. With temperature above 0, a rejection-sampling rule keeps the distribution the same. You get the big model's answer, faster.

Speculative decoding simulator
Simulation

Each row is one big-model pass. Accepted guesses are kept up to the first rejected one, and the big model adds the outlined token itself. This run takes 7 passes instead of 16.

pass 1Eachguestfee;person+2
pass 2owes$30butAnn$36+2
pass 3afterthe20%tip,+5
pass 4but!Allguestand+1
pass 5Samstillguestpaysalready+2
pass 6paidhispartbeforeshare+3
pass 7.+1
Big-model passes, normal decoding16
Big-model passes with drafting7
Expected tokens per pass3.05
draft guess accepteddraft guess rejectedguessed after a rejection, thrown awaytoken from the big model

Simplified model: each guess is right with the chance you set, independently of the others, and the draft model’s own time is left out.

The speedup depends on how often the draft is right, which depends on how predictable the text is. Code and structured text accept long runs; creative writing accepts fewer. Each round also costs the draft model's time, which is why good drafts are tiny.

SGLang supports several drafting methods through --speculative-algorithm:

  • EAGLE3 (the docs' recommendation) and EAGLE: a one-layer draft head, cheap and well informed, that drafts a small tree of guesses. The widget below walks through one pass. You need a trained draft head for your model, published for many popular ones. SpecForge, a sibling project, trains them.
  • MTP: models like DeepSeek-V3 ship with built-in multi-token-prediction layers. There is no plain MTP value; you pass EAGLE (or its alias NEXTN) and SGLang uses those layers as the draft.
  • STANDALONE: a small ordinary model from the same family as the big model serves as the draft, for example Qwen2.5-1.5B for Qwen2.5-7B.
  • NGRAM: no extra model at all; it guesses by finding the current text earlier in the context. It works well for editing and retrieval tasks that copy text.

Once it runs, the decode log line shows accept len, the average number of tokens gained per big-model pass.

The simulator above drafts a single line of guesses, so one wrong guess ends the run. EAGLE hedges its bets. Its draft head works from the big model's hidden states, the vectors the big model computes for each token on its way to choosing the next one. At each step the draft head keeps its top few guesses, and the guesses grow into a small tree. Here is one pass, picking up the sentence after "Each person owes".

One EAGLE pass: draft a tree, verify it at once
Step through

Server flags --speculative-num-steps 3 --speculative-eagle-topk 2 --speculative-num-draft-tokens 7

10 of 10 · Result

The big model’s own pick “20%” comes free with the pass and becomes the root of the next tree. This pass gained 4 tokens from one big-model forward pass. Plain decoding gains 1, and topk 1 with the same depth would have gained 2 here.

Output
Eachpersonowes$36afterthe20%

Draft tree · score under each guess

owesroot$36.55$30.30with.25after.22before.14plus.090the.18tip.051the.16tip.035

Tree attention mask · one verify pass, 7 tokens

Tree attention mask: each row is one token in the verify pass; a filled cell means that token can read the column’s token.
tokennextprefix0123456
big model’s next token: $36readsreads owesmaskedmaskedmaskedmaskedmaskedmasked
big model’s next token: afterreadsreads owesreads $36maskedmaskedmaskedmaskedmasked
big model’s next token: beforereadsreads owesmaskedreads $30maskedmaskedmaskedmasked
big model’s next token: thereadsreads owesreads $36maskedreads withmaskedmaskedmasked
big model’s next token: thereadsreads owesreads $36maskedmaskedreads aftermaskedmasked
big model’s next token: 20%readsreads owesreads $36maskedreads withmaskedreads themasked
big model’s next token: 20%readsreads owesreads $36maskedmaskedreads aftermaskedreads the

Each row is one token in the verify pass. next is the big model’s own choice after the row’s token. A filled cell means the row’s token can read that column’s token.

Row 6 (“owes $36 after the”) reads the cached prefix and its own branch. The mask hides the other 3 tokens. Tap a row or a guess in the tree to compare.

Tokens verified in one big-model pass7
Tokens gained this pass (log: accept len)4
Guesses accepted (log: accept rate)3 of 6
guess being drafted or verifiedpruned before verifyacceptedrejectedtoken from the big modelcached prefix

Simplified model: one request, greedy decoding (temperature 0), and hand-written probabilities for the draft head and choices for the big model. A real draft head scores its whole vocabulary at every step.

Two details make the single pass work. Guesses at the same depth share a position number, because each would sit at the same place in the text, and the mask keeps them from reading each other. The verify pass also saves the big model's hidden states, so the draft head can start the next tree from the token the big model just added. Bigger trees are not free. Each draft token adds work and memory to the verify pass, and each draft step is one more draft-head pass, so with a full batch a bigger tree can be slower. The docs suggest leaving all three flags unset so SGLang picks values for your model. SGLang's automatic choice for Llama models is 5 steps, topk 4 and 8 draft tokens. That makes 68 candidate guesses, pruned to the best 7.

check yourself

Why can checking 6 draft tokens cost the big model about as much as generating 1?

Many GPUs, many machines

In one line

When one GPU is not enough, you can split each layer across GPUs (tensor parallel), run copies (data parallel), spread a mixture-of-experts model's experts out (expert parallel), stack layers in stages (pipeline parallel), or send prefill and decode to different machines (PD disaggregation).

You will see these acronyms in every launch command for a big model. Each one is a different way to cut the same work. Pick a tab.

Five ways to use four GPUs
Simulation

All-reduce over NVLink. The GPUs swap their partial results and add them up, so each one ends with the full output of the attention block. This happens twice per layer, so an 80-layer model does it 160 times in every forward pass.

Layer12 of 80
BlockAttention
All-reduces in this forward pass23 of 160
this GPU’s slice of the weightsslices held by the other GPUspartial results, one color per GPUKV cache, split by attention head

One model, sliced. Every weight matrix is cut into four slices, one per GPU, and the KV cache is split by attention head the same way. Each GPU multiplies the same input by its own slice, then all four add up their partial results (an all-reduce) before the next block can start. That exchange happens twice per layer, so TP needs very fast links between GPUs (NVLink) and normally stays inside one machine. It is the standard way to fit a model too big for one GPU, such as Llama 3.1 70B on four H100s, and it also speeds up each token, since every GPU reads only a quarter of the weights.

Flag

--tp-size 4, also spelled --tensor-parallel-size 4.

In the codelinear.py · RowParallelLinear, the all-reducellama.py · LlamaDecoderLayerparallel.py · tp_size

Simplified model: one all-reduce after the attention block and one after the MLP block of each layer, as in Llama. Each GPU sends its partial result to every other GPU here; real all-reduce kernels move the same data in smarter patterns. Slowed down enormously.

These combine. A typical large deployment of a DeepSeek-class model uses PD disaggregation across machines, expert parallelism and DP attention within each server, and the gateway in front. For a model that fits on one machine, plain TP plus a few DP copies behind the gateway covers most needs.

check yourself

You have two machines with 8 GPUs each, joined by an ordinary network. Which split usually goes between the machines?

Next: SGLang vs vLLM →

SGLang, vLLM and the rest

In one line

SGLang and vLLM are the two big open-source engines, they borrow from each other constantly, and by now they share most core ideas. Pick by model support and by benchmarking your own workload, not by reputation.

Everything in chapters 1-3 applies to both. vLLM made paged KV memory famous in 2023 with PagedAttention; SGLang made prefix sharing famous in 2024 with RadixAttention. Each has since adopted the other's idea in its own form. The README of SGLang credits vLLM among the projects it learned from, and SGLang's own benchmark tool can drive a vLLM server for side-by-side tests.

SGLangvLLM
Started2024, LMSYS (RadixAttention paper)2023, UC Berkeley (PagedAttention paper)
Prefix cacheRadix tree of token runs, on by default; 1-token granularity on most GPUs (64-token pages on B200, whose default kernels need them)Hashes of fixed-size blocks ("automatic prefix caching"), on by default
BatchingContinuous batching, chunked prefill, CPU work overlapped with the GPU (on by default)Continuous batching, chunked prefill, async scheduling that overlaps CPU work with the GPU (on by default)
Structured outputxgrammar by defaultxgrammar and others
APIsOpenAI, Anthropic Messages, Ollama, native /generate, offline EngineOpenAI-compatible server, Anthropic Messages, offline LLM
Often chosen forLarge MoE deployments (DeepSeek-style), RL rollouts, agent and chat workloads with heavy prefix reuseBroadest model and hardware coverage, largest community and integrations
Launchsglang serve MODELvllm serve MODEL

The prefix cache row hides a small practical difference. SGLang's radix tree compares a new prompt with cached ones token by token. vLLM's automatic prefix caching cuts every prompt into fixed blocks (16 tokens by default for most models on NVIDIA GPUs; its MLA kernels for DeepSeek-style models need 32 to 128), hashes each full block together with the hash of the block before it, and reuses a block only when that whole chain of hashes matches. A block that is only partly shared is computed again.

How much of a shared prefix gets reused?
Interactive · words stand in for tokens

SGLang radix tree

page size 1 · every token is its own page46 of 53 reused

Block hashing, as in vLLM

16-token blocks32 of 53 reused

A and B share their first 46 tokens. SGLang’s radix tree, matching token by token, reuses 46 tokens. The block cache reuses 32 tokens. Block 2 holds 14 tokens shared with A, but B changes partway through it, so its hash differs. That is 14 tokens of extra prefill. On a 2,000-token shared prefix, rounding would cost at most 15 tokens, 0.75% of it.

Tokens A and B share46
SGLang reuses46 of 53
Block cache reuses32 of 53
Worst-case rounding on a 2,000-token prefix0.75%
reused from the cachecomputedshared with A, computed anywayread, waiting for its block or page to fillB stops matching A

Simplified model: one word or punctuation mark stands for one token, and A is the only cached prompt. Real tokenizers cut text into more pieces than words. Both engines always compute the last prompt token, since its output is the first new token.

The rounding never costs more than one block or page minus a token per request, however long the shared prefix is. With a system prompt of a few thousand tokens that is under 1% of the prompt for 16-token blocks and up to about 3% for 64-token pages, so for most chat and agent traffic the two designs reuse nearly the same amount. The gap shows up when shared prefixes are short. SGLang rounds down too when an attention backend needs pages of more than one token. On a B200 its default kernels use 64-token pages, so there a 16-token block cache rounds less than SGLang does for Llama- and Qwen-style models. For DeepSeek-V3, vLLM's own MLA kernels use blocks of 32 tokens or more too.

Benchmarks between the two change with nearly every release, and both teams publish numbers that favor their own setups. If performance decides it for you, run python -m sglang.benchmark.serving (or vLLM's equivalent) against both, on your model, your GPU and a dataset shaped like your traffic.

Other options you will hear about

ToolWhat it isWhen it fits
Hosted APIs (OpenAI, Anthropic …)Someone else runs the engine and the GPUsLow or spiky volume, closed models, no ops team
Hugging Face transformersThe reference model code, with generate()Research and prototyping; not for serving many users
llama.cpp, OllamaC++ engine for quantized models on laptops, Macs and CPUs; Ollama wraps itOne user, local, offline, no datacenter GPU
TensorRT-LLMNVIDIA's engine, built around compiled, heavily tuned kernelsSqueezing the last bit from NVIDIA hardware, with more setup

When would you reach for SGLang?

  • Self-hosting an open model for cost, privacy or control, at a volume where a GPU stays busy.
  • Big batch jobs with a shared prompt: tagging 500,000 reviews with the same instructions and examples. The radix cache computes the shared part in the first batch and reuses it for the rest, and the offline Engine skips HTTP entirely.
  • Agents with long, repeated context: tool definitions, files, history.
  • Training with reinforcement learning, where a trainer needs millions of sampled answers quickly. verl, slime, AReaL and others use SGLang for this.

When traffic is low, a hosted API is usually cheaper than a GPU idling at a few dollars an hour.

Next: Running it yourself →

Running it yourself

In one line

One command starts an OpenAI-compatible server on port 30000. Point any OpenAI SDK at it, then watch the log lines to see the scheduler and cache at work.

1 · Get a GPU

You need a Linux machine with an NVIDIA GPU (AMD and others work too; see the hardware docs). Renting one by the hour from a cloud GPU provider is the easy path. A 24 GB card is plenty for Qwen/Qwen3-0.6B or an 8B model. SGLang also runs on Apple Silicon Macs through Metal and MLX, which is handy for poking around, though real serving means datacenter GPUs.

2 · Start the server

# with Docker (includes everything)
docker run --rm --gpus all --ipc=host -p 30000:30000 \
  lmsysorg/sglang:latest \
  sglang serve Qwen/Qwen3-0.6B --host 0.0.0.0 --port 30000

# or with uv in a Python 3.12 environment
uv pip install --prerelease=allow sglang
sglang serve Qwen/Qwen3-0.6B --host 0.0.0.0 --port 30000

Wait for The server is fired up and ready to roll!. The older spelling, python -m sglang.launch_server --model-path …, still works and appears in many docs pages.

3 · Call it like OpenAI

from openai import OpenAI

client = OpenAI(base_url="http://localhost:30000/v1", api_key="EMPTY")

resp = client.chat.completions.create(
    model="Qwen/Qwen3-0.6B",
    messages=[{"role": "user", "content": "Split $90 three ways."}],
    temperature=0.7,
    stream=True,
)
for chunk in resp:
    print(chunk.choices[0].delta.content or "", end="")

Structured output uses the standard OpenAI parameter:

resp = client.chat.completions.create(
    model="Qwen/Qwen3-0.6B",
    messages=[{"role": "user", "content": "Extract the dish and price: 'Pizza, $12'"}],
    response_format={
        "type": "json_schema",
        "json_schema": {"name": "line_item", "schema": {
            "type": "object",
            "properties": {"item": {"type": "string"}, "price": {"type": "number"}},
            "required": ["item", "price"],
        }},
    },
)

4 · Or skip HTTP for batch jobs

The offline engine runs the same three-process system inside your Python script, which is ideal for one-off jobs over a big file:

import sglang as sgl

llm = sgl.Engine(model_path="Qwen/Qwen3-0.6B")
prompts = [f"Label the review as positive or negative: {r}\nLabel:" for r in reviews]
outputs = llm.generate(prompts, {"temperature": 0, "max_new_tokens": 4})
labels = [o["text"].strip() for o in outputs]
llm.shutdown()

Every prompt in that list shares its instruction prefix, so the radix cache computes it in the first batch and reuses it for every batch after that.

5 · The flags worth knowing

FlagWhat it controlsChapter
--tp-size NSplit the model across N GPUs9
--dp-size NRun N copies inside one server. For production the docs recommend the gateway: the same flag on python -m sglang_router.launch_server9
--mem-fraction-staticShare of GPU memory for weights plus KV pool. Lower it if you hit out-of-memory errors3
--context-lengthMaximum tokens per request; defaults to the model's config3
--kv-cache-dtypeStore the cache in FP8 to fit about twice the tokens3
--max-running-requestsCap on concurrent decoding requests5
--chunked-prefill-sizeMost prompt tokens one prefill pass computes, summed over its requests; longer prompts are split into slices5
--schedule-policyQueue order: fcfs (default), lpm, …5
--disable-radix-cacheTurn prefix caching off (for comparison or debugging)6
--enable-hierarchical-cacheSpill evicted KV to CPU memory and storage6
--enable-cache-reportReport cached prompt tokens in API usage6
--attention-backendWhich attention kernels to use7
--quantizationLoad weights in a compressed format (e.g. fp8)3
--speculative-algorithmTurn on speculative decoding (EAGLE3, NGRAM, …)8
--reasoning-parser, --tool-call-parserSplit thinking from the answer; parse tool calls into OpenAI format-
--enable-metricsPrometheus metrics at /metrics-

The full list is long; run sglang serve --help or see the server arguments page in the docs. The SGLang Cookbook gives ready-made launch commands per model and GPU, which beats tuning by hand.

Most of those flags have good defaults, so a real command needs only a few. Pick a model, your GPUs and what you need below. The builder sizes tensor parallelism with the memory math from chapter 3, adds only the flags your needs call for, and writes the client code that goes with them.

Build a launch command
Interactive · tap a flag
What you need

Memory per GPU for each tensor-parallel size

Each copy needs its weights plus room for at least 16K tokens of KV cache, a few requests at once. All copies together should hold about 256K for many users.

1 GPU: weights do not fit; 2 GPUs: 70.6 GB of weights per GPU and room for 4.3K tokens; 4 GPUs: 35.3 GB of weights per GPU and room for 429K tokens; 8 GPUs: 17.6 GB of weights per GPU and room for 1.3M tokens. Picked 4.

model weightsKV cache poolscratchthe size the builder picks

The command

sglang serve \   \   \   \   \   \   

--tp-size 4 splits each layer across 4 GPUs (tensor parallelism, chapter 9). Llama 3.1 70B has 70.6B parameters at 2 bytes each, 141 GB of weights. With 2 GPUs the cache would hold only 4.3K tokens. With 4, each GPU keeps 35.3 GB of weights and about 35.1 GB of KV cache, room for 429K tokens (target 16K per copy).

    Calling it with the OpenAI SDK

    from openai import OpenAI client = OpenAI(base_url="http://localhost:30000/v1", api_key="EMPTY") tools = [{"type": "function", "function": {    "name": "split_bill", "description": "Split a bill evenly",    "parameters": {"type": "object", "properties": {        "total": {"type": "number"}, "people": {"type": "integer"}},        "required": ["total", "people"]}}}] resp = client.chat.completions.create(    model="meta-llama/Llama-3.1-70B-Instruct",    messages=[{"role": "user", "content": "Split $90 three ways."}],    tools=tools,)msg = resp.choices[0].messageprint(msg.tool_calls)  # parsed by --tool-call-parserprint(msg.content)print(resp.usage.prompt_tokens_details.cached_tokens)  # needs --enable-cache-report

    Simplified rule: each model copy gets the fewest GPUs that hold its weights plus the cache target above, and Many users adds copies on the GPUs left over.

    JSON output is the one goal with no flag. Structured output is on by default with the xgrammar backend, and each request asks for it with response_format. Speculative decoding (chapter 8) is left out. EAGLE-style drafting needs a draft model trained for your model, DeepSeek-V3 carries its own draft layer (multi-token prediction, MTP), and NGRAM needs no draft model at all, so take the exact flags from the model's Cookbook page.

    Read the server log
    Tap or hover any field
    [2026-10-24 14:02:11] Prefill batch, #new-seq: 3, #new-token: 1536, #cached-token: 6144, token usage: 0.03, #running-req: 12, #queue-req: 0, #pending-token: 0, cuda graph: True, input throughput (token/s): 21480.55
    [2026-10-24 14:02:13] Decode batch, #running-req: 15, #token: 18240, token usage: 0.04, cuda graph: True, gen throughput (token/s): 1287.40, #queue-req: 0

    Prefill batch This pass ran prefill (EXTEND) for newly admitted requests. You get one of these lines for every prefill pass.

    6 · Measure it

    python -m sglang.benchmark.serving --backend sglang \
      --host 127.0.0.1 --port 30000 --num-prompts 500

    It reports the numbers serving people argue about: TTFT (time to first token, mostly prefill and queueing), ITL or TPOT (time between output tokens, mostly decode), and total throughput. Try the same run with --disable-radix-cache on the server, or with a different --max-running-requests, and you will see the trade-offs from this guide in your own numbers.

    check yourself

    A decode line shows #running-req: 15 and gen throughput (token/s): 1287.40. Roughly how fast is each user's stream?

    Next: Reading the code →

    Reading the code

    In one line

    The repository is huge (over 11,000 files), but the core path is about a dozen files. Read them in the order a request travels.

    Almost everything that matters is under python/sglang/srt/. The rest is kernels (python/sglang/kernels/, 3rdparty/), the Rust gateway (sgl-model-gateway/), image and video generation (python/sglang/multimodal_gen/), docs, benchmarks and tests. The paths below are relative to python/sglang/srt/ and link to the exact commit this guide was written against.

    Here is srt/ drawn to scale. Each box's area is its lines of Python. The blue numbers show where the 14 files of the reading order below live; open a folder to find them, or walk the reading order and watch a request's path jump between folders.

    python/sglang/srt/, sized by lines of Python
    Interactive
    14 files
    layers/207,866 lines13models/192,720 lines12mem_cache/92,874 lines89managers/44,837 lines3–71014disaggre…34,950hardware_backend/32,713 linesspeculative/31,916 linesentrypoints/30,768 lines12multimodal/28,482 linesmodel_executor/25,05511arg_groups/22,758lora/22,451utils/21,920configs/19,550function_call/15,266+17 more13,336distri…12,596model_loader/12,366debug_utils/11,593parser/10,037
    python/sglang/srt/

    921,656 lines · 2,115 Python files

    layers/ and models/ alone are 43% of it. The 14 reading-order files add up to 40,447 lines, 4.4%. Pick a folder to look inside.

    Open python/sglang/srt/ on GitHub
    Lines of Python in view921,656
    Python files in view2,115
    Share of srt/100%
    Reading-order files inside14 of 14
    Serving: API, scheduler, sampling rulesKV cache memoryRunning the modelModel definitionsMulti-GPU and hardwareOptional featuresConfig, tools, otherfile in the reading ordersmaller items grouped into one box

    Lines counted like wc -l over every .py file under python/sglang/srt/ at commit 773cb93, so comments and blank lines count. The color groups are this guide’s own. Keyboard: Tab into the map, move between boxes with the arrow keys, press Enter to open a folder and Backspace to go back up. Escape closes the card first, then goes back up.

    Nearly half of the code outside the reading order sits in models/, layers/ and hardware_backend/, which hold per-model code, attention and MoE variants, quantization formats and support for other chips. A first read can skip all of that.

    1. 1entrypoints/http_server.py

      FastAPI app and every HTTP route. Find /v1/chat/completions and follow the call.

    2. 2entrypoints/openai/serving_chat.py

      Turns an OpenAI chat request into a native request: chat template, tools, response format.

    3. 3managers/tokenizer_manager.py

      generate_request tokenizes and sends; handle_loop receives results and wakes waiting requests.

    4. 4managers/io_struct.py

      The message types passed between processes. Skim it to learn the vocabulary.

    5. 5managers/scheduler.py

      The event loops and get_next_batch_to_run. 6,000+ lines; read the two loops and that one function first.

    6. 6managers/schedule_policy.py

      Queue ordering and PrefillAdder, the admission budget.

    7. 7managers/schedule_batch.py

      Req and ScheduleBatch: per-request state, prepare_for_extend, prepare_for_decode, retraction.

    8. 8mem_cache/radix_cache.py

      The radix tree in its clearest form, about 830 lines, plus RadixKey, the prefix key every cache uses. A default server builds the same idea from mem_cache/unified_radix_cache.py on a Rust core (chapter 6). Then mem_cache/README.md for the map of the rest.

    9. 9mem_cache/memory_pool.py

      ReqToTokenPool and the KV pool classes.

    10. 10managers/tp_worker.py

      The bridge: ScheduleBatch → ForwardBatch → forward → sample.

    11. 11model_executor/model_runner.py

      Loads the model, sizes memory, picks the graph runner or eager path.

    12. 12models/llama.py

      A whole model in one file. Compare it with the Hugging Face version.

    13. 13layers/attention/flashinfer_backend.py

      Where K/V gets written to the pool and attention runs.

    14. 14managers/detokenizer_manager.py

      Incremental decoding of token ids back into text.

    Easier ways in

    • Mini-SGLang: a compact rewrite by the same project, meant for learning. If the real scheduler is too much to start with, read this first.
    • DeepLearning.AI short course on efficient inference with SGLang.
    • docs.sglang.io: the advanced features section has a page per topic in this guide.
    • The LMSYS blog: release write-ups that explain why each big feature exists.

    A warning when reading older tutorials: this code moves fast. Server flags moved into arg_groups/fields/, the default radix cache is now the unified version (unified_radix_cache.py) on a Rust tree core, the old ChunkCache is gone, and several flags were renamed. Trust the code over a blog post from last year.

    Next: Glossary →

    Glossary

    All-reduce

    A step where GPUs exchange partial results and each ends with their sum. Tensor parallelism does two per layer.

    Attention backend

    The kernel library that runs attention over the paged KV cache: FlashAttention 3, FlashInfer, TensorRT-LLM, Triton and others.

    Batch
    The set of requests processed together in one forward pass.
    Chunked prefill

    Splitting a long prompt into slices processed over several passes, which caps the memory one pass needs. With --enable-mixed-chunk, decoding requests ride along with each slice.

    Continuous batching

    Adding and removing requests from the batch between every forward pass instead of waiting for a whole group to finish.

    CUDA graph

    A recorded sequence of GPU kernel launches replayed with one call. It removes launch overhead from small decode steps.

    Decode

    Generating output one token per forward pass, after prefill. Limited by memory bandwidth.

    Detokenizer

    The process that turns token ids back into text, holding back half-finished characters.

    DP attention

    Running attention data-parallel (each GPU owns its own requests and KV cache) while the rest of the model stays tensor- or expert-parallel. --attn-dp-size.

    EAGLE

    A speculative decoding method whose draft is a tiny head that reads the target model's hidden states. EAGLE3 is the newest version.

    Eviction

    Freeing cached KV to make room. SGLang evicts unlocked leaves of the radix tree, least recently used first by default.

    EXTEND

    SGLang's name for a prefill pass. It extends a sequence by many tokens, possibly on top of a cached prefix.

    Expert parallelism (EP)

    Spreading a mixture-of-experts model's experts across GPUs and routing each token to the GPUs holding its experts.

    Forward pass

    One run of the input through every layer of the model. A decode forward pass is also called a decode step.

    ForwardBatch

    The GPU-tensor description of a batch that the model actually runs on.

    Grammar mask

    A per-step list of tokens allowed by a JSON schema, regex or grammar; every other token gets probability zero.

    Hidden states

    The vectors a model computes for each token on its way through the layers. EAGLE's draft head reads them.

    HiCache

    Hierarchical cache: KV kept on the GPU, then in CPU memory, then in shared storage. --enable-hierarchical-cache.

    KV cache

    The saved keys and values for every earlier token, so each new token does not recompute them. The main consumer of GPU memory.

    Logit

    The model's raw score for one vocabulary token. Softmax turns the scores into probabilities.

    LPM

    Longest prefix match: a schedule policy that serves waiting requests with the most cached prefix first.

    mem-fraction-static

    The share of GPU memory used for weights plus the KV pool. The rest is scratch space.

    MLA

    Multi-head latent attention (DeepSeek): stores one compressed vector per token per layer instead of full keys and values per head.

    MoE

    Mixture of experts: each layer has many small feed-forward networks, and each token uses only a few.

    New-token ratio

    The scheduler's guess of how much of each running request's remaining max_tokens it will really use. Admission reserves that much room. It starts at 0.7 and decays toward about 0.1.

    Overlap scheduler

    SGLang's default loop, which prepares the next batch on the CPU while the GPU runs the current one.

    Page size

    How many tokens share one allocation unit in the KV pool. SGLang defaults to 1 on most GPUs and usually to 64 on Blackwell GPUs such as the B200, whose default kernels need larger pages.

    PagedAttention

    vLLM's name for attention over KV memory stored in non-contiguous fixed-size blocks.

    PD disaggregation

    Running prefill and decode on separate servers and transferring the KV cache between them.

    Pipeline parallelism (PP)

    Splitting the model's layers into stages on different GPUs or machines.

    Prefill

    Processing the whole prompt in one parallel pass to build its KV cache and produce the first token.

    Prefix caching
    Reusing the KV cache of a shared beginning across requests.
    RadixAttention

    SGLang's prefix caching: a radix tree of token sequences mapping to KV slots, with LRU eviction and cache-aware scheduling.

    Rank

    The number of one process in a multi-GPU group. Rank 0 is the first and coordinates the others.

    Req

    The scheduler's object for one request: prompt ids, output ids, cached length, sampling settings, finish reason.

    Retraction

    Pausing running requests and requeueing them when the KV pool fills mid-generation.

    Router / Model Gateway

    The Rust front door for many SGLang servers, with cache-aware load balancing and PD routing.

    Speculative decoding

    A draft model guesses several tokens; the target model verifies them all in one pass.

    SRT
    SGLang Runtime, the engine itself (python/sglang/srt/).
    Tensor parallelism (TP)

    Splitting every layer's weights across GPUs that exchange partial results after each layer.

    TTFT
    Time to first token: queueing plus prefill.
    TPOT / ITL

    Time per output token, or inter-token latency: the gap between streamed tokens during decode.

    xgrammar

    The default library SGLang uses to compile schemas and grammars into per-step token masks.

    ZeroMQ (ZMQ)

    The messaging library SGLang's processes use to pass requests and results on one machine.

    Back to the start ↑