vLLM Explained

Tuning vLLM: throughput and latency knobs, their real defaults and what each one does

AdvancedvLLM conceptsabout 20 min read + 5 min deep dives

Code version: vLLM @ 3e44c0d · 2026-10-22

In one sentence

Memory knobs fix the KV block pool at start-up; scheduler knobs shape each step; execution and front-end knobs change the cost of a step.

The idea#

A vLLM server has a few dozen flags that change speed. They act in three places, at three times. This page sorts them by place, gives each real default with the line that sets it, and says what the knob trades. It does not give performance numbers: measure your own workload with Benchmarking vLLM: vllm bench serve, throughput, latency and startup.

  1. Memory, once at start-up. --gpu-memory-utilization, --kv-cache-memory-bytes, --kv-cache-dtype, --max-model-len and the size of the weights decide how many KV blocks exist. The block count does not change while the server runs. The engine core logs it as the maximum concurrency.
  2. The scheduler, every step. --max-num-seqs, --max-num-batched-tokens (the token budget), chunked prefill and the scheduling policy decide which requests share a step. They trade time to first token (TTFT) against inter-token latency (ITL) and throughput (Serving metrics: TTFT, TPOT, ITL, throughput, goodput).
  3. Execution and the front end. CUDA graphs, torch.compile, speculative decoding and async scheduling change how long a step takes. --api-server-count, the renderer threads and --stream-interval change the CPU work in the API server.

Most knobs have a default that the code computes. The dataclass default is often not the real default: for example, SchedulerConfig says 128 sequences, but vllm serve on an 80 GiB H100 uses 1024. The full flag list with real defaults is on the serve-flags reference; this page explains the mechanisms behind the most important ones.

An everyday comparison. Think of a restaurant. The size of the dining room is fixed when it opens (memory knobs). The head waiter decides each minute which guests to seat and how many dishes the kitchen starts (scheduler knobs). The kitchen equipment and the number of waiters at the door decide how fast one round goes (execution and front-end knobs). The comparison stops at the tables. In vLLM, one long conversation can take the space of many short ones. A guest can also lose the table (preemption) when the room is full.

The picture#

Figure: the three places where tuning knobs act. The values are the vllm serve defaults on one 80 GiB H100 (not an A100).
Open full size ↗

Read the panels from top to bottom. Panel 1 runs once, so its knobs need a restart. Panel 2 runs in the EngineCore process at every step. Panel 3 mixes the GPU workers (compile, graphs, drafts) and the API server process (amber).

In the code#

Step 1: the memory budget and the KV cache size#

At start-up, each worker measures how much memory is left for the KV cache. The page The KV cache manager: blocks, the block pool and memory sizing explains the block pool; Executors and workers: from the engine core to the GPUs explains the profile run. The worker first computes the requested memory, a share of the whole GPU, and refuses to start when less is free (vllm/v1/worker/utils.py:724-726, vllm/v1/worker/utils.py:761-770). After a dummy forward pass with max_num_batched_tokens tokens, it computes the rest:

        self.total_consumed = profile_result.total_consumed
        self.peak_activation_memory = (
            profile_result.transient_peak_headroom + cudagraph_memory_estimate_applied
        )
        self.cudagraph_memory_estimate = cudagraph_memory_estimate

        self.available_kv_cache_memory_bytes = (
            self.requested_memory
            - profile_result.non_kv_cache_memory
            - cudagraph_memory_estimate_applied
        )

non_kv_cache_memory is the memory consumed since the start snapshot (weights, buffers, non-torch memory) plus the transient peak of the activations (vllm/utils/mem_utils.py:312-321). The CUDA graph estimate is on by default (VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS=1, vllm/envs.py:2217-2219). So the KV cache gets:

available_kv = ceil(total × gpu_memory_utilization) − (consumed + peak_activation_headroom) − cudagraph_estimate

Four knobs change this result:

Step 2: max_model_len and the maximum-concurrency line#

After the KV cache is sized, the engine core stores the capacity and logs one line (vllm/v1/engine/core.py:342):

def update_kv_cache_capacity(
    vllm_config: VllmConfig, kv_cache_config: KVCacheConfig
) -> None:
    """Store and log the resolved KV cache capacity."""
    num_tokens, max_concurrency = get_kv_cache_capacity(vllm_config, kv_cache_config)
    vllm_config.cache_config.kv_cache_size_tokens = num_tokens
    vllm_config.cache_config.kv_cache_max_concurrency = max_concurrency
    max_model_len = vllm_config.model_config.max_model_len
    pools = _kv_cache_device_name(vllm_config)
    if vllm_config.attention_config.hisparse_config is not None:
        pools += " + host"
    logger.info_once(
        "%s KV cache size: %s tokens, "
        "Maximum concurrency for %s tokens per request: %.2fx",
        pools,
        f"{num_tokens:,}",
        f"{max_model_len:,}",
        max_concurrency,
    )

The concurrency is the number of blocks divided by the blocks that one request of max_model_len needs. For a model with one KV cache group, that is num_blocks / cdiv(max_model_len, block_size). With several groups, vLLM adds the blocks per request of all groups (vllm/v1/core/kv_cache_utils.py:1031-1041). The token count in the log is int(concurrency × max_model_len) (vllm/v1/core/kv_cache_utils.py:1074-1082). So "Maximum concurrency 3.41x" means: 3.41 requests of the full length fit at once. Shorter requests fit many more times.

--max-model-len does not change the block count. It changes the check at start-up: one request of that length must fit in the KV cache, minus one block for the null block (vllm/v1/core/kv_cache_planning.py:1572-1582, vllm/v1/core/kv_cache_planning.py:1590-1598). If it does not fit, start-up stops with an estimate of the largest length that fits (vllm/v1/core/kv_cache_planning.py:68-87). With --max-model-len auto (or -1), vLLM lowers the length to that estimate and logs "Auto-fit max_model_len: reduced from ..." (vllm/v1/core/kv_cache_planning.py:1584-1587, vllm/v1/core/kv_cache_planning.py:1393-1410). The default is the length from the model's config (vllm/config/model.py:2639-2653).

InteractiveKV cache sizing playground

This interactive visual needs JavaScript. Turn on JavaScript in your browser to use it. The text on this page explains the same idea.

Change any value; the results update at once. All memory values are per GPU. The profile parts (weights, activations, graph estimate) are your inputs: vLLM measures them at start-up. A simplified model: see the formulas below and the box after them.

The playground uses only these formulas from the code:

  1. requested = ceil(total × gpu_memory_utilization) (vllm/v1/worker/utils.py:724-726).
  2. available = requested − (consumed + peak_headroom) − cudagraph_estimate (vllm/v1/worker/gpu_worker.py:744-748, vllm/utils/mem_utils.py:312-321). With --kv-cache-memory-bytes: available = that value (vllm/v1/worker/gpu_worker.py:653-679).
  3. bytes_per_block = bytes_per_token × block_size, where bytes per token is the sum over the layers of num_kv_heads × (head_size + head_size_v) × dtype_bytes (vllm/v1/kv_cache_interface.py:571-579, vllm/v1/core/kv_cache_planning.py:587-593).
  4. num_blocks = available // bytes_per_block (vllm/v1/core/kv_cache_planning.py:1610-1611).
  5. Fit check: cdiv(max_model_len, block_size) × bytes_per_block ≤ available − bytes_per_block (vllm/v1/core/kv_cache_planning.py:1572-1582, vllm/v1/core/kv_cache_planning.py:68-87). When available − bytes_per_block ≤ 0, start-up stops earlier with "No available memory for the cache blocks." (vllm/v1/core/kv_cache_planning.py:56-64).
  6. max_concurrency = num_blocks / cdiv(max_model_len, block_size); logged tokens = int(max_concurrency × max_model_len) (vllm/v1/core/kv_cache_utils.py:1031-1047, vllm/v1/core/kv_cache_utils.py:1074-1082).
  7. "Use vllm serve defaults" applies the memory tiers, the throughput doubling and the two caps of Step 3 (vllm/engine/arg_utils.py:2958-2993, vllm/engine/arg_utils.py:3171-3175, vllm/engine/arg_utils.py:3206-3221).

Step 3: max_num_seqs and max_num_batched_tokens#

Both flags are None by default. create_engine_config fills them by the usage context and the memory of one GPU (usage context). For vllm serve (OPENAI_API_SERVER):

        # throughput, see PR #17885 for more details.
        # So here we do an extra device name check to prevent such regression.
        if device_memory >= 160 * GiB_bytes:
            # for GPUs like B200/B300 with >= 160GB memory, use the largest defaults
            default_max_num_batched_tokens = {
                UsageContext.LLM_CLASS: 16384,
                UsageContext.OPENAI_API_SERVER: 16384,
            }
            default_max_num_seqs = {
                UsageContext.LLM_CLASS: 1024,
                UsageContext.OPENAI_API_SERVER: 1024,
            }
        elif (
            device_memory >= 70 * GiB_bytes
            and "a100" not in device_name
            and not is_gb10
        ):
            # For GPUs like H100 and H200, use larger offline defaults.
            default_max_num_batched_tokens = {
                UsageContext.LLM_CLASS: 16384,
                UsageContext.OPENAI_API_SERVER: 8192,
            }
            default_max_num_seqs = {
                UsageContext.LLM_CLASS: 1024,
                UsageContext.OPENAI_API_SERVER: 1024,
            }
        else:
            # TODO(woosuk): Tune the default values for other hardware.
            default_max_num_batched_tokens = {
                UsageContext.LLM_CLASS: 8192,
                UsageContext.OPENAI_API_SERVER: 2048,
            }
            default_max_num_seqs = {
                UsageContext.LLM_CLASS: 256,
                UsageContext.OPENAI_API_SERVER: 256,
            }

Then these rules apply, only to values that you did not set yourself:

        # If throughput mode is set, double max_num_batched_tokens and max_num_seqs.
        if self.performance_mode == "throughput":
            if orig_max_num_batched_tokens is None:
                self.max_num_batched_tokens *= 2
            if orig_max_num_seqs is None:
                self.max_num_seqs *= 2

Values that you set are checked, not changed. max_num_seqs larger than the token budget raises ValueError at start-up (vllm/config/scheduler.py:331-336). When chunked prefill is on, SchedulerConfig logs the budget once: "Chunked prefill is enabled with max_num_batched_tokens=N." (vllm/config/scheduler.py:309-313).

What the two knobs change:

  • max_num_seqs caps the running requests per step (vllm/v1/core/sched/scheduler.py:979-981). It also sizes the per-request buffers of the model runner and the default CUDA graph capture sizes (Step 6). A larger value gives more throughput only when the KV cache can hold that many requests. --max-num-active-seqs lowers only the admission limit and keeps the buffers (vllm/config/scheduler.py:69-76).
  • max_num_batched_tokens caps the tokens per step. The scheduler gives running requests their tokens first, then admits waiting requests with the rest (vllm/v1/core/sched/scheduler.py:713-715, vllm/v1/core/sched/scheduler.py:956-957). A larger budget moves prompts through faster (lower TTFT under load) but makes the steps longer, so running requests see a higher ITL. It also sets the profile-run size (Step 1) and is part of the compile cache key, so a change compiles again (vllm/config/scheduler.py:271-284).

Step 4: chunked prefill and the long-prefill cap#

Chunked prefill is on for generative decoder models: the default comes from is_chunked_prefill_supported of the model, not from the dataclass (vllm/engine/arg_utils.py:3031-3040). A prompt that does not fit the rest of the budget gets a chunk, and the next steps continue it (Batching). Without chunked prefill, the budget must be at least max_model_len (vllm/config/scheduler.py:318-329).

A long prompt can still take the whole rest of a step. --long-prefill-token-threshold N caps the tokens that one prefill takes per step. The default 0 turns the cap off (vllm/config/scheduler.py:79-84):

        # `long_prefill_token_threshold` exists to stop a long prefill from
        # starving other requests of the token budget. When it is the only
        # request there is nobody to starve, so let it use the whole budget.
        num_running, num_waiting = self.get_request_counts()
        num_eligible_reqs = num_running + num_waiting
        long_prefill_token_threshold = (
            self.scheduler_config.long_prefill_token_threshold
            if num_eligible_reqs > 1
            else 0
        )
        if long_prefill_token_threshold > 0 and self.adaptive_long_prefill_threshold:
            # Floor the cap at a fair share of the input budget so it never
            # cuts a request below max_num_batched_tokens / num requests.
            long_prefill_token_threshold = max(
                long_prefill_token_threshold, input_budget // num_eligible_reqs
            )

The cap applies only when more than one request runs or waits (lines 679-683). With --long-prefill-token-threshold-adaptive, the cap is never below a fair share of the budget (lines 684-689). A smaller cap spreads a long prompt over more steps: its own TTFT grows, and the other requests get more room in each step.

Step 5: bytes per token: block size, prefix caching, KV dtype, quantization#

Step 6: compile, CUDA graphs and -O levels: start-up time against step time#

The default optimization level is O2 (vllm/config/vllm.py:434). O2 compiles the model with torch.compile and captures FULL and PIECEWISE CUDA graphs (vllm/config/vllm.py:131-143, vllm/config/vllm.py:310). torch.compile and CUDA graphs in vLLM explains both. For tuning, the trade is simple: compile and capture cost start-up time and some memory, and save launch overhead in every step (CUDA graphs).

  • -O0 or --enforce-eager: no compile, no graphs. --enforce-eager sets the compile mode and the graph mode to NONE (vllm/config/vllm.py:1745-1747). Start-up is fastest, and the graph memory goes to the KV cache (Step 1). Every step pays the full kernel launch cost.
  • -O1: compile with PIECEWISE graphs only.
  • Capture sizes. When you set no sizes, the largest captured batch is min(max_num_seqs × decode_query_len × 2, 512) (1024 on data-center Blackwell), then at most the token budget (vllm/config/vllm.py:2439-2449, vllm/config/vllm.py:2530-2531). The sizes are 1, 2, 4, then steps of 8 up to 248, then steps of 16 (vllm/config/vllm.py:2557-2570). A decode batch larger than the largest size runs without a graph.
  • --performance-mode interactivity captures every size from 1 to 32 instead of 1, 2, 4, 8, 16, 24, 32 (vllm/config/vllm.py:2552-2556). Small batches then need less padding, and start-up captures more graphs.

The warm start matters as much as the level: the compile cache under VLLM_CACHE_ROOT makes the second start-up much shorter (Tour: Start-up of vllm serve, Troubleshooting vLLM: symptom, cause and fix).

Step 7: async scheduling and speculative decoding: fewer idle gaps, fewer steps#

Async scheduling is on by default for generative models when the executor supports it (vllm/config/vllm.py:1664-1698). The engine core schedules step N+1 while the GPU runs step N (Async scheduling: overlap the CPU and the GPU). Turn it off (--no-async-scheduling) only to compare.

Speculative decoding (--speculative-config, off by default, vllm/config/vllm.py:383) proposes k draft tokens and checks them in one target step (Speculative decoding; Speculative decoding in vLLM serving). It saves steps only when drafts are accepted. The vLLM docs say that it reduces "inter-token latency under medium-to-low QPS (queries per second), memory-bound workloads" (docs/features/speculative_decoding/README.md:3). Their table rates the gain per method for low and high QPS (docs/features/speculative_decoding/README.md:30-39). Watch the acceptance with these PromQL formulas from the code (vllm/v1/spec_decode/metrics.py:178-196):

acceptance rate      = rate(vllm:spec_decode_num_accepted_tokens_total[5m]) / rate(vllm:spec_decode_num_draft_tokens_total[5m])
mean acceptance len  = 1 + rate(vllm:spec_decode_num_accepted_tokens_total[5m]) / rate(vllm:spec_decode_num_drafts_total[5m])

The log line "SpecDecoding metrics: Mean acceptance length: ..." shows the same values every log interval (vllm/v1/spec_decode/metrics.py:107-136). A mean acceptance length near 1 means that the drafts cost work and save no step.

Step 8: parallelism, API servers, streaming and priorities#

Deep dive#

The sections below give the full detail. Open them one at a time, or use Open all deep dives at the top of the page.

Deep dive

Worked example: Llama-3.1-8B on one 80 GiB H100, five changes

Start: an 80 GiB GPU and gpu_memory_utilization 0.92. The profile finds 15 GiB weights, 1 GiB other consumed memory, a 2.5 GiB activation peak and a 0.5 GiB graph estimate. These four memory values are assumptions (the same as in the figure of The KV cache manager: blocks, the block pool and memory sizing); vLLM measures them on your GPU. The KV values are from the model's Hugging Face config (inferred, not in the repository). One token needs 32 layers × 8 KV heads × (128 + 128) × 2 bytes = 128 KiB. With block_size 16, one block is 2 MiB.

SettingAvailable KVBlocksLog: tokens, concurrencyRequests of 2,500 tokens that fit
Defaults, max_model_len 131,07254.60 GiB27,955447,280; 3.41x178
--kv-cache-dtype fp854.60 GiB55,910894,560; 6.82x356
--max-model-len 3276854.60 GiB27,955447,280; 13.65x178
--gpu-memory-utilization 0.9557.00 GiB29,184466,944; 3.56x185
--enforce-eager (graph estimate 0)55.10 GiB28,211451,376; 3.44x179
  1. Requested: ceil(80 GiB × 0.92) = 73.6 GiB. Available: 73.6 − (16.0 + 2.5) − 0.5 = 54.6 GiB (vllm/v1/worker/gpu_worker.py:744-748).
  2. Blocks: 54.6 GiB / 2 MiB = 27,955 (vllm/v1/core/kv_cache_planning.py:1610-1611).
  3. Concurrency: 131,072 / 16 = 8,192 blocks per full request; 27,955 / 8,192 = 3.41 (vllm/v1/core/kv_cache_utils.py:1031-1047).
  4. Requests that fit: one 2,500-token request uses cdiv(2500, 16) = 157 blocks; (27,955 − 1) / 157 = 178 (the null block is not usable).

Three lessons follow. First, --max-model-len changes the concurrency in the log, but not the memory: the same 178 average requests fit. Second, max_num_seqs 1024 does not bind here: the KV cache stops this workload at about 178 running requests, so a larger max_num_seqs changes nothing. Third, the KV dtype doubles the capacity; the other knobs give a few percent.

In one step at that load, 178 decode tokens use 178 of the 8,192 budget tokens. About 8,014 tokens are left for prefill chunks, so a new 2,500-token prompt fits into one step (derived from the order in vllm/v1/core/sched/scheduler.py:713-715).

Deep dive

Default capture sizes, counted

On one 80 GiB H100 with the defaults (max_num_seqs 1024, budget 8,192, no speculative decoding):

  1. Largest size: min(1024 × 1 × 2, 512) = 512, then min(8192, 512) = 512 (vllm/config/vllm.py:2442-2450, vllm/config/vllm.py:2530-2531).
  2. Sizes: 1, 2, 4 (3 sizes); 8, 16, ..., 248 (31 sizes); 256, 272, ..., 512 (17 sizes) = 51 sizes (vllm/config/vllm.py:2557-2570).
  3. With --performance-mode interactivity: 1 to 32 (32 sizes) plus 40, ..., 248 (27 sizes) plus the 17 large sizes = 76 sizes (derived from vllm/config/vllm.py:2552-2570).
  4. On an A100 with 2,048 tokens and 256 sequences: min(256 × 2, 512) = 512, so again 51 sizes.

Each size is captured for each graph mode that applies, so more sizes mean a longer capture at start-up and more graph memory. A decode batch of 600 requests is above 512 and runs without a CUDA graph (torch.compile and CUDA graphs in vLLM).

Deep dive

What --performance-mode really changes

The docstring of the field says that interactivity uses "fine-grained CUDA graphs, latency-oriented kernels" and throughput uses "larger CUDA graphs, more aggressive batching, throughput-oriented kernels" (vllm/config/vllm.py:440-445). At this commit, the code reads the field in two places only: the doubling of the batch defaults (vllm/engine/arg_utils.py:3171-3175) and the interactivity capture sizes (vllm/config/vllm.py:2552-2556). A search of vllm/ finds no other reader of performance_mode (a git grep at the pinned commit; open question for later commits). The doubling applies only to values that you did not set. A mode other than balanced is logged: "Performance mode set to ..." (vllm/config/vllm.py:1518-1519).

Deep dive

Flags and environment variables of this page

Flag or variableReal default for vllm serveRead at
--gpu-memory-utilization0.92vllm/config/cache.py:212
--kv-cache-memory-bytesnone (measure)vllm/config/cache.py:371
--max-model-lenfrom the model config; auto fits to memoryvllm/config/model.py:2639-2653
--max-num-seqs, --max-num-batched-tokensby GPU memory: 1024 / 16384 (≥ 160 GiB), 1024 / 8192 (≥ 70 GiB, not A100/GB10), 256 / 2048vllm/engine/arg_utils.py:2958-2993
--performance-modebalancedvllm/config/vllm.py:440
--long-prefill-token-threshold0 (off)vllm/config/scheduler.py:79
--scheduling-policyfcfsvllm/config/scheduler.py:159
--stream-interval1vllm/config/scheduler.py:216
--async-schedulingon unless incompatiblevllm/config/vllm.py:1664-1698
-O / --enforce-eagerO2 / offvllm/config/vllm.py:434, vllm/config/model.py:276
--renderer-num-workers1vllm/config/model.py:412
--api-server-countDP size (internal LB), local DP size (hybrid LB), else 1vllm/entrypoints/cli/serve.py:113-130
VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS1vllm/envs.py:2217-2219

The complete list is generated on vllm serve flags and Environment variables; the config classes are on Config classes.

Deep dive

A tuning method (advice)

This method is advice, not code. Each step names the signal that the code gives.

  1. Read the start-up log: "Available KV cache memory", "GPU KV cache size: N tokens, Maximum concurrency ..." (vllm/v1/worker/gpu_worker.py:763-766, vllm/v1/core/kv_cache_utils.py:1101-1108). If the concurrency for your real request length is low, fix memory first (Step 1, Step 5).
  2. Run vllm bench serve with your request lengths at several request rates (Benchmarking vLLM: vllm bench serve, throughput, latency and startup).
  3. Watch vllm:num_requests_waiting, vllm:kv_cache_usage_perc and rate(vllm:num_preemptions_total[1m]) during the run (Metrics and observability: how to watch a vLLM server). Waiting requests with low KV usage point to the token budget or max_num_seqs; high KV usage with preemptions points to memory.
  4. Change one knob at a time, and compare the goodput at the same SLO.

What can go wrong#

The table maps a symptom to the knob to try first. Each row is advice; the cause column cites the code.

SymptomLikely cause (code)Knob to try
Start-up: "No available memory for the cache blocks"weights + activation peak + graphs ≥ requested memory (vllm/v1/core/kv_cache_planning.py:56-64)raise --gpu-memory-utilization; lower --max-num-batched-tokens; --enforce-eager; TP or quantization
Start-up: "To serve at least one request with the model's max seq len ..."one full-length request does not fit (vllm/v1/core/kv_cache_planning.py:68-87)--max-model-len (or auto); --kv-cache-dtype fp8
vllm:num_preemptions rate above 0running requests grow past the free blocks (The V1 scheduler: one token budget, no phases)more KV memory (Step 1, Step 5); lower --max-num-seqs
High TTFT, many waiting requests, KV usage lowthe token budget or max_num_seqs limits admission (vllm/v1/core/sched/scheduler.py:979-981)raise --max-num-batched-tokens, then --max-num-seqs; more replicas (-dp)
High ITL spikes when long prompts arrivelarge prefill chunks share the step with decodes (vllm/v1/core/sched/scheduler.py:674-689)lower --max-num-batched-tokens; set --long-prefill-token-threshold
Low GPU use, API server process at 100 % CPUrender, detokenize and JSON share one event loop (The OpenAI-compatible server: launch modes, app, serving classes and streaming)--api-server-count; --stream-interval; --renderer-num-workers
Slow start-upcompile and graph capture (torch.compile and CUDA graphs in vLLM)keep the compile cache; -O0 or --enforce-eager for development
Speculative decoding makes ITL worselow acceptance (vllm/v1/spec_decode/metrics.py:107-115)fewer draft tokens, another method, or off
Changed --max-num-batched-tokens, start-up compiles againthe value is part of the compile hash (vllm/config/scheduler.py:271-284)expected; keep the value stable in production

Why it matters. Most slow servers have one binding limit: KV memory, the token budget, the batch width or the CPU of the front end. Know the code path of each knob. Then the start-up log and four metrics show the limit, and you change the right knob.

Check yourself#

  1. 1
    Which two values set max_num_seqs and max_num_batched_tokens when you start vllm serve without these flags?
    easy
    Show answer

    The usage context (OPENAI_API_SERVER for vllm serve) and the memory of one GPU, plus a name check for A100 and GB10 (vllm/engine/arg_utils.py:2958-2993). --performance-mode throughput then doubles both (vllm/engine/arg_utils.py:3171-3175).

  2. 2
    The log says "Maximum concurrency for 131,072 tokens per request: 3.41x". Can the server run only 3 requests at once?
    easy
    Show answer

    No. The value is blocks divided by the blocks of one request of max_model_len (vllm/v1/core/kv_cache_utils.py:1031-1047). Shorter requests use fewer blocks, so many more fit; the running count is limited by max_num_seqs and the real request lengths.

  3. 3
    You set --max-num-seqs 2048 --max-num-batched-tokens 1024. What happens?
    medium
    Show answer

    Start-up stops with ValueError: the token budget must be at least max_num_seqs (vllm/config/scheduler.py:331-335). vLLM caps max_num_seqs only when it fills the default itself (vllm/engine/arg_utils.py:3219-3221).

  4. 4
    Why can a larger --max-num-batched-tokens lower the KV cache size?
    medium
    Show answer

    The profile run uses max_num_batched_tokens tokens (vllm/v1/worker/gpu/model_runner.py:231, vllm/v1/worker/gpu/model_runner.py:1022-1027). A larger run has a larger activation peak, and the peak is subtracted from the requested memory (vllm/utils/mem_utils.py:318-321, vllm/v1/worker/gpu_worker.py:744-748).

  5. 5
    In the worked example, 178 requests of 2,500 tokens fit, and max_num_seqs is 1024. You raise max_num_seqs to 2048. What changes, and what does not?
    hard
    Show answer

    The KV cache still holds only about 178 such requests: the block count comes from memory, not from max_num_seqs (deep dive "Worked example"). The scheduler admits a request only when its tokens fit in free blocks (vllm/v1/core/kv_cache_manager.py:522-539), so the running count stays near 178. Three things do change. The per-request buffers of the runner grow. The default capture size stays at 512, because min(2048 × 2, 512) = 512 (vllm/config/vllm.py:2442-2450). The compile hash changes, so start-up compiles again (vllm/config/scheduler.py:271-284).