Skip to content

Commit 73f23ba

Browse files
committed
fix(uat): append GKE driver lib path for served vLLM worker
The #1644 proving run (32732018329) reached phase_serve for the first time and the VllmDecodeWorker crash-looped before binding its health port. GKE mounts the node driver at /usr/local/nvidia without putting it on LD_LIBRARY_PATH, so vLLM cannot dlopen libcuda.so.1 — the same hole inference-perf already works around, which is why it served the same runtime on the same cluster minutes earlier. Wrap the worker command with the append used by the validators/performance Dynamo templates, and capture --previous container logs in serve_debug so a crash loop is diagnosable instead of dumping the empty stdout of its replacement container. Signed-off-by: Rohit Rajani <rorajani@nvidia.com>
1 parent ed6c69e commit 73f23ba

2 files changed

Lines changed: 70 additions & 7 deletions

File tree

tests/uat/lib/phases.sh

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,10 +1307,15 @@ serve_debug() {
13071307
kubectl describe pods -n "${SERVE_NAMESPACE}" 2>&1 || true
13081308
echo "--- events ---"
13091309
kubectl get events -n "${SERVE_NAMESPACE}" --sort-by=.lastTimestamp 2>&1 || true
1310-
echo "--- logs (all pods, all containers, last 200 lines) ---"
1310+
echo "--- logs (all pods, all containers, last 200 lines; --previous for crash loops) ---"
13111311
for p in $(kubectl get pods -n "${SERVE_NAMESPACE}" -o name 2>/dev/null); do
13121312
echo "=== ${p} ==="
13131313
kubectl logs -n "${SERVE_NAMESPACE}" "${p#pod/}" --all-containers --tail=200 2>&1 || true
1314+
# CrashLoopBackOff replaces the current container before this dump
1315+
# runs, so the dying process's stdout is on --previous. Missing
1316+
# previous (first start, or already GC'd) is a no-op.
1317+
echo "=== ${p} (previous) ==="
1318+
kubectl logs -n "${SERVE_NAMESPACE}" "${p#pod/}" --all-containers --previous --tail=200 2>&1 || true
13141319
done
13151320
} | tee serve-logs/"${SERVE_NAME}".log
13161321
}
@@ -1384,7 +1389,24 @@ spec:
13841389
- name: main
13851390
image: ${SERVE_RUNTIME_IMAGE}
13861391
workingDir: /workspace/examples/backends/vllm
1387-
command: ["python3", "-m", "dynamo.vllm"]
1392+
# A bare `python3 -m dynamo.vllm` here crash-looped on GKE before
1393+
# binding its health port (run 32732018329), while inference-perf
1394+
# served the same runtime on the SAME cluster minutes earlier
1395+
# using this wrapper. GKE mounts the node driver at
1396+
# /usr/local/nvidia without putting it on LD_LIBRARY_PATH, so
1397+
# vLLM cannot dlopen libcuda.so.1 ("Failed to infer device type",
1398+
# observed live in gke-default qualification — see the same
1399+
# wrapper in validators/performance/testdata/inference).
1400+
# Shell APPEND with \${VAR:+} so we do not clobber
1401+
# the image's nixl/ucx/cuda entries or create a leading empty
1402+
# ld.so entry. Harmless no-op when the path is absent (EKS/AKS
1403+
# GPU Operator toolkit). \$ so the unquoted heredoc does not
1404+
# expand LD_LIBRARY_PATH at render time.
1405+
command:
1406+
- /bin/bash
1407+
- -c
1408+
- export LD_LIBRARY_PATH="\${LD_LIBRARY_PATH:+\${LD_LIBRARY_PATH}:}/usr/local/nvidia/lib64"; exec python3 -m dynamo.vllm "\$@"
1409+
- dynamo.vllm
13881410
args:
13891411
- --model
13901412
- ${SERVE_MODEL}
@@ -1402,9 +1424,12 @@ phase_serve() {
14021424
# DynamoGraphDeployment in demos/cuj2-inference.md
14031425
# (demos/workloads/inference/vllm-agg.yaml): the KAI queue and a
14041426
# two-component (Frontend + decode Worker) graph serving an OpenAI-compatible
1405-
# endpoint. Frontend placement intentionally diverges — the demo pins
1406-
# nodeGroup=cpu-worker; this graph selects the GPU pool for both components
1407-
# (pool-selection note below, #1644). The worker requests its GPU as a scalar
1427+
# endpoint. Two intentional divergences from the demo: Frontend placement
1428+
# (the demo pins nodeGroup=cpu-worker; this graph selects the GPU pool for
1429+
# both components — pool-selection note below, #1644), and the worker
1430+
# command (the demo uses python3 -m dynamo.vllm; this graph wraps it with
1431+
# the GKE driver-lib append used by inference-perf, or vLLM cannot see
1432+
# libcuda.so.1 on gke-default). The worker requests its GPU as a scalar
14081433
# nvidia.com/gpu limit — the device-plugin production default (#1327).
14091434
#
14101435
# Tolerations are a portable SUPERSET of the taints across all UAT clusters
@@ -1424,7 +1449,9 @@ phase_serve() {
14241449
# no device: the Frontend declares no nvidia.com/gpu limit, so the device
14251450
# plugin never allocates one to it. This matches how the inference-perf
14261451
# validator places every component on the GPU cohort
1427-
# (validators/performance/inference_perf_constraint.go).
1452+
# (validators/performance/inference_perf_constraint.go). The worker command
1453+
# is the same GKE driver-lib append as that validator's Dynamo templates;
1454+
# without it, vLLM crash-loops on gke-default (run 32732018329).
14281455
#
14291456
# This selects the POOL, not a node: the pool holds two GPU nodes and nothing
14301457
# constrains the two components to the same one, so they may be split. That is

tests/uat/serve_manifest_test.go

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"io"
2121
"os"
2222
"os/exec"
23+
"reflect"
2324
"strings"
2425
"testing"
2526

@@ -164,6 +165,39 @@ func TestServeGraphSelectorIsOverridable(t *testing.T) {
164165
}
165166
}
166167

168+
// TestServeGraphWorkerDriverLibPathAppend pins the GKE driver-lib wrapper on
169+
// the decode worker. GKE's managed device plugin mounts /usr/local/nvidia
170+
// without setting LD_LIBRARY_PATH; a bare `python3 -m dynamo.vllm` then
171+
// crash-loops before it binds its health port (UAT run 32732018329). The
172+
// shape matches validators/performance testdata Dynamo templates: a shell
173+
// APPEND with ${VAR:+}, argv0 consumed by the trailing "dynamo.vllm" element
174+
// so --model stays args[0]. A revert to ["python3", "-m", "dynamo.vllm"]
175+
// must fail here.
176+
func TestServeGraphWorkerDriverLibPathAppend(t *testing.T) {
177+
graph := renderServeGraph(t, nil)
178+
worker := graph.component(t, workerComponent)
179+
180+
wantCommand := []string{
181+
"/bin/bash",
182+
"-c",
183+
`export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:+${LD_LIBRARY_PATH}:}/usr/local/nvidia/lib64"; exec python3 -m dynamo.vllm "$@"`,
184+
"dynamo.vllm",
185+
}
186+
for _, c := range worker.PodTemplate.Spec.Containers {
187+
if c.Name != "main" {
188+
continue
189+
}
190+
if !reflect.DeepEqual(c.Command, wantCommand) {
191+
t.Errorf("worker main command = %#v, want %#v", c.Command, wantCommand)
192+
}
193+
if len(c.Args) == 0 || c.Args[0] != "--model" {
194+
t.Errorf("worker main args = %v, want first element %q", c.Args, "--model")
195+
}
196+
return
197+
}
198+
t.Fatal("main container not found in VllmDecodeWorker")
199+
}
200+
167201
// renderServeGraph sources phases.sh, runs serve_render_manifest, and returns
168202
// the DynamoGraphDeployment document. env entries (KEY=VALUE) override the
169203
// script's defaults. It also asserts the render produced the Queue and Namespace
@@ -279,7 +313,9 @@ type dynamoComponent struct {
279313
NodeSelector map[string]string `yaml:"nodeSelector"`
280314
Tolerations []toleration `yaml:"tolerations"`
281315
Containers []struct {
282-
Name string `yaml:"name"`
316+
Name string `yaml:"name"`
317+
Command []string `yaml:"command"`
318+
Args []string `yaml:"args"`
283319
Resources struct {
284320
// Quantities are `any`: YAML renders `nvidia.com/gpu: 1` as an
285321
// int, while a chart-style "1" would be a string. Comparison is

0 commit comments

Comments
 (0)