3.2

--mem-fraction-static and startup sizing

SGLang sizes the KV pool once at startup from a snapshot of free memory, so a machine that changes after startup can kill the whole server.

One of our first SGLang servers on the capture Mac ran for four minutes. Then a benchmark at 32 concurrent requests crashed it with a Metal out-of-memory error, and every process of the server exited. The KV pool at that moment was 95 percent empty. The memory had gone to the pool, and the next prefill had nowhere to run.

When you finish this section, you can read how SGLang divides memory at startup and predict when that division fails. You can also set --mem-fraction-static on purpose.

The crash we recorded

We started the server twice before the capture kit existed, with the same command and no --mem-fraction-static. The two startups sized the pool very differently.

two startups, two pool sizestextFull log
# run 1 startup
[2026-10-16 13:30:31] Auto-sized attention KV pool: sys_available=27.81 GB, mlx_limit=51.8 GB, mlx_used=0.31 GB, kv_budget=24.25 GB, bytes_per_slot=114688, pool_size=227022
# run 2 startup
'mem_fraction_static': 0.872
[2026-10-16 13:38:58] Auto-sized attention KV pool: sys_available=38.29 GB, mlx_limit=51.8 GB, mlx_used=0.31 GB, kv_budget=33.39 GB, bytes_per_slot=114688, pool_size=312581
Lines from two exploratory runs on the same Mac, before the capture kit. The comment lines are ours.

Nothing changed in SGLang between the runs. Other programs on the Mac used less memory at the second start, so sys_available grew from 27.81 GB to 38.29 GB. The pool grew with it, from 24.25 GB to 33.39 GB. Both budgets are 87.2 percent of the free memory, because the automatic mem_fraction_static resolved to 0.872.

the crash at concurrency 32textFull log
[2026-10-16 13:42:53] Decode batch, #running-req: 32, #token: 16095, token usage: 0.05, cuda graph: False, gen throughput (token/s): 564.55, #queue-req: 0
[2026-10-16 13:42:55] Prefill batch, #new-seq: 1, #new-token: 511, #cached-token: 0, token usage: 0.00, #running-req: 0, #queue-req: 9, #pending-token: 0, cuda graph: False, input throughput (token/s): 66.85
RuntimeError: [METAL] Command buffer execution failed: Insufficient Memory (00000008:kIOGPUCommandBufferCallbackErrorOutOfMemory).

[2026-10-16 13:42:57] SIGQUIT received. signum=None, frame=None. It usually means one child failed.
…
[2026-10-16 13:43:02] kill_process_tree called: parent_pid=81559, include_parent=True, pid=81559
Lines from run 2. The traceback above the RuntimeError line is cut.

Read token usage: 0.05 first. Thirty-two requests held 16,095 tokens, which is 5 percent of a 312,581-token pool. The pool reserved 33 GB and used under 2 GB of it. The activations of the next forward pass needed memory outside the pool, and the operating system had none left. One failed command buffer took down the scheduler, and the server killed its whole process tree.

Fig. 3.3the same Mac, two memory splitscomparison
The same Mac, two memory splits Two bars on one scale of 0 to 50 GB. Exploratory run 2 saw 38.29 GB free and gave 33.39 GB to the KV pool with mem_fraction_static 0.872, leaving 4.90 GB, and it crashed with Insufficient Memory at concurrency 32. The kit run saw 48.49 GB free and gave 19.39 GB to the pool with 0.4, leaving 29.10 GB, and it finished 128 requests at concurrency 32. EXPLORATORY RUN 2 · FRACTION 0.872 (AUTOMATIC) KV pool 33.39 GB left for activations and other programs: 4.90 GB Insufficient Memory at concurrency 32, server killed CAPTURE KIT · --MEM-FRACTION-STATIC 0.4 KV pool 19.39 GB left for activations 29.10 GB 128 of 128 requests finished at concurrency 32 0 GB 10 GB 20 GB 30 GB 40 GB 50 GB
A pool sized at 87 percent of free memory left too little for activations, and a fraction of 0.4 kept the server alive. Read each bar left to right. The green part is the KV pool, and the grey part is everything else.

How SGLang decides the split

--mem-fraction-static: the share of memory that SGLang gives to the model weights plus the KV pool. Everything outside that share is for activations, CUDA graphs, and other programs. The flag defaults to None, and SGLang computes a value at startup.

On a GPU, the automatic value comes from a reserve. SGLang starts with 512 MB and adds 1.5 MB for each token of the prefill chunk. Then it adds more for tensor and pipeline parallelism and for CUDA graphs. On a GPU with more than 60 GB, the reserve is at least 10 GiB. The fraction is then the GPU memory minus the reserve, divided by the GPU memory. When SGLang cannot read the device capacity, it uses 0.95.

After the weights load, SGLang measures free memory again. The KV budget is the free memory minus the 1 - fraction share of the memory that was free before loading. The pool holds that budget divided by the bytes per token from section 3.1. If the budget is not positive, startup stops and prints the smallest fraction that would work.

The MLX backend on Apple silicon uses a simpler rule. It takes the smaller of two numbers. The first is the fraction of the Metal working set, minus memory already in use. The second is the fraction of system memory available right now. The second term is the snapshot that changed between our two runs.

Read the fraction as the share for the weights and the KV pool together. That is what the code does.

The default assumes the machine belongs to SGLang

The automatic rule is reasonable on a dedicated GPU. Nothing else allocates memory there, and the reserve covers the activations of one prefill chunk and the CUDA graphs. The rule fails in three common cases.

  • A shared machine. A laptop, a workstation, or a node with other jobs. The snapshot at startup does not hold for the life of the server.
  • Unknown capacity. On a device where SGLang cannot read the memory size, the fraction becomes 0.95 and leaves 5 percent for activations. Issue #39675 reports exactly that failure on Apple MPS, an out-of-memory error on the first prefill.
  • Extra memory the estimate does not know about. Issue #42510 shows draft-model copies that stay in memory while the target pool is sized, so the draft pool allocation fails. Issue #6027 shows a 64K-token DeepSeek-V3 prompt that ran out of memory in attention, although the log reported room for 274,934 KV tokens.

Unified memory on a GB10

We measured the same rule on an NVIDIA GB10, the chip of a DGX Spark. Its CPU and GPU share 121.7 GiB of memory. There is no separate GPU memory, and that changes what each number in the startup log means. Raw runs

one GB10 startup, from capacity to pooltextFull log
[2026-10-17 14:12:57] Failed to get GPU memory capacity from nvidia-smi. Falling back to torch.cuda.mem_get_info(). Reported total GPU memory per device (MiB): [124610], using min: 124610 MiB.
…
[2026-10-17 14:13:05] Load weight begin. avail mem=112.59 GB
…
[2026-10-17 14:18:29] Load weight end. elapsed=323.61 s, type=Qwen3MoeForCausalLM, avail mem=54.84 GB, mem usage=57.75 GB.
[2026-10-17 14:18:32] KV Cache is allocated. dtype: torch.bfloat16, #tokens: 227243, K size: 10.40 GB, V size: 10.40 GB
[2026-10-17 14:18:32] Memory pool end. avail mem=31.22 GB
Lines cut from the startup of the default GPU configuration, Qwen3-30B-A3B in BF16 with a fraction of 0.7.

The first line is the capacity. nvidia-smi reports no memory size for the GB10, so SGLang takes the total from PyTorch.

The second line is free memory, and here the code is careful. On a device that shares system memory, torch.cuda.mem_get_info() counts the file cache of the kernel as used. On a day with a large file cache, that reading can be far below what the machine can give. SGLang checks whether the device is integrated, and then it reads the available memory of the operating system instead. That number counts cache that the kernel can free. In our recording after a reboot, the two numbers agreed: 112.1 GiB from PyTorch and 112.59 GB from SGLang.

The cost of that choice is that the pool size moves with the rest of the machine. Six BF16 starts used the same command and the same weights:

ConfigurationAvailable before weightsWeights, as SGLang measured themKV pool, tokens
default112.59 GB57.75 GB227,243
no CUDA graph113.21 GB54.78 GB258,017
deterministic113.44 GB57.90 GB234,517
no radix cache113.17 GB57.34 GB236,315
no overlap113.37 GB57.92 GB233,822
EAGLE-3, with a draft model113.45 GB57.49 GB216,775

The weights did not change, yet their measured size went from 54.78 GB to 57.92 GB. SGLang measures them as the drop in available memory, and the kernel cache changed during each load. Among the five starts without a draft model, the pool differs by 14 percent. The flags do not explain the spread, because the same weights measured 3 GB apart. On unified memory, a pool size from one start is an estimate for the next start, not a constant.

A pool that other programs see

On a GB10, the KV pool is tens of GB, and SGLang allocates it in a few seconds. Every other program on the machine sees that memory go. Our GB10 also ran a memory guard daemon for its other users. It stops the process that grows fastest when free memory gets low. It stopped our scheduler three times during startup:

the memory guard stops the schedulertextFull log
2026-10-16T12:53:16+00:00 2026/10/16 12:53:16 guardian level: Critical: 34.2 GiB left before the freeze zone.
2026-10-16T12:53:16+00:00 2026/10/16 12:53:16 guardian pause: Paused sglang::schedul (pid 2983880, rohit): it grew 36.7 GiB in 5s with 34.2 GiB left. Resume or stop it from the app.
The first two lines of the guard log for our runs on the GB10.

We did not keep the server logs of those three starts. The guard log shows the scheduler growing by 36.7 GiB in 5 seconds, which is the size of a KV pool allocation. The guard was correct by its own rule: the scheduler was the fastest-growing process. Seconds later the guard logged "Back to normal", because the pool was complete. We agreed with the owner of the machine to stop the guard for the time of our runs.

The NVIDIA driver also reports memory errors at startup. The kernel log of the GB10 holds bursts of this line:

the driver line at server startuptextFull log
NVRM: nvCheckOkFailedNoLog: Check failed: Out of memory [NV_ERR_NO_MEMORY] (0x00000051) returned from _memdescAllocInternal(pMemDesc) @ mem_desc.c:1359
The last line of the file. The lines above it count the bursts per minute.

The bursts are dated 2026-10-16, the day of our first test and sweep starts on the GB10. We did not keep the server logs of that day, so we cannot match each burst to one start. Every server in our later captures became healthy and finished its benchmarks.

Our position: on unified memory, the KV pool is memory that the whole machine gives up. Pass --mem-fraction-static on purpose, and tell the other users of the machine before you start. Do not alert on the driver line alone. Alert when a server does not become healthy.

Memory outside the fraction

The fraction does not bound all the memory that SGLang takes at startup. One user served a 27B model with SGLang on a DGX Spark. The user reported 25 to 40 GB of short-lived allocations outside the memory accounting of SGLang. The FlashInfer autotuner and the CUDA graph capture made them. Run directly on the host, the server froze the machine, and it needed a power cycle. The user then ran it in a container with a memory cap and --mem-fraction-static 0.50.

That is a field report from before v0.5.22, and it does not name its SGLang version. Treat it as a warning, not as a fact about v0.5.22. We did not measure it ourselves. Our guard log above shows a fast growth at startup, but it does not separate the pool from the other allocations.

On a GB10 that other programs share, start SGLang in a container with a memory limit. Start below the fraction of 0.7 that our kit used. Log the free memory of the machine once a second during startup. Raise the fraction only after you saw the peak.

Our position: set the fraction yourself on any shared machine

On a machine that runs anything besides SGLang, pass --mem-fraction-static yourself. The capture kit uses 0.4 on the Mac. With that value, the same benchmark that crashed run 2 finished all 128 requests at concurrency 32.

the kit's server, with an explicit fractiontextFull log
[2026-10-16 13:58:03] Auto-sized attention KV pool: sys_available=48.49 GB, mlx_limit=51.8 GB, mlx_used=0.31 GB, kv_budget=19.39 GB, bytes_per_slot=114688, pool_size=181574
One startup line from the default kit configuration.

The budget is now 19.39 GB, which is 0.4 of the 48.49 GB snapshot. The pool still holds 181,574 tokens, far more than 32 requests of 640 tokens need. A smaller pool cost nothing in this workload, because the workload never filled the pool.

We tested the other direction on the GB10. The load sweep of section 5.1 ran twice, at the kit's fraction of 0.7 on 2026-10-16 and at 0.82 on 2026-10-17, three seeds per point.

a larger pool on the GB10, at its fullesttextFull log
[2026-10-17 16:35:26] KV Cache is allocated. dtype: torch.bfloat16, #tokens: 383065, K size: 17.54 GB, V size: 17.54 GB
[2026-10-17 16:47:45] Decode batch, #running-req: 64, #token: 40751, token usage: 0.11, cuda graph: True, gen throughput (token/s): 263.88, #queue-req: 0
The pool at a fraction of 0.82, and the decode line with the highest token usage in the whole sweep.

We measured a BF16 pool of 383,065 tokens against 234,600, 63 percent more. The FP8 pool grew from 1,084,458 to 1,378,545 tokens. The throughput did not move. At 64 users, BF16 gave 238.72 against 235.38 tok/s, and FP8 gave 442.71 against 440.86. At every concurrency, the two medians were within 2 percent of each other. Raw runs

Fig. 3.4A larger pool bought nothing when the pool was never fullchart
A larger pool bought nothing when the pool was never full Output throughput against concurrency from 1 to 64 for Qwen3-30B-A3B on one NVIDIA GB10, three seeds per point, at --mem-fraction-static 0.7 and 0.82. The BF16 pool grew from 234,600 to 383,065 tokens and the FP8 pool from 1,084,458 to 1,378,545, yet each pair of lines lies on top of the other: 235 against 239 tok/s for BF16 and 441 against 443 for FP8 at 64. 0 100 200 300 400 500 1 2 4 8 16 32 64 max concurrency output tokens per second BF16, 0.7 BF16, 0.82 FP8, 0.7 FP8, 0.82
Raising the fraction from 0.7 to 0.82 on the GB10 grew the BF16 pool by 63 percent and changed closed-loop throughput by less than 2 percent. Output throughput against max concurrency, three seeds per point. Solid lines are 0.7 and dashed lines are 0.82. Plum is BF16 and green is FP8 with an FP8 KV cache.

The log line explains it. Sixty-four requests held 40,751 tokens, 11 percent of the pool. On the kit's server at 0.7, the highest token usage was 18 percent. The pool was never the limit, so a larger pool had nothing to do. Compare the L4 in section 4.3, where the BF16 pool was full at 64 users and FP8 nearly tripled the throughput.

The open-loop sweep of section 5.1 gave the other half of the answer. At 16 requests per second, more than the GB10 serves, requests kept arriving while earlier ones still ran. There the larger pool did matter.

the pool at 0.7 under an open-loop overloadtextFull log
[2026-10-16 19:08:23] Decode batch, #running-req: 374, #token: 227530, token usage: 0.96, cuda graph: False, gen throughput (token/s): 866.37, #queue-req: 101
One decode line at 16 offered requests per second, at a fraction of 0.7.
the pool at 0.82 under the same overloadtextFull log
[2026-10-17 18:00:45] Decode batch, #running-req: 480, #token: 289721, token usage: 0.75, cuda graph: False, gen throughput (token/s): 994.20, #queue-req: 0
The decode line with the highest token usage at 16 offered requests per second, at a fraction of 0.82.

At 0.7 the pool was 96 percent full, and 101 requests waited outside it. At 0.82 all 480 requests of the run fit at once. We measured 5.19 completed requests per second against 3.88, 34 percent more, and 664 against 497 output tok/s, three seeds each. The two runs were a day apart, so part of the gap can be the machine. The log lines show the mechanism, and the size of the gain belongs to this run. FP8 had a pool of over a million tokens at both fractions, and its rate did not change: 8.29 against 8.26.

So the extra 13.6 GB bought nothing at 64 users and a third more throughput in an overload. Size the pool from your token usage at peak load, including the burst that arrives faster than you serve. On a machine that the GB10 shares with other programs, memory you do not need is memory another program cannot use.

On a dedicated GPU, trust the automatic value first. Raise the fraction in steps of 0.02 only when the scheduler log shows a full pool and retractions. Find the upper limit in staging with your longest prompts, then run production one or two steps below it. Lower it when long prompts fail in attention, as in issue #6027.

The fraction also changes comparisons. One independent benchmark ran SGLang with its automatic fraction and vLLM with its default of 0.9 of GPU memory. The two engines had KV pools of very different sizes. Read section 5.3 before you trust a comparison that does not state both values.

What to measure

  • The max_total_num_tokens line at startup. It is the result of the whole calculation.
  • token usage in decode lines. A value near 1.0 means the pool is full.
  • The free memory of the machine during the heaviest prefill, from the operating system.

One more caution for Apple silicon. The kv_cache_memory_usage_gb metric read 0.0 in our capture, because the MLX backend allocates its own pool outside the accounting that the metric reads. Use the MlxAttentionKVPool log line, which reported about 19,860 MB.

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