A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 4, 00:00 UTC
[ROCm][CI] Fix NIXL spec-decode acceptance startup and diagnostics (#41313)
Signed-off-by: Andreas Karatzas <akaratza@amd.com>
Andreas Karatzas committed
May 10, 2026 at 01:50 UTC
f2840120f632c55a972dc2ec482241db89b6fc0f
5 files changed
+137
-27
.buildkite/test-amd.yaml
+2
-2
@@ -880,7 +880,7 @@ steps:
880
- vllm/platforms/rocm.py
881
commands:
882
- uv pip install --system -r /vllm-workspace/requirements/kv_connectors_rocm.txt
883
- - ROCM_ATTN=1 bash v1/kv_connector/nixl_integration/spec_decode_acceptance_test.sh
883
+ - ATTENTION_BACKEND=ROCM_ATTN bash v1/kv_connector/nixl_integration/spec_decode_acceptance_test.sh
884
885
- label: V1 e2e (2 GPUs) # TBD
886
timeout_in_minutes: 180
@@ -3318,7 +3318,7 @@ steps:
3318
- vllm/platforms/rocm.py
3319
commands:
3320
- uv pip install --system -r /vllm-workspace/requirements/kv_connectors_rocm.txt
3321
- - ROCM_ATTN=1 bash v1/kv_connector/nixl_integration/spec_decode_acceptance_test.sh
3321
+ - ATTENTION_BACKEND=ROCM_ATTN bash v1/kv_connector/nixl_integration/spec_decode_acceptance_test.sh
3322
3323
- label: Distributed NixlConnector PD accuracy (4 GPUs) # TBD
3324
timeout_in_minutes: 180
tests/v1/kv_connector/nixl_integration/nixl_side_channel_probe.py
new
+48
@@ -0,0 +1,48 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Probe a NIXL side-channel socket for handshake metadata readiness."""
4
+
5
+import argparse
6
+import ipaddress
7
+
8
+import msgspec
9
+import zmq
10
+
11
+GET_META_MSG = b"get_meta_msg"
12
+
13
+
14
+def parse_args() -> argparse.Namespace:
15
+ parser = argparse.ArgumentParser()
16
+ parser.add_argument("--host", required=True)
17
+ parser.add_argument("--port", required=True, type=int)
18
+ parser.add_argument("--rank", default=0, type=int)
19
+ parser.add_argument("--timeout-ms", default=1000, type=int)
20
+ return parser.parse_args()
21
+
22
+
23
+def make_zmq_path(host: str, port: int) -> str:
24
+ try:
25
+ if isinstance(ipaddress.ip_address(host), ipaddress.IPv6Address):
26
+ return f"tcp://[{host}]:{port}"
27
+ except ValueError:
28
+ pass
29
+ return f"tcp://{host}:{port}"
30
+
31
+
32
+def main() -> None:
33
+ args = parse_args()
34
+ ctx = zmq.Context()
35
+ sock = ctx.socket(zmq.REQ)
36
+ sock.setsockopt(zmq.LINGER, 0)
37
+ sock.setsockopt(zmq.RCVTIMEO, args.timeout_ms)
38
+ try:
39
+ sock.connect(make_zmq_path(args.host, args.port))
40
+ sock.send(msgspec.msgpack.encode((GET_META_MSG, args.rank)))
41
+ sock.recv()
42
+ finally:
43
+ sock.close()
44
+ ctx.term()
45
+
46
+
47
+if __name__ == "__main__":
48
+ main()
tests/v1/kv_connector/nixl_integration/spec_decode_acceptance_test.sh
+64
-18
@@ -51,6 +51,8 @@ PREFILLER_TP_SIZE=${PREFILLER_TP_SIZE:-1}
51
DECODER_TP_SIZE=${DECODER_TP_SIZE:-1}
52
GPU_MEMORY_UTILIZATION=${GPU_MEMORY_UTILIZATION:-0.7}
53
BLOCK_SIZE=${BLOCK_SIZE:-16}
54
+SERVER_HOST="${SERVER_HOST:-127.0.0.1}"
55
+NIXL_SIDE_CHANNEL_HOST="${NIXL_SIDE_CHANNEL_HOST:-$SERVER_HOST}"
56
57
# Resolve the repository root from the script location instead of `.git`.
58
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
@@ -96,18 +98,57 @@ trap 'echo " Interrupted."; exit 130' INT TERM
98
99
wait_for_server() {
100
local port=$1
99
- local deadline=600
101
+ local server_pid=$2
102
+ local server_name=$3
103
+ local endpoint=${4:-/v1/completions}
104
+ local deadline=${5:-600}
105
local elapsed=0
101
- echo "Waiting for server on port ${port}..."
106
+ echo "Waiting for ${server_name} on port ${port}..."
107
while [ $elapsed -lt $deadline ]; do
103
- if curl -s "localhost:${port}/v1/completions" > /dev/null 2>&1; then
104
- echo "Server on port ${port} ready"
108
+ if ! ps -p "$server_pid" > /dev/null 2>&1; then
109
+ local status=0
110
+ wait "$server_pid" || status=$?
111
+ echo "FAIL: ${server_name} process ${server_pid} exited with status ${status} before port ${port} became ready"
112
+ exit 1
113
+ fi
114
+ if curl -s "http://${SERVER_HOST}:${port}${endpoint}" > /dev/null 2>&1; then
115
+ echo "${server_name} on port ${port} ready"
116
return 0
117
fi
118
sleep 2
119
elapsed=$((elapsed + 2))
120
done
110
- echo "FAIL: Server on port ${port} did not start within ${deadline}s"
121
+ echo "FAIL: ${server_name} on port ${port} did not start within ${deadline}s"
122
+ exit 1
123
+}
124
+
125
+wait_for_nixl_side_channel() {
126
+ local host=$1
127
+ local port=$2
128
+ local server_pid=$3
129
+ local server_name=$4
130
+ local deadline=120
131
+ local elapsed=0
132
+ echo "Waiting for ${server_name} NIXL side channel on ${host}:${port}..."
133
+ while [ $elapsed -lt $deadline ]; do
134
+ if ! ps -p "$server_pid" > /dev/null 2>&1; then
135
+ local status=0
136
+ wait "$server_pid" || status=$?
137
+ echo "FAIL: ${server_name} server process ${server_pid} exited with status ${status} before NIXL side channel ${host}:${port} became ready"
138
+ exit 1
139
+ fi
140
+ if python3 "${GIT_ROOT}/tests/v1/kv_connector/nixl_integration/nixl_side_channel_probe.py" \
141
+ --host "$host" \
142
+ --port "$port" \
143
+ --timeout-ms 1000 > /dev/null 2>&1
144
+ then
145
+ echo "${server_name} NIXL side channel on ${host}:${port} ready"
146
+ return 0
147
+ fi
148
+ sleep 2
149
+ elapsed=$((elapsed + 2))
150
+ done
151
+ echo "FAIL: ${server_name} NIXL side channel ${host}:${port} did not start within ${deadline}s"
152
exit 1
153
}
154
@@ -158,6 +199,8 @@ run_test_for_device() {
199
echo "KV buffer device: ${kv_device}"
200
echo "Attention backend: ${ATTENTION_BACKEND}"
201
echo "GPU platform: ${GPU_PLATFORM}"
202
+ echo "Server host: ${SERVER_HOST}"
203
+ echo "NIXL side channel: ${NIXL_SIDE_CHANNEL_HOST}"
204
echo "GPUs available: ${ALL_GPUS[*]}"
205
echo "================================================================"
206
@@ -167,7 +210,8 @@ run_test_for_device() {
210
local DECODE_PORTS=()
211
local GPU_IDX=0
212
170
- # Start prefill instances
213
+ # Start prefill instances and wait for each one before allocating the next
214
+ # server. This keeps failures from leaving extra model servers spinning.
215
for i in $(seq 0 $((NUM_PREFILL_INSTANCES-1))); do
216
local GPU_ID="${ALL_GPUS[$GPU_IDX]}"
217
GPU_IDX=$((GPU_IDX + 1))
@@ -184,6 +228,7 @@ run_test_for_device() {
228
${GPU_DEVICE_VAR}=$GPU_ID \
229
VLLM_KV_CACHE_LAYOUT='HND' \
230
UCX_NET_DEVICES=all \
231
+ VLLM_NIXL_SIDE_CHANNEL_HOST=$NIXL_SIDE_CHANNEL_HOST \
232
VLLM_NIXL_SIDE_CHANNEL_PORT=$SIDE_CHANNEL_PORT \
233
vllm serve $MODEL_NAME \
234
--port $PORT \
@@ -195,12 +240,15 @@ run_test_for_device() {
240
--kv-transfer-config "$kv_config" \
241
--speculative-config "$PREFILL_SPEC_CONFIG" \
242
--attention-backend $ATTENTION_BACKEND &
243
+ local SERVER_PID=$!
244
199
- PREFILL_HOSTS+=("localhost")
245
+ PREFILL_HOSTS+=("$SERVER_HOST")
246
PREFILL_PORTS+=("$PORT")
247
+ wait_for_server "$PORT" "$SERVER_PID" "prefill"
248
+ wait_for_nixl_side_channel "$NIXL_SIDE_CHANNEL_HOST" "$SIDE_CHANNEL_PORT" "$SERVER_PID" "prefill"
249
done
250
203
- # Start decode instances
251
+ # Start decode instances after prefill is ready.
252
for i in $(seq 0 $((NUM_DECODE_INSTANCES-1))); do
253
local GPU_ID="${ALL_GPUS[$GPU_IDX]}"
254
GPU_IDX=$((GPU_IDX + 1))
@@ -217,6 +265,7 @@ run_test_for_device() {
265
${GPU_DEVICE_VAR}=$GPU_ID \
266
VLLM_KV_CACHE_LAYOUT='HND' \
267
UCX_NET_DEVICES=all \
268
+ VLLM_NIXL_SIDE_CHANNEL_HOST=$NIXL_SIDE_CHANNEL_HOST \
269
VLLM_NIXL_SIDE_CHANNEL_PORT=$SIDE_CHANNEL_PORT \
270
vllm serve $MODEL_NAME \
271
--port $PORT \
@@ -228,17 +277,12 @@ run_test_for_device() {
277
--kv-transfer-config "$kv_config" \
278
--speculative-config "$DECODE_SPEC_CONFIG" \
279
--attention-backend $ATTENTION_BACKEND &
280
+ local SERVER_PID=$!
281
232
- DECODE_HOSTS+=("localhost")
282
+ DECODE_HOSTS+=("$SERVER_HOST")
283
DECODE_PORTS+=("$PORT")
234
- done
235
-
236
- # Wait for servers
237
- for PORT in "${PREFILL_PORTS[@]}"; do
238
- wait_for_server "$PORT"
239
- done
240
- for PORT in "${DECODE_PORTS[@]}"; do
241
- wait_for_server "$PORT"
284
+ wait_for_server "$PORT" "$SERVER_PID" "decode"
285
+ wait_for_nixl_side_channel "$NIXL_SIDE_CHANNEL_HOST" "$SIDE_CHANNEL_PORT" "$SERVER_PID" "decode"
286
done
287
288
# Start proxy
@@ -250,12 +294,14 @@ run_test_for_device() {
294
--prefiller-ports ${PREFILL_PORTS[*]} \
295
--decoder-hosts ${DECODE_HOSTS[*]} \
296
--decoder-ports ${DECODE_PORTS[*]} &
297
+ local PROXY_PID=$!
298
254
- sleep 5
299
+ wait_for_server "$PROXY_PORT" "$PROXY_PID" "proxy" "/healthcheck" 60
300
301
# Run test
302
echo "Running spec decode acceptance test (kv_buffer_device=${kv_device}, backend=${ATTENTION_BACKEND})..."
303
DECODE_PORT=${DECODE_PORTS[0]} \
304
+ SERVER_HOST=$SERVER_HOST \
305
TEST_MODEL=$MODEL_NAME \
306
python3 -m pytest -s -x "${GIT_ROOT}/tests/v1/kv_connector/nixl_integration/test_spec_decode_acceptance.py"
307
tests/v1/kv_connector/nixl_integration/test_spec_decode_acceptance.py
+4
-3
@@ -27,7 +27,8 @@ from transformers import AutoTokenizer
27
28
from vllm.benchmarks.datasets import get_samples
29
30
-PROXY_BASE_URL = "http://localhost:8192/v1"
30
+SERVER_HOST = os.environ.get("SERVER_HOST", "127.0.0.1")
31
+PROXY_BASE_URL = f"http://{SERVER_HOST}:8192/v1"
32
DECODE_PORT = os.environ.get("DECODE_PORT", "8200")
33
MODEL_NAME = os.environ.get("TEST_MODEL", "meta-llama/Llama-3.1-8B-Instruct")
34
@@ -101,7 +102,7 @@ def _get_mt_bench_prompts() -> list[str]:
102
103
def _fetch_metric(metric_name: str) -> float:
104
"""Fetch a single counter metric from the decode server's /metrics."""
104
- url = f"http://localhost:{DECODE_PORT}/metrics"
105
+ url = f"http://{SERVER_HOST}:{DECODE_PORT}/metrics"
106
body = urlopen(url).read().decode()
107
for line in body.split("\n"):
108
if line.startswith(metric_name + "{") or line.startswith(metric_name + " "):
@@ -111,7 +112,7 @@ def _fetch_metric(metric_name: str) -> float:
112
113
def _fetch_per_position_acceptance() -> dict[int, float]:
114
"""Fetch per-position acceptance counts from decode /metrics."""
114
- url = f"http://localhost:{DECODE_PORT}/metrics"
115
+ url = f"http://{SERVER_HOST}:{DECODE_PORT}/metrics"
116
body = urlopen(url).read().decode()
117
counts: dict[int, float] = {}
118
for line in body.split("\n"):
vllm/distributed/kv_transfer/kv_connector/v1/nixl/worker.py
+19
-4
@@ -413,7 +413,10 @@ class NixlConnectorWorker:
413
414
self.kv_cache_layout = get_kv_cache_layout()
415
self.host_buffer_kv_cache_layout = self.kv_cache_layout
416
- logger.info("Detected attention backend %s", self.backend_name)
416
+ logger.info(
417
+ "Detected attention backend(s) %s",
418
+ [backend.get_name() for backend in self.attn_backends],
419
+ )
420
logger.info("Detected kv cache layout %s", self.kv_cache_layout)
421
422
# lazy initialized in register_kv_caches
@@ -870,9 +873,21 @@ class NixlConnectorWorker:
873
else:
874
self.block_len_per_layer.append(physical_page_size)
875
873
- assert cache.shape[0] == num_blocks, (
874
- "All kv cache tensors must have the same number of blocks"
875
- )
876
+ if cache.shape[0] != num_blocks:
877
+ raise AssertionError(
878
+ "All kv cache tensors must have the same number of "
879
+ f"blocks; layer={layer_name}, "
880
+ f"expected_num_blocks={num_blocks}, "
881
+ f"cache_shape={tuple(cache.shape)}, "
882
+ f"cache_stride={tuple(cache.stride())}, "
883
+ f"layer_spec={type(layer_spec).__name__}, "
884
+ f"backend={self.backend_name}, "
885
+ "all_backends="
886
+ f"{[backend.get_name() for backend in self.attn_backends]}, "
887
+ f"kv_cache_layout={self.kv_cache_layout}, "
888
+ "blocks_first="
889
+ f"{self.transfer_topo.is_kv_layout_blocks_first}"
890
+ )
891
892
if not self.use_mla:
893
# Different kv cache shape is not supported by HeteroTP.