The lever set on this wheel is narrower than upstream-Linux because TurboQuant KV, FlashInfer, and a few Genesis patches are unavailable. What remains:
-
MTP num_speculative_tokens. The widely-repeated "n=3 is the sweet spot" conventional wisdom is for short prompts. On long-prompt dense code (~100 KB / ~24 k tokens of Python source, our bench harness) the acceptance curve shifts later:
n=3 / 4 / 5 / 6 / 7 / 8 → 53.4 / 58.3 / 62.8 / 64.5 / 61.5 / 58.0 tok/sn=6 is the long-prompt peak, dropping a cliff at n=7. Always re-sweep on a representative prompt for a new workload, don't trust a single fixed number.
On RTX 5090 (Blackwell zip, vLLM 0.20.0+cu132.devnen.1) the picture shifts. The numbers below are from the AutoRound INT4 5090 snapshots that were retired in v1.3.7 (NVFP4 is now the only 5090 path); they are kept here as the reference MTP sweep on Blackwell:
500W power cap (v1.2.0-v1.2.2 baseline, three snapshots shipped): ctx 120k MTP n=6 (rtx5090_speed) -> short 130.9 / 24k decode ~89 tok/s [retired in v1.2.3] ctx 240k MTP n=6 (rtx5090) -> short 124.9 / 24k decode 89.3 tok/s [retired in v1.3.7] ctx 280k MTP n=3 (rtx5090_max) -> short 138.0 / 24k decode 81.0 tok/s [retired in v1.3.7] 575W power cap (v1.2.3 re-bench, --no-enable-prefix-caching, two snapshots): ctx 240k MTP n=6 (rtx5090) -> short 158.1 / 24k decode 107.8 / 24k prefill 3,100-3,300 ctx 280k MTP n=3 (rtx5090_max) -> short 154.3 / 24k decode 90.2 / 24k prefill 3,100-3,300 v1.2.5 (--enable-prefix-caching ON, vLLM PR #25752 / Mamba2 APC in wheel): ctx 240k MTP n=6 (rtx5090) -> short ~125 / KV pool 94,656 (+18%) prefill 12k 686 -> 2147 tok/s (3.1x) prefill 16k 559 -> 2034 tok/s (3.6x) prefill 24k+ timeout -> 2-4k tok/s decode after 2x 24k hits: stableThe AutoRound 5090 path was dropped in v1.3.7 because long-prompt prefill on consumer Blackwell is capped at ~170W (see
SM120_GDN_CEILING.md); the NVFP4 path routes FFN GEMMs through FlashInfer's sm_120 native FP4 tensor cores and reaches ~5,300 tok/s @ 47k prompt at full TDP.Snapshot history. v1.2.3 retired
rtx5090_speed. The 575W re-bench showed it tiedrtx5090on short decode (158 vs 158) but was slower on long decode (103 vs 108) AND had a reproducible long-prompt prefill regression (~343 tok/s vs ~3,200 for the other two). Same MTP n=6 + chunked-prefill flags, cause not yet root-caused, but the practical conclusion was that it offered no inference advantage and a real prefill loss. v1.3.7 then retiredrtx5090andrtx5090_maxthemselves in favor of the NVFP4 snapshots.Three Blackwell observations from the 575W re-bench:
- Power cap matters for decode on Blackwell, 500W → 575W adds ~20-30% short decode and ~10-20% long decode. (On Ampere the equivalent 250→350W bump moved only prefill, never decode, because decode was bandwidth-bound. Blackwell evidently has compute headroom even at batch=1.) If your PSU and cooling can handle it, the cap pays out.
- MTP n=3 vs n=6 short-prompt advantage flips with power. At 500W n=3 edged out n=6 (138 vs 131); at 575W n=6 wins (158 vs 154). The "n=3 sweet spot for short prompts" claim is power-cap-dependent.
- Bigger ctx does NOT slow decode at fixed MTP on Blackwell. The 240k snapshot lands the headline 158 tok/s short-decode number. The 280k snapshot is close behind (154) only because its MTP is n=3, not n=6.
-
Power cap. 250 W → 350 W: prefill +16 %, decode unchanged (decode is memory-bandwidth-bound at batch=1 / max-num-seqs=1). If you want decode speed, more power doesn't help; if you want short-prompt TTFT, it does.
-
Bigger ctx slows decode at fixed MTP (3090 / Ampere only). Measured at MTP n=6: ctx=90 k → 64.5, ctx=112 k → 60.4 (–6 %). Likely KV-pool memory bandwidth during attention. If a snapshot doesn't need the full ctx headroom, don't pay for it, split into a "speed" snapshot (smaller ctx) and a "max-ctx" snapshot. We have both.
This does NOT hold on Blackwell. The 5090 re-bench at 575W showed the retired AutoRound
rtx5090(240k, MTP n=6) hit 158.1 tok/s short decode whilertx5090_max(280k, MTP n=3) hit 154.3 tok/s. The 4 % gap is the MTP n drop, not the ctx ceiling. Bigger ctx is effectively free on Blackwell at fixed MTP n. The shipping 5090 path is now NVFP4 (rtx5090_nvfp4, 200k); seeBLACKWELL.mdfor the current table. -
Cudagraphs on.
--enforce-eagercosts ~55 % on Linux; untested on Windows but almost certainly similar. Don't disable. -
Attention backend.
TRITON_ATTNonly, FlashInfer JIT is broken on Windows because ninja trips MAX_PATH inside flashinfer cache dirs. Pass--attention-backend=TRITON_ATTNas a CLI flag (the env var alone is ignored on 0.19.0). -
Small-boost env vars:
VLLM_ENABLE_CUDAGRAPH_GC=1 # no downside VLLM_MARLIN_USE_ATOMIC_ADD=1 # no downside VLLM_USE_FLASHINFER_SAMPLER=1 # ONLY if MSVC 2022 + ninja are presentVLLM_USE_FLASHINFER_SAMPLER=1JIT-compiles a sampling module on the firstprofile_run, which shells out to ninja andcl.exe. Without MSVC, EngineCore dies withFileNotFoundErrorinrun_ninjabefore the server boots. The shipped snapshots auto-detect MSVC + ninja and set this to0(PyTorch fallback) when either is missing. ninja ships in the launcher zip since v0.1.17, so installing the free Visual Studio 2022 Build Tools ("Desktop development with C++" workload) is enough to flip the snapshot probe to=1automatically. See the README "Optional: install MSVC 2022" section.
The rtx5090_nvfp4 snapshot runs FlashInfer's fp4_gemm autotuner
once per unique GEMM shape encountered during CUDA-graph capture and
warmup. With MTP n=6, chunked prefill, max_num_batched_tokens=4128,
ctx 200 k, that's a lot of shapes. Expected wall-clock to first
Application startup complete:
| State | Cold-boot time |
|---|---|
| Warm cache (subsequent boots) | 3–8 min |
| Cold FlashInfer cache only | ~9 min |
After wipe_caches.py (all four caches wiped) |
11–25 min |
You will see many [AutoTuner]: Tuning fp4_gemm: 0/13 → 13/13 cycles.
That's expected, not a loop, each cycle is a different shape. The
[Autotuner]: Skipped 6 unsupported tactic(s) for fp4_gemm line is
also normal: those six are TMA-WS kernels gated on compute_120f,
not shipped in the bundled cubin pack. The validated 5,300 tok/s
prefill is achieved without them. See
SM120_GDN_CEILING.md for the research.
If prefill collapses to ~750 tok/s on a 47 k NVFP4 prompt (SM=100 %, mem-BW≈0 %, ~200 W during load), the vLLM AOT compile cache was poisoned by an earlier polluted-env boot. Recovery is one command:
python windows_tools\wipe_caches.pyThen relaunch the snapshot. v1.3.2's clean_cuda_env() and
preflight_sm120a_or_die() prevent the original poisoning, but a
cache populated under a pre-v1.3.2 boot can still be sticky.
-
--max-num-batched-tokenspeak is around 4128, non-monotonic. Lowering to 2048 shrinks the available KV from 4.74 GiB → 2.99 GiB; raising to 8192 shrinks it to 2.64 GiB. Leave at 4128. (The chunked-prefill scratch buffer scales weirdly with this number on this wheel.) -
--enable-chunked-prefillis on in every shipped snapshot. -
--enable-prefix-cachingis ON in every shipped snapshot (since v1.2.5, 2026-05-06). v1.2.2 had it off as a defensive workaround for vLLM issue #17140, the stepwise decode regression on Qwen3-Next where short-prompt decode dropped 30 % after one 24 k-token request and a further 30 % after a second (130 → 90 → 40 tok/s, stuck) because SSM state was not tracked across prefix-cached KV blocks. That bug is fixed upstream by vLLM PR #25752 (Mamba2 Automatic Prefix Caching, merged 2025-10-04). Both shipped wheels include the fix in their source tree, and with prefix caching enabled vLLM auto-setsmamba_cache_mode='align'forQwen3_5ForConditionalGenerationatvllm/model_executor/models/config.py:367, so SSM state aligns with cache-block boundaries.What we measured on the 5090 (
start_5090, ctx 240k, MTP n=6,max_num_batched_tokens=4128) re-bench at 575 W:Prompt size OFF (v1.2.2-v1.2.4) ON (v1.2.5) Speedup 4 k 3744 tok/s 3375 tok/s similar 8 k 3019 tok/s 3497 tok/s +16 % 12 k 686 tok/s 2147 tok/s 3.1x 16 k 559 tok/s 2034 tok/s 3.6x 20 k timeout 2473 tok/s unblocks 24 k timeout 4333 tok/s unblocks 32 k timeout 2479 tok/s unblocks 48 k timeout 2444 tok/s unblocks Decode regression test (
windows_tools/repro_17140.py, the exact protocol from the v1.2.2 bug write-up: 3 short → one 24k hit → 3 short → one 24k hit → 3 short):Phase tok/s drift baseline (3 short) 119.3 , after 1st 24k hit 122.4 +2.6 % after 2nd 24k hit 122.0 +2.3 % No stepwise drop. The old 130 → 90 → 40 pattern does not reproduce on the 0.20.0 wheel with prefix caching on. KV pool also gains ~18 % headroom (94,656 vs 79,968 tokens at the same ctx=240k). Warm-prefix TTFT on a 24 k re-hit drops from ~42 s cold to ~1.6 s, useful for agents that re-send long context.
Why the 3090 snapshots got the same flip without a separate hardware test: the
vllm-0.19.0+devnen.1source tree the Ampere/Ada wheel is built from contains the same Mamba2 APC code (alignment_tokens,MambaSpec, theconfig.py:367auto-default), verified via thedevnen/vllm-windowsGitHub API at tagv0.19.0-devnen.1. If the regression resurfaces on a real 3090 / 4090, please open an issue with therepro_17140.pyoutput.Reproducing the bench yourself:
python windows_tools\bench_prefill_quick.py ` --sizes 4096,8000,12000,16000,20000,24000,32000,48000 --timeout 60 python windows_tools\repro_17140.py
-
Just raise
--max-model-len. On a freshstart_72tpssnapshot the KV pool already holds ~75 k tokens of physical KV but--max-model-len=32000caps it at 32 k. Bumping to 121 k costs nothing (measured: prefill 845 vs 835 tok/s, decode within noise on the ~25 k-token bench prompt). The 32 k conservatism is a copy-from-Linux artifact, not a hardware limit.Easiest path: clone an existing snapshot and bump the context. Press
eon the dashboard, Duplicate the closest match (e.g.gpu0_50korstart_speed), edit the Context field, Save. The launcher rewrites bothconfigs.yamlandstart_<id>.pyfor you. If you push past what fits, vLLM raises a clearValueErrorat boot before any model weights load and prints the safe ceiling (see the OOM oracle below). Nothing gets corrupted, just relaunch with a smaller value. Seedocs/SNAPSHOTS.mdfor the full editor reference. -
GPU1 mem-util ceiling is 0.948 (display-free). When GPU0 is idle, vLLM sees free=22.76/24 GiB on GPU1 after CUDA context init reserves ~1.5 GiB. 0.948 passes; 0.95 trips by ~40 MiB. The 0.92 → 0.94 → 0.948 progression: each step adds ~0.2 GiB KV / ~6.4 k tokens of ctx ceiling. GPU0 with display: 0.948 is also reachable if you boot-quiet (close heavy GPU apps before launching, reopen after
Application startup complete). If you can't boot-quiet, stay at 0.92 max, that's thestart_gpu0_50kpath. -
Use vLLM's auto-profiler as a context oracle. Two ways:
a. OOM probe. Set
--max-model-len=200000(deliberately too high) and read the engine'sValueError: To serve at least one request with the models's max seq len (200000), (X GiB KV cache is needed, which is larger than the available KV cache memory (Y GiB). Based on the available memory, the estimated maximum model length is N. ...That's the exact ceiling for the current config. Push max-model-len to ~99 % of N. Wrapped as a tool:
python windows_tools\probe_max_ctx.py. Source:vllm/v1/core/kv_cache_utils.py:641.b. Headroom from a successful boot. When a snapshot boots cleanly, vLLM logs (
vllm/v1/core/kv_cache_utils.py:1319 / :1325):INFO ... GPU KV cache size: N tokens INFO ... Maximum concurrency for X tokens per request: Y.YYxYis the multiplier of unused KV. If a 90 k snapshot reports concurrency 1.5x, you have ~50 % headroom and could push ctx to ~135 k (X * Y) without re-probing. Trust theMaximum concurrencyline, not theGPU KV cache size(the latter is a derived ceiling on this wheel, not the physical pool). -
PP=2 when you need >121 k. ~2× the KV pool but kills MTP, capping decode at ~43 tok/s. Use
start_pp2_160kfor this case only. -
--kv-cache-dtype=fp8_e4m3always. fp8_e5m2 is rejected by TRITON_ATTN. -
--max-num-seqs=1always, single-user only.
Quick reference for the two oracle log lines (full discussion in Context
item 3 above). After a successful boot, in logs\vllm_server.<port>.log:
INFO ... [kv_cache_utils.py:1319] GPU KV cache size: N tokens
INFO ... [kv_cache_utils.py:1325] Maximum concurrency for X tokens per request: Y.YYx
safe_max_ctx ≈ X × Y. The GPU KV cache size: N tokens line is not
the physical pool on this wheel, it's a derived ceiling. Trust the
Maximum concurrency line.
When ctx is too high, vLLM raises the estimated maximum model length is N
ValueError before any weights load. Read N, set --max-model-len just
under, re-launch. The CRUD editor (press e on the dashboard) is the
fastest way to apply the new value.
windows_tools\bench_summarize.py runs a ~100 KB / ~24 k-token Python
source-summary prompt against any port and appends one TSV row per run
to runs.tsv.
Schema:
label kv_pool_GiB ctx mtp_n prefill_tok_s decode_tok_s ttft_s wall_tok_s prompt_tokens completion_tokens notes
Convention: cold rows labeled <config>, warm rerun rows <config>_run2,
<config>_run3. Cold runs hit fresh prefix cache (TTFT ~25 s on the
~25 k-token prompt); warm runs report TTFT ~4 s / prefill ~5 900 tok/s
. That warm
prefill is bogus for cross-config comparison. Decode TPS is meaningful
on warm runs and useful for run-to-run noise.
windows_tools\tune_restart.py --port 5001 kills any orphan EngineCore /
APIServer / ZMQ listener tied to that port (regex-parsing the log) and
relaunches the snapshot. Use this between sweeps so port 5001 isn't
held by yesterday's process.