Prefill budget: tokens per engine step

Result. A bigger budget is not a faster engine. On 2x DGX Spark (GB10, TP=2), doubling it from 4,096 to 8,192 made prefill 64% faster and decode 8.2% slower on a dense model; doubling 8,192 to 16,384 on an MoE made prefill itself 24% slower. Panel defaults on the pair are 2,048–8,192.

What one step holds

max_num_batched_tokens = the token budget of one engine step (one forward pass). How vLLM V1 fills it (general vLLM behaviour, source-read):

  1. Decode first. Each seated request in decode takes 1 token, or 1 + K with K speculative tokens. 8 seats at K=4 = 40 tokens.
  2. Prefill fills the rest. Waiting prompts are cut into chunks that fit the remaining budget (chunked prefill). A 32k prompt at a 8,192 budget takes 4+ steps.
  3. One pass, one clock. Decode and the prefill chunk run in the same pass, so decode never stops. But every stream’s next token waits for the whole step, and a step lasts as long as its chunk takes.

So the budget trades TTFT (fewer, bigger chunks per prompt) against inter-token latency for everyone already decoding (longer steps).

long_prefill_token_threshold

Caps the tokens a single prompt may take in one step, so a giant prompt cannot claim the whole budget and several prompts prefill side by side (vLLM; 0 = off). The panel refuses a threshold above the context length. SGLang has no per-request cap; its budget bounds every chunk. Not measured: every perf-log minute recorded so far ran with it off.

Measured A/Bs

Both on 2x DGX Spark, vLLM, everything else held, engine argv verified at launch.

Prefill budget A/B on 2x DGX Spark. Dense 27B with DFlash2: 4,096 gives 654 prefill tok/s and 201.5 decode tok/s, 8,192 gives 1,071 and 185.0. Flash-Next MoE: 8,192 gives 3,073 prefill tok/s and 202.3 decode, 16,384 gives 2,348 and 197.4 Prefill budget A/B · 2x DGX Spark · vLLM Prefill tok/s (0.08 px each) Decode tok/s (0.9 px each) Dense 27B + DFlash2, 8 streams 4,096 654 201.5 8,192 1,071 +64% 185.0 −8.2% Flash-Next MoE, 16 × 32k prompts / 100 decodes 8,192 3,073 202.3 16,384 2,348 −24% 197.4 Different models, prompts and scales; compare the two rows inside a group. 197.4 = mean of 193.6 and 201.2.
Dense 27B: 2026-10-02/03, K=4, 4,096 = mean of two generations. MoE: 2026-09-30, 8 seats, K=4.
RunBudgetPrefillDecodeOther
Dense 27B + DFlash2, 4 streams4,096 → 8,192—128.9 → 121.3 (−5.9%)error bar 0.9%
Dense 27B + DFlash2, 8 streams4,096 → 8,192654 → 1,071 (+64%)201.5 → 185.0 (−8.2%)drafter acceptance 0.577 → 0.530; error bar 1.4%
Flash-Next MoE, pure prefill8,192 → 16,3843,073 → 2,348 (−23.6%)flatsteps ~3 s → ~7 s; one box down to ~0.8 GiB free
Flash-Next MoE, production traffic2,048 / 4,096 / 8,192—51.4 and 117.0 / 135.6 / 112.0 gen tok/sgenerations 101–102 / 99 (889 requests) / 97; not a controlled A/B

SGLang: the budget is also a freeze

SGLang on the pair runs prefill first: when a prefill batch is ready it runs alone (prefill_decode_interval 0, mixed chunks off), and every seated stream emits nothing until it ends. With first-come-first-served admission, an in-flight chunk uses the whole budget, so nothing else is admitted until the prompt’s last chunk.

SGLang decode freeze per prefill chunk on 2x DGX Spark: 2,048 tokens about 1.0 s measured, 8,192 tokens 3.4 s measured, 4,096 about 1.7 to 2.0 s and 16,384 about 6.8 to 8.0 s computed at 0.415 to 0.49 ms per token All decode frozen per prefill chunk, s · SGLang · 2x DGX Spark (55 px per s) 2,048-token chunk ~1.0 measured 2026-10-16 4,096 1.7–2.0 computed 8,192 3.4 measured 2026-10-03 16,384 6.8–8.0 computed Computed = chunk × 0.415 ms/token (the 8,192 fit) to 0.49 ms/token (the 2,048 fit). Bar at the midpoint. A cold 131k prompt at 8,192 = 16 chunks ≈ 55 s with all 30 streams frozen.
Dense 27B + DFlash2, TP=2, from engine logs. Prefill cost per token: 415 µs (2026-10-03), ~490 µs (2026-10-16).

What a prompt costs on the pair

Time to first token, SGLang, uncached prompts, 14–15 other requests running, 2026-10-03:

Prompt~8k~16k~24k~48k
TTFT, s2.874.827.2215.81

FlashInfer prefill for the linear-attention layers; Triton was 5–8% slower. Single run each.

Defaults in the panel (2026-10-24)

ModelEngineBudgetBasis
Qwen3.8 Flash-Next MoE, 2x DGX SparkvLLM4,096production generations above
Qwen3.8 Flash-Next MoE, 2x DGX SparkSGLang4,096carried from vLLM; not swept on SGLang
Qwen3.8-27B dense + DFlash2, 2x DGX SparkSGLang2,048the day-long run
DeepSeek-V4-Flash NVFP4, 2x DGX SparkvLLM8,192recipe value; not swept
Qwen3.8-27B NVFP4, RTX 5090 nodevLLM2,048vLLM’s own default for a card under 70 GiB; no flag passed

On this hardware the budget that wins is the smallest one that keeps prefill from starving: decode is what most of the traffic waits on. Related: concurrency, context length, prefix caching.

Questions? Let's Chat :)

vllm settings