Skip to content

Commit 8027098

Browse files
authored
Merge branch 'main' into p-less-decoding
2 parents b0862d5 + 4085ff7 commit 8027098

56 files changed

Lines changed: 1184 additions & 707 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.buildkite/lm-eval-harness/configs/NVIDIA-Nemotron-3-Nano-30B-A3B-FP8.yaml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ tasks:
66
value: 0.7142
77
- name: "exact_match,flexible-extract"
88
value: 0.4579
9-
env_vars:
10-
VLLM_USE_FLASHINFER_MOE_FP8: "1"
11-
VLLM_FLASHINFER_MOE_BACKEND: "throughput"
9+
moe_backend: "flashinfer_cutlass"
1210
limit: 1319
1311
num_fewshot: 5
1412
max_model_len: 262144

.buildkite/lm-eval-harness/test_lm_eval_correctness.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ def launch_lm_eval(eval_config, tp_size):
6868
if current_platform.is_rocm() and "Nemotron-3" in eval_config["model_name"]:
6969
model_args += "attention_backend=TRITON_ATTN"
7070

71+
moe_backend = eval_config.get("moe_backend", None)
72+
if moe_backend is not None:
73+
model_args += f"moe_backend={moe_backend},"
74+
7175
env_vars = eval_config.get("env_vars", None)
7276
with scoped_env_vars(env_vars):
7377
results = lm_eval.simple_evaluate(

.buildkite/test_areas/disaggregated.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,20 @@ steps:
6161
- bash /vllm-workspace/.buildkite/scripts/install-kv-connectors.sh
6262
- HYBRID_SSM=1 bash v1/kv_connector/nixl_integration/config_sweep_accuracy_test.sh
6363

64+
- label: Hybrid SSM NixlConnector PD prefix cache test (2 GPUs)
65+
key: hybrid-ssm-nixlconnector-pd-prefix-cache-2-gpus
66+
timeout_in_minutes: 25
67+
working_dir: "/vllm-workspace/tests"
68+
num_devices: 2
69+
source_file_dependencies:
70+
- vllm/distributed/kv_transfer/kv_connector/v1/nixl/
71+
- vllm/v1/core/sched/
72+
- vllm/v1/core/kv_cache_coordinator.py
73+
- tests/v1/kv_connector/nixl_integration/
74+
commands:
75+
- bash /vllm-workspace/.buildkite/scripts/install-kv-connectors.sh
76+
- bash v1/kv_connector/nixl_integration/run_mamba_prefix_cache_test.sh
77+
6478
- label: MultiConnector (Nixl+Offloading) PD accuracy (2 GPUs)
6579
key: multiconnector-nixl-offloading-pd-accuracy-2-gpus
6680
timeout_in_minutes: 30

benchmarks/kv_cache_watermark.sh

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
#!/bin/bash
2+
# SPDX-License-Identifier: Apache-2.0
3+
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
4+
#
5+
# Reproducible demonstration of the KV cache watermark (`--watermark`) for
6+
# reducing preemption thrashing.
7+
#
8+
# The watermark is the fraction of total KV cache blocks the scheduler keeps
9+
# free when admitting a waiting/preempted request into the running queue.
10+
#
11+
# Why this workload triggers thrashing:
12+
# Requests are admitted based on the KV cache they need *at admission time*.
13+
# With `--scheduler-reserve-full-isl` (default) the input length is reserved up
14+
# front, but the *output* length is unknown and unreserved. A decode-heavy
15+
# workload (output >> input) at high concurrency therefore over-admits while
16+
# requests are short, then runs out of KV cache as they all grow during decode
17+
# -> the scheduler preempts (recompute) recently-admitted requests, re-prefills
18+
# them later, and repeats. The watermark keeps a block of KV cache free so
19+
# running requests can grow into it instead of triggering this churn.
20+
#
21+
# This script launches `vllm serve` under a deliberately KV-constrained config
22+
# and a decode-heavy workload, sweeping the watermark across several values, and
23+
# reports the preemption count (scraped from /metrics), throughput, and latency
24+
# percentiles for each. It then plots the results.
25+
#
26+
# Default workload: concurrency 200, input ~300 tokens, output ~4000 tokens
27+
# (+/- 20% variance), sized to run each config for ~5 minutes.
28+
#
29+
# Usage:
30+
# benchmarks/kv_cache_watermark.sh
31+
# MODEL=Qwen/Qwen2.5-14B-Instruct TP=2 benchmarks/kv_cache_watermark.sh
32+
#
33+
# Run inside the vLLM virtualenv (so `vllm` and `python` resolve to it).
34+
set -euo pipefail
35+
36+
# ---- Config (override via environment) -------------------------------------
37+
MODEL=${MODEL:-Qwen/Qwen2.5-7B-Instruct}
38+
TP=${TP:-1}
39+
PORT=${PORT:-8000}
40+
URL="http://127.0.0.1:${PORT}"
41+
# Constrain the KV cache to a *near-critical* size: large enough that the engine
42+
# can run stably, but small enough that greedy over-admission tips it into
43+
# preemption thrashing. (Independent of GPU size, so the demo is reproducible.)
44+
# At the default workload this fits ~1.5x the mean concurrent KV demand.
45+
KV_CACHE_MEMORY_GB=${KV_CACHE_MEMORY_GB:-16}
46+
MAX_MODEL_LEN=${MAX_MODEL_LEN:-8192}
47+
MAX_NUM_SEQS=${MAX_NUM_SEQS:-256}
48+
# Optional weight loader (e.g. fastsafetensors on the GCP cluster).
49+
LOAD_FORMAT=${LOAD_FORMAT:-auto}
50+
# Decode-heavy workload: moderate input, long output, with length variance. The
51+
# long output means preempted requests have generated a lot before eviction, so
52+
# resuming them re-prefills a long sequence (high recomputation cost).
53+
INPUT_LEN=${INPUT_LEN:-1000}
54+
OUTPUT_LEN=${OUTPUT_LEN:-5000}
55+
RANGE_RATIO=${RANGE_RATIO:-0.2}
56+
CONCURRENCY=${CONCURRENCY:-128}
57+
# Enough prompts to keep each config saturated for ~5+ minutes.
58+
NUM_PROMPTS=${NUM_PROMPTS:-450}
59+
OUTDIR=${OUTDIR:-./watermark_bench_results}
60+
# Watermark fractions compared. "label value" per line; value=0 disables it.
61+
CONFIGS=${CONFIGS:-"off 0
62+
w0.02 0.02
63+
w0.05 0.05
64+
w0.10 0.10
65+
w0.15 0.15"}
66+
67+
KV_CACHE_MEMORY_BYTES=$((KV_CACHE_MEMORY_GB * 1024 * 1024 * 1024))
68+
mkdir -p "$OUTDIR"
69+
70+
SERVER_PID=""
71+
cleanup() { [[ -n "$SERVER_PID" ]] && kill "$SERVER_PID" 2>/dev/null || true; }
72+
trap cleanup EXIT
73+
74+
scrape_preemptions() {
75+
# Sum the vllm:num_preemptions_total counter across engines.
76+
python - "${URL}/metrics" <<'PY'
77+
import sys, urllib.request
78+
total = 0.0
79+
try:
80+
body = urllib.request.urlopen(sys.argv[1], timeout=10).read().decode("utf-8", "replace")
81+
for line in body.splitlines():
82+
if line.startswith("vllm:num_preemptions_total"):
83+
total += float(line.rsplit(" ", 1)[-1])
84+
except Exception as e: # noqa: BLE001
85+
print(f"scrape error: {e}", file=sys.stderr)
86+
print(int(total))
87+
PY
88+
}
89+
90+
wait_for_server() {
91+
for _ in $(seq 1 300); do
92+
if curl -s "${URL}/health" >/dev/null 2>&1; then return 0; fi
93+
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
94+
echo "ERROR: server process exited during startup" >&2; return 1
95+
fi
96+
sleep 5
97+
done
98+
echo "ERROR: server did not become ready" >&2; return 1
99+
}
100+
101+
run_one() {
102+
local label=$1 watermark=$2
103+
echo
104+
echo "==================== watermark: ${label} (${watermark}) ===================="
105+
vllm serve "$MODEL" \
106+
--tensor-parallel-size "$TP" \
107+
--load-format "$LOAD_FORMAT" \
108+
--kv-cache-memory-bytes "$KV_CACHE_MEMORY_BYTES" \
109+
--max-model-len "$MAX_MODEL_LEN" \
110+
--max-num-seqs "$MAX_NUM_SEQS" \
111+
--no-enable-prefix-caching \
112+
--watermark "$watermark" \
113+
--port "$PORT" >"${OUTDIR}/serve_${label}.log" 2>&1 &
114+
SERVER_PID=$!
115+
wait_for_server
116+
sleep 5
117+
118+
local pre post
119+
pre=$(scrape_preemptions)
120+
vllm bench serve \
121+
--backend vllm \
122+
--base-url "$URL" \
123+
--model "$MODEL" \
124+
--dataset-name random \
125+
--random-input-len "$INPUT_LEN" \
126+
--random-output-len "$OUTPUT_LEN" \
127+
--random-range-ratio "$RANGE_RATIO" \
128+
--ignore-eos \
129+
--num-prompts "$NUM_PROMPTS" \
130+
--max-concurrency "$CONCURRENCY" \
131+
--percentile-metrics "ttft,tpot,itl,e2el" \
132+
--metric-percentiles "50,90,99" \
133+
--save-result \
134+
--result-dir "$OUTDIR" \
135+
--result-filename "bench_${label}.json"
136+
post=$(scrape_preemptions)
137+
echo "${label} ${watermark} $((post - pre))" >>"${OUTDIR}/preemptions.txt"
138+
139+
kill "$SERVER_PID" 2>/dev/null || true
140+
for _ in $(seq 1 60); do curl -s "${URL}/health" >/dev/null 2>&1 || break; sleep 2; done
141+
SERVER_PID=""
142+
sleep 10
143+
}
144+
145+
: >"${OUTDIR}/preemptions.txt"
146+
while read -r label watermark; do
147+
[[ -z "${label:-}" ]] && continue
148+
run_one "$label" "$watermark"
149+
done <<<"$CONFIGS"
150+
151+
echo
152+
echo "==================== summary ===================="
153+
python - "$OUTDIR" <<'PY'
154+
import json, os, sys
155+
outdir = sys.argv[1]
156+
pre = {}
157+
order = []
158+
for line in open(os.path.join(outdir, "preemptions.txt")):
159+
label, watermark, n = line.split()
160+
pre[label] = (float(watermark), int(n))
161+
order.append(label)
162+
163+
def g(d, *names):
164+
for n in names:
165+
if d.get(n) is not None:
166+
return d[n]
167+
return float("nan")
168+
169+
cols = ["watermark", "frac", "preempt", "out_tok/s", "req/s",
170+
"TTFT_p50", "TTFT_p99", "ITL_p99", "E2EL_p50"]
171+
print(" ".join(f"{c:>10}" for c in cols))
172+
rows = []
173+
for label in order:
174+
watermark, n = pre[label]
175+
d = json.load(open(os.path.join(outdir, f"bench_{label}.json")))
176+
rows.append(dict(
177+
label=label, watermark=watermark, preempt=n,
178+
out_tok_s=g(d, "output_throughput"),
179+
req_s=g(d, "request_throughput"),
180+
ttft_p50=g(d, "p50_ttft_ms", "median_ttft_ms"),
181+
ttft_p99=g(d, "p99_ttft_ms"),
182+
itl_p99=g(d, "p99_itl_ms"),
183+
e2el_p50=g(d, "p50_e2el_ms", "median_e2el_ms"),
184+
))
185+
print(" ".join(f"{str(v):>10}" for v in [
186+
label, watermark, n,
187+
f"{rows[-1]['out_tok_s']:.0f}",
188+
f"{rows[-1]['req_s']:.3f}",
189+
f"{rows[-1]['ttft_p50']/1000:.2f}",
190+
f"{rows[-1]['ttft_p99']/1000:.2f}",
191+
f"{rows[-1]['itl_p99']:.2f}",
192+
f"{rows[-1]['e2el_p50']/1000:.1f}",
193+
]))
194+
print("\n(TTFT/E2EL in seconds; ITL in ms. Lower preempt is better.)")
195+
196+
# ---- Plot -------------------------------------------------------------------
197+
try:
198+
import matplotlib
199+
matplotlib.use("Agg")
200+
import matplotlib.pyplot as plt
201+
except Exception as e: # noqa: BLE001
202+
print(f"\n(skip plot: matplotlib unavailable: {e})")
203+
sys.exit(0)
204+
205+
x = [r["watermark"] for r in rows]
206+
xt = [f"{r['watermark']:g}\n({r['label']})" for r in rows]
207+
idx = list(range(len(rows)))
208+
209+
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
210+
fig.suptitle(
211+
f"KV cache watermark sweep — {os.path.basename(os.path.abspath(outdir))}",
212+
fontsize=12,
213+
)
214+
215+
ax = axes[0][0]
216+
ax.bar(idx, [r["preempt"] for r in rows], color="tab:red")
217+
ax.set_title("Preemptions (lower is better)")
218+
ax.set_ylabel("preemptions")
219+
ax.set_xticks(idx); ax.set_xticklabels(xt)
220+
221+
ax = axes[0][1]
222+
ax.plot(idx, [r["out_tok_s"] for r in rows], "o-", color="tab:green")
223+
ax.set_title("Output throughput (higher is better)")
224+
ax.set_ylabel("tokens/s")
225+
ax.set_xticks(idx); ax.set_xticklabels(xt)
226+
227+
ax = axes[1][0]
228+
ax.plot(idx, [r["itl_p99"] for r in rows], "o-", color="tab:blue")
229+
ax.set_title("Inter-token latency p99 (lower is better)")
230+
ax.set_ylabel("ITL p99 (ms)")
231+
ax.set_xlabel("watermark fraction")
232+
ax.set_xticks(idx); ax.set_xticklabels(xt)
233+
234+
ax = axes[1][1]
235+
ax.plot(idx, [r["ttft_p50"] / 1000 for r in rows], "o-", label="TTFT p50")
236+
ax.plot(idx, [r["ttft_p99"] / 1000 for r in rows], "o-", label="TTFT p99")
237+
ax.plot(idx, [r["e2el_p50"] / 1000 for r in rows], "o-", label="E2EL p50")
238+
ax.set_title("Latency (lower is better)")
239+
ax.set_ylabel("seconds")
240+
ax.set_xlabel("watermark fraction")
241+
ax.set_xticks(idx); ax.set_xticklabels(xt)
242+
ax.legend()
243+
244+
fig.tight_layout(rect=(0, 0, 1, 0.95))
245+
out_png = os.path.join(outdir, "watermark_results.png")
246+
fig.savefig(out_png, dpi=120)
247+
print(f"\nWrote plot: {out_png}")
248+
PY

docs/design/moe_kernel_features.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ th {
4242
1. All types: mxfp4, nvfp4, int4, int8, fp8
4343
2. A,T quantization occurs after dispatch.
4444
3. All quantization happens after dispatch.
45-
4. Controlled by different env vars (`VLLM_FLASHINFER_MOE_BACKEND` "throughput" or "latency")
45+
4. Controlled by `--moe-backend` (`flashinfer_cutlass` or `flashinfer_trtllm`)
4646
5. This is a no-op dispatcher that can be used to pair with any modular experts to produce a modular kernel that runs without dispatch or combine. These cannot be selected via environment variable. These are generally use for testing or adapting an expert subclass to the `fused_experts` API.
4747
6. This depends on the experts implementation.
4848

docs/models/pooling_models/reward.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,4 +143,4 @@ More examples can be found here: [examples/pooling/reward](../../../examples/poo
143143

144144
### `LLM.reward`
145145

146-
`llm.reward` api is deprecated and will be removed in v0.23. Please use `LLM.encode` with `pooling_task="classify"` or `pooling_task="token_classify"` instead.
146+
`llm.reward` API is deprecated and was removed in v0.24. Please use `LLM.encode` with `pooling_task="classify"` or `pooling_task="token_classify"` instead.

mkdocs.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ plugins:
114114
features/quantization/int4.md: features/quantization/llm_compressor/int4.md
115115
features/quantization/int8.md: features/quantization/llm_compressor/int8_w8a8.md
116116
serving/openai_compatible_server.md: serving/online_serving/README.md
117+
examples/others/lmcache.md: examples/disaggregated/lmcache.md
117118

118119
markdown_extensions:
119120
- attr_list

tests/compile/correctness_e2e/test_async_tp.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def test_async_tp_pass_correctness(
102102

103103

104104
@create_new_process_for_each_test()
105-
def test_async_tp_pass_nvfp4_correctness(num_gpus_available: int, monkeypatch):
105+
def test_async_tp_pass_nvfp4_correctness(num_gpus_available: int):
106106
if (
107107
not current_platform.is_cuda()
108108
or not current_platform.is_device_capability_family(100)
@@ -111,8 +111,6 @@ def test_async_tp_pass_nvfp4_correctness(num_gpus_available: int, monkeypatch):
111111
if not has_flashinfer():
112112
pytest.skip("FlashInfer is required for the NVFP4 AsyncTP path")
113113

114-
monkeypatch.setenv("VLLM_NVFP4_GEMM_BACKEND", "flashinfer-cutlass")
115-
116114
tp_size = 2
117115
if num_gpus_available < tp_size:
118116
pytest.skip(f"Need at least {tp_size} GPUs")
@@ -126,6 +124,8 @@ def test_async_tp_pass_nvfp4_correctness(num_gpus_available: int, monkeypatch):
126124
"8",
127125
"--load-format",
128126
"dummy",
127+
"--linear-backend",
128+
"flashinfer_cutlass",
129129
"--hf-overrides",
130130
json.dumps(NVFP4_HF_OVERRIDES),
131131
]

tests/conftest.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1226,10 +1226,6 @@ def token_classify(self, prompts: list[str]) -> list[list[float]]:
12261226
req_outputs = self.llm.encode(prompts, pooling_task="token_classify")
12271227
return [req_output.outputs.data for req_output in req_outputs]
12281228

1229-
def reward(self, prompts: list[str]) -> list[list[float]]:
1230-
req_outputs = self.llm.encode(prompts, pooling_task="token_classify")
1231-
return [req_output.outputs.data for req_output in req_outputs]
1232-
12331229
def score(
12341230
self,
12351231
text_1: list[str] | str,

tests/distributed/test_eplb_fused_moe_layer_dep_nvfp4.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class TestConfig:
3737
hidden_size: int
3838
intermediate_size: int
3939
num_tokens: int
40+
moe_backend: str
4041

4142

4243
def make_fused_moe_layer(
@@ -114,6 +115,7 @@ def _test_eplb_fml(env, world_size: int, test_config: TestConfig):
114115
vllm_config = VllmConfig()
115116
vllm_config.parallel_config.data_parallel_size = world_size
116117
vllm_config.parallel_config.enable_expert_parallel = True
118+
vllm_config.kernel_config.moe_backend = test_config.moe_backend
117119

118120
with set_current_vllm_config(vllm_config):
119121
ensure_model_parallel_initialized(
@@ -250,20 +252,16 @@ def _test_eplb_fml(env, world_size: int, test_config: TestConfig):
250252
@pytest.mark.parametrize("hidden_size", [256])
251253
@pytest.mark.parametrize("intermediate_size", [256])
252254
@pytest.mark.parametrize("num_tokens", [256])
253-
@pytest.mark.parametrize("backend", ["latency", "throughput"])
255+
@pytest.mark.parametrize("moe_backend", ["flashinfer_trtllm", "flashinfer_cutlass"])
254256
def test_eplb_fml(
255257
world_size: int,
256258
num_layers: int,
257259
num_experts: int,
258260
hidden_size: int,
259261
intermediate_size: int,
260262
num_tokens: int,
261-
backend: str,
262-
monkeypatch,
263+
moe_backend: str,
263264
):
264-
monkeypatch.setenv("VLLM_USE_FLASHINFER_MOE_FP4", "1")
265-
monkeypatch.setenv("VLLM_FLASHINFER_MOE_BACKEND", backend)
266-
267265
if torch.accelerator.device_count() < world_size:
268266
pytest.skip(f"Need at least {world_size} GPUs to run the test")
269267

@@ -278,6 +276,7 @@ def test_eplb_fml(
278276
hidden_size=hidden_size,
279277
intermediate_size=intermediate_size,
280278
num_tokens=num_tokens,
279+
moe_backend=moe_backend,
281280
)
282281

283282
distributed_run(

0 commit comments

Comments
 (0)