Skip to content

Commit fbfc1b9

Browse files
Yadan-WeiYadan Wei
andauthored
test(vllm): add EFA test for vLLM Ubuntu (NCCL + NIXL) (#6113)
* test(vllm): add EFA test for vLLM Ubuntu image (NCCL + NIXL) Wires the existing EFA test harness into pr-vllm-ec2.yml. Same 2x p4d fixture covers three checks against EFA: - NCCL collectives over EFA (existing test, now image-agnostic). - NIXL libfabric plugin packaging smoke (new, single-process). - NIXL disaggregated prefill/decode across nodes with LIBFABRIC backend (new, two vLLM servers + minimal proxy, gated on RUN_NIXL_TESTS=1). setup_nccl_tests.sh is a no-op on PyTorch DLCs (binary preinstalled) and compiles nccl-tests against the nvidia-nccl-cu12 wheel on vLLM Ubuntu. nccl_allreduce.sh now defaults CUDA_HOME=/usr/local/cuda for images that do not export it. Signed-off-by: Yadan Wei <yadanwei@amazon.com> * test(vllm): add efa-test-change filter so EFA-only edits trigger the job Without a dedicated change filter, build-change stayed false on EFA-only edits (test/efa/**, reusable-efa-tests.yml), build-image was skipped, and efa-test never fired. Mirrors the sanity-test/telemetry-test/model-tests pattern: fall back to the prod image when no rebuild happened. Signed-off-by: Yadan Wei <yadanwei@amazon.com> * fix(efa): cap nccl-tests NVCC_GENCODE to host GPU to avoid OOM verifiable.cu compiled for nccl-tests' default 9 archs peaks at 8+GB and got SIGKILL'd on the test host (exit 137). Detect the compute capability via nvidia-smi and build only for that one arch; serialize make to keep memory pressure flat. Signed-off-by: Yadan Wei <yadanwei@amazon.com> * fix(efa): replace nccl-tests build with torch.distributed fallback nccl-tests' verifiable.cu compiled with nvcc OOM-kills the build on the test host even with single-arch and -j1 (>8GB peak per file). It's also unconditionally linked into every _perf binary, so we can't strip it. Drop the build entirely. Use torch.distributed all_reduce when the preinstalled binary isn't present (vLLM image): same NCCL→aws-ofi-nccl→ EFA path, no compile step. nccl_allreduce.sh picks the implementation at runtime; existing log validators (aws-ofi-nccl, "Selected provider is efa", Libfabric, GDRDMA) work for both since they parse NCCL_DEBUG=INFO output. Bandwidth threshold (3 GB/s) is now read from a JSON line that torch_allreduce.py emits on rank 0. PyTorch DLC path unchanged: setup_nccl_tests.sh short-circuits, the existing all_reduce_perf binary is used, the existing perf check parses the same column 11 it always did. Signed-off-by: Yadan Wei <yadanwei@amazon.com> * fix(efa): mirror master's build_all_reduce_perf.sh pattern master's V1 EFA test (test/dlc_tests/container_tests/bin/efa/build_all_reduce_perf.sh) builds nccl-tests inside the test container with NCCL_HOME=/usr/local and default NVCC_GENCODE — and works on p4d. Earlier OOMs were from a different NCCL_HOME path (the python wheel) which can produce different template instantiation against alternate headers. Match master: - NCCL_HOME=/usr/local first; fall back to the nvidia-nccl-cu12 wheel only when /usr/local/include/nccl.h is absent (vLLM image case). - No NVCC_GENCODE override — let nccl-tests use its defaults. - Drop the torch.distributed fallback and revert nccl_allreduce.sh to the original single-binary path. Removes torch_allreduce.py. Signed-off-by: Yadan Wei <yadanwei@amazon.com> * chore: gitignore .claude/ runtime files Signed-off-by: Yadan Wei <yadanwei@amazon.com> * fix(efa): widen nccl wheel detection across cu12/cu13 layouts Last run failed at \`python3 -c \"import nvidia.nccl\"\` because import succeeded but \`nvidia.nccl.__file__\` was None — namespace package behavior differs across cu12 and cu13 wheel builds. Replace the import probe with a path-walk over Python's site-packages / dist-packages dirs, looking for the canonical nvidia/nccl/include/nccl.h layout that both cu12 and cu13 wheels use. Add diagnostic output (sys.path + find) so the next failure (if any) surfaces actual evidence instead of an empty WHEEL_DIR. Signed-off-by: Yadan Wei <yadanwei@amazon.com> * fix(efa): install libnccl-dev from apt to avoid wheel-header OOM verifiable.cu compiled against nvidia-nccl-cu13 wheel headers OOMs nvcc (SIGKILL at exit 137) because the wheel ships heavier-templated headers from a different NCCL build than master uses. master's V1 PyTorch DLC builds NCCL from source into /usr/local, producing the thin headers nccl-tests expects. Cheapest reliable fix: apt-install libnccl-dev at test time. Same NCCL version the image runs against (Ubuntu repo tracks the upstream NCCL release), thin headers, ~50MB transient. Use NCCL_HOME=/usr which is where apt puts it. The container is torn down after the test so this doesn't persist into the published image. Signed-off-by: Yadan Wei <yadanwei@amazon.com> * fix(efa): bump nccl-tests build timeout to 1500s Previous run was killed at ~787s with exit 137. DEFAULT_TIMEOUT (600s) applied to setup_nccl_tests.sh wasn't enough — verifiable.cu is template-heavy and the build legitimately takes ~13 min on a p4d. Earlier hypothesis was nvcc OOM, but the log shows nccl.h was already at /usr/include before the apt install (libnccl-dev was already present from the image's EFA install path), so the apt-headers-fix attempt didn't change the compile inputs at all. The kill was a fabric/invoke timeout sending SIGKILL to docker exec, surfacing as 137. Signed-off-by: Yadan Wei <yadanwei@amazon.com> * fix(efa): single-arch nvcc + diagnostics on build failure Latest CI run failed with exit 137 from docker exec — UnexpectedExit from Fabric, not CommandTimedOut, so the 1500s timeout wasn't hit. The container itself was SIGKILL'd. p4d has 1.1TB RAM and devbox repro peaked at 150MB, so plain OOM is unlikely; remaining hypotheses are container cgroup limit, nvidia-runtime hook, or disk exhaustion. Two changes: - Build for the host's actual SM arch only (typically compute_80 on p4d) with -j1. Default GENCODE targets ~9 archs and at peak runs multiple nvcc forks; even with each fork small, the aggregate may trip an unknown limit. - Print free/df/nproc/cgroup memory.max BEFORE the build, and a trap EXIT that dumps the same plus memory.peak and partial build artifacts AFTER any failure (including SIGKILL — the trap runs on signal exits). Next failure (if any) will have actionable evidence in the captured log: which file was being compiled, how much memory the cgroup allowed, how full /tmp was when killed. Signed-off-by: Yadan Wei <yadanwei@amazon.com> * test(efa): trigger PyTorch EFA test on test/efa/** changes PyTorch's efa-test was gated on a fresh image build via 'if: success()' of build-images + sanity + security + unit-test. So edits to test/efa/** or reusable-efa-tests.yml that don't touch the Dockerfile would skip PyTorch's EFA validation entirely — even though those test files are shared with vLLM. Mirror the vllm workflow: a new efa-test-change paths filter, and the efa-test job now fires on (build OR efa-test-change), falling back to the prod image when no rebuild happened. This way a PR like the current one (which only edits test/efa/scripts/ setup_nccl_tests.sh) actually validates that change against both PyTorch and vLLM DLCs. Signed-off-by: Yadan Wei <yadanwei@amazon.com> * fix(efa): override entrypoint when starting test containers vLLM image's ENTRYPOINT is dockerd_entrypoint.sh which invokes 'python3 -m vllm.entrypoints.openai.api_server "$@"'. Running \`docker run -id <vllm-image> bash\` becomes \`api_server bash\`, vllm treats 'bash' as a model_tag and tries to download it from HF. Eventually it crashes and the container exits — by the time the slower side of the test (worker) runs setup_nccl_tests.sh, the container is gone and dockerd returns "container is not running" with exit 1. Fix: pass --entrypoint /bin/bash and 'sleep infinity' so the container stays alive regardless of which framework-specific entrypoint the image ships. PyTorch DLC's entrypoint is also bypassed, but that's harmless since the test only uses docker exec. This was hidden behind the prior failure mode (137 from setup_nccl_tests hitting the master container before its api_server crashed). Worker containers had time to fully crash by the time the loop reached them. Signed-off-by: Yadan Wei <yadanwei@amazon.com> * fix(efa): per-caller concurrency group for efa-test job The reusable efa-test workflow uses 'efa-test-global / cancel-in-progress: false' to serialize p4d capacity. But GitHub allows only one *pending* run per group: when N>=2 callers (PyTorch + vLLM) target the same group on a new commit, the second pending one displaces the first with "Canceling since a higher priority waiting request for efa-test-global exists". Same effect on every push: existing pending efa-test gets cancelled by the next caller pushing into queue. Add a per-caller concurrency group keyed by workflow + PR number with cancel-in-progress: true on the calling efa-test job. Now: - Same PR re-pushed → previous efa-test of that workflow is cancelled (which is what we want — old commit is obsolete). - Different workflow (PyTorch vs vLLM) → different group → both queue on efa-test-global without displacing each other. - Different PR → different group → same. Signed-off-by: Yadan Wei <yadanwei@amazon.com> * fix(efa): preserve PyTorch entrypoint for cuda-compat LD_LIBRARY_PATH Last commit overrode entrypoint to /bin/bash for both image families. That fixed vLLM (whose dockerd_entrypoint.sh execs `vllm serve "$@"` and crashes on `bash`). But it broke PyTorch: its entrypoint sets LD_LIBRARY_PATH=/usr/local/cuda/compat:... when the host's nvidia driver is older than the cuda-compat version. Without that env var, NCCL fails to initialize on p4d hosts ("aws-ofi-nccl is not working"). Detect vLLM by image URI substring; only override the entrypoint there. PyTorch keeps its original `entrypoint.sh bash` invocation. Both paths still produce a long-lived container that docker exec can land on. Signed-off-by: Yadan Wei <yadanwei@amazon.com> * revert(efa): keep PyTorch efa-test gated on fresh build only Adding efa-test-change to the PyTorch workflow caused efa-test to run against the prod image on this PR (which only edits test/efa/**). The prod pytorch:2.11-cu130-amzn2023 image fails the EFA test with "aws-ofi-nccl is not working" — a preexisting issue unrelated to this PR's changes. Surfacing it here red-blocks merging. Revert to the original pattern: PyTorch efa-test only runs when the image was actually rebuilt by this PR. The prod image is presumed validated at its release. The vLLM workflow keeps the efa-test-change trigger because we're still iterating on the test against the vLLM prod image. Keep the per-caller concurrency block — it's still needed to prevent multiple workflows from cross-cancelling on efa-test-global. Signed-off-by: Yadan Wei <yadanwei@amazon.com> * debug(efa): dump pre-mpirun + post-mpirun diagnostics Test failures show only 2 lines from mpirun (Test failure common.cu:1218 + "Process exited with code 2"), no NCCL_DEBUG output despite -x NCCL_DEBUG=INFO. Need actual evidence of: - whether libnccl is on ldconfig path - whether all_reduce_perf links against the expected libnccl - whether libfabric finds the EFA provider - whether aws-ofi-nccl plugin .so is in place Also cat the captured TRAINING_LOG file at the end since the master container is torn down on test exit and the file is otherwise lost. Signed-off-by: Yadan Wei <yadanwei@amazon.com> * debug(efa): stage diagnostics through a file so Fabric truncation can't drop them Previous diagnostic dump (nvidia-smi, ldd, fi_info, etc.) was printed inline to stdout before mpirun, but Fabric/invoke's UnexpectedExit only keeps the last few KB of stdout in the failure trace — those lines got truncated from the captured output. Write diagnostics to /test/efa/logs/diagnostics.log first, then cat that file at the very end (right before validators), so the diagnostic content lands in the tail of stdout that survives truncation. Signed-off-by: Yadan Wei <yadanwei@amazon.com> * debug(efa): put highest-signal probes LAST so truncation can't drop them ca89ff0 wrote diagnostics to a file and cat'd them, but Fabric still truncated to the last ~3KB of stdout — and the captured tail showed only the testEFA.log content + "aws-ofi-nccl is not working", with the diagnostics block dropped from the head of the truncation window. Reorder so diagnostics + final probes are the LAST output before the validators run. The probes are minimal: ldd output, libnccl paths, aws-ofi-nccl .so paths. Those three are enough to diagnose the runtime linker's view. Signed-off-by: Yadan Wei <yadanwei@amazon.com> * fix(efa): pin AMI to 2026-05-01 (last known-good FM/driver pairing) The current SSM-resolved latest AL2023 base-with-single-cuda AMI ships NVIDIA driver 580.150, but no matching nvidia-fabricmanager package exists in NVIDIA's cuda-rhel9 repo (only 580.65/580.95/580.159). On NVSwitch systems (p4d.24xlarge) cuInit returns CUDA_ERROR_SYSTEM_NOT_ YET_INITIALIZED (802) without a matching FM running, which makes the nccl-tests harness fail before NCCL even initializes — surfaces as the misleading "aws-ofi-nccl is not working" validator message. Pin to ami-0d2923a2dd541bdeb (us-west-2, 2026-05-01 build, driver 580.126.09 + matching FM preinstalled). Verified locally on 2026-05-20: cuInit succeeds, NCCL initializes, EFA provider selected and GDRDMA channels established between two p4d instances. Drop the pin once AWS DLAMI publishes an AMI where the bundled driver matches an available FM package. Signed-off-by: Yadan Wei <yadanwei@amazon.com> * fix(efa): parse hosts file in Python, avoid shell quoting run_on_container wraps the command in `bash -c '<cmd>'`. Any single quotes inside cmd (cut -d ' ', awk 'NR==2{...}', etc.) break the outer wrapping and the docker exec sees a malformed command: cut: option requires an argument -- 'd' Replace the inline `sed | cut` with `cat` of the whole file, then split lines and fields in Python — no shell quotes to manage. Signed-off-by: Yadan Wei <yadanwei@amazon.com> * test(efa): print NIXL step stdout/stderr + dump prefill/decode/proxy logs Last green run hid all NIXL output: pytest's captured-log section only shows the LOGGER.info "Running on ..." lines, not the result.stdout of each run_on_container call. So we couldn't see the libfabric_smoke output, the disagg PD orchestrator's prefill/decode handshake, or the completion request response. Wrap each NIXL step in a small _run_nixl helper that prints the cmd, the result.stdout, the result.stderr, and the exit code with ========== markers — pytest is run with -s, so prints land in the captured stdout that survives Fabric truncation. Also cat /test/efa/logs/{prefill,decode,proxy}.log from inside the containers at the end of the NIXL block. Those files are written by nixl_disagg_pd*.sh and disappear when the EC2 fixture terminates the instances; without dumping them here, debugging a failure means relaunching by hand. Signed-off-by: Yadan Wei <yadanwei@amazon.com> * test(efa): apply verbose _step wrapper to NCCL/sanity steps too Same quiet-on-success problem the NIXL block had also affected the upstream NCCL path: a green run hid mpirun output, NCCL_DEBUG, the EFA provider selection, the diagnostics block, and the bandwidth extraction. So we couldn't see what actually happened — only "test passed in 1034s" with no audit trail. Hoist the _step helper to the top of the test and apply it to: - setup_nccl_tests (master + worker) - efa_sanity - nccl_allreduce - nixl_libfabric_smoke (already wrapped, just renamed) - nixl decode_launch + disagg_pd_orchestrator (renamed) Every step now prints cmd, stdout, stderr, exit code with ====== markers so green runs leave evidence we can grep for "Selected provider is efa", "NET/Libfabric/0/GDRDMA", "nixl_disagg_pd test passed", etc. Signed-off-by: Yadan Wei <yadanwei@amazon.com> * test(efa): make NIXL disagg PD strictly verify KV transfer over libfabric Switch kv_role from kv_both → kv_producer (prefill) / kv_consumer (decode) and assert via Prometheus /metrics that decode never ran a local prefill. Before: both sides as kv_both meant decode could silently fall back to re-prefilling the prompt locally if the libfabric KV-transfer channel was broken. The completion-text check would still pass and we'd never notice. After: - decode (kv_consumer) refuses to prefill locally; if KV bytes don't arrive from prefill over libfabric, the decode request hangs and the orchestrator's curl times out → hard failure. - After the completion succeeds, scrape /metrics from both servers and assert: prefill: vllm:prompt_tokens_total >= 6 (it did the prefill) decode: vllm:prompt_tokens_total == 0 (it did NOT prefill — proof KV came over the wire) decode: vllm:generation_tokens_total >= 1 (decode produced tokens) The decode.prompt_tokens_total == 0 line is the smoking-gun assertion: the only way decode can generate tokens for a 6-token prompt without seeing those 6 tokens locally is if the prefill node shipped its KV cache to decode over libfabric/EFA. Signed-off-by: Yadan Wei <yadanwei@amazon.com> * test(efa): drop unused pytest import flagged by ruff after main merge Merge of main into vllm-efa-test re-introduced 'import pytest' from upstream, but no @pytest.fixture / pytest.* call exists in this file — the verbose _step refactor removed the only usage. ruff hook fails CI's check-changes job; remove the import to make pre-commit green. Signed-off-by: Yadan Wei <yadanwei@amazon.com> * test(efa): drop stale warn=True kwarg on _step nccl_allreduce call The merge of main (PR #6114, 956819d) added warn=True to a run_on_container() call. Our verbose-_step refactor (9522545) wraps that same call inside a local _step() helper which doesn't accept warn, so the merge produced _step(..., warn=True) — TypeError at runtime. _step already prints stdout/stderr/exit on success and pytest captures the UnexpectedExit on failure, so warn=True is no longer needed (it was only useful when the broken DLAMI was masking real errors). Signed-off-by: Yadan Wei <yadanwei@amazon.com> * test(efa): wire NIXL kv_transfer_params handshake; revert to kv_both Root cause of the previous run's failure (decode.prompt_tokens=6 instead of 0): NixlConnector in vLLM 0.21.0 does NOT enforce kv_role at the engine level. Whether the decoder fetches remote KV vs re-prefills is driven entirely by the per-request `kv_transfer_params` dict — which our toy proxy was not shipping. So the kv_role=kv_consumer setup was a no-op and decode silently re-prefilled the full prompt locally, even while libfabric came up cleanly. Fixes: - toy_proxy_server.py: do the upstream-style 2-step handshake. Inject placeholder {do_remote_decode: True, ...} into the prefill body with max_tokens=1, read remote_engine_id/block_ids/host/port from prefill's response, then forward those into the decode request so D's scheduler uses the remote-pull path. - nixl_disagg_pd.sh + nixl_disagg_pd_decode.sh: revert kv_role to kv_both (matches upstream tests/v1/kv_connector/nixl_integration/ run_accuracy_test.sh). Also export VLLM_NIXL_SIDE_CHANNEL_HOST set to the box's primary IP so the cross-host side channel is reachable. - nixl_disagg_pd.sh metrics assertion: relax decode.prompt_tokens from ==0 to <=1. With the handshake, decode legitimately processes the last token before generation; only the full-prompt re-prefill (==6) indicates the connector failed. Reference: upstream's tests/v1/kv_connector/nixl_integration/ toy_proxy_server.py at vllm-project/vllm. Signed-off-by: Yadan Wei <yadanwei@amazon.com> * test(efa): always dump prefill/proxy/decode logs on NIXL test failure Wrap the orchestrator _step in try/finally so the prefill/proxy/decode log dumps fire even when the orchestrator script raises UnexpectedExit. Today's failure (decode.prompt_tokens=6 — KV transfer over libfabric did not happen) is opaque without those logs because we can't tell whether: - prefill returned populated kv_transfer_params or null, - the proxy successfully extracted remote_engine_id/block_ids/etc, - decode's NixlConnector saw the do_remote_prefill flag. The proxy log in particular is on the master container and would show which side of the handshake silently dropped, but the previous code only dumped on success — exactly when we don't need the diagnostics. Diagnostic-only change; no behavior modifications. Signed-off-by: Yadan Wei <yadanwei@amazon.com> * test(efa): align NIXL PD launch with upstream run_accuracy_test.sh Apply the launch flags that the upstream tests/v1/kv_connector/ nixl_integration/run_accuracy_test.sh uses on both prefill and decode: - --block-size 128 — must match across P and D for remote_block_ids to map correctly. OPT defaults to 16; upstream pins 128 explicitly so the lookup at scheduler.py works regardless of model. - VLLM_KV_CACHE_LAYOUT=HND — required by NixlConnector. Without it the attention backend can pick a layout NIXL doesn't support, silently falling back to local prefill. - kv_load_failure_policy=fail in kv_connector_extra_config — turns a missing/invalid KV handoff into a hard error instead of a silent re-prefill. Symptoms become loud (HTTP 5xx) instead of "passed but decode.prompt_tokens=6". - Drop UCX_NET_DEVICES=all on both — UCX-only env, no-op for the LIBFABRIC backend we use. Also enrich the proxy diagnostic so the next failure (if any) is trivially debuggable: dump the full kv_transfer_params dict and the sorted key list returned by prefill, so we can see at a glance whether all required keys (do_remote_prefill, remote_block_ids, remote_engine_id, remote_request_id, remote_host, remote_port, tp_size, remote_num_tokens) are present. Reference: vllm/tests/v1/kv_connector/nixl_integration/run_accuracy_test.sh at the v0.21.0 tag. Signed-off-by: Yadan Wei <yadanwei@amazon.com> * test(efa): assert NIXL transfer via vllm:nixl_xfer_time_seconds_count Replace the prompt_tokens_total / cache-hit assertion with a direct NixlConnector counter check. The previous run proved (via vLLM's own log line "KV Transfer metrics: Num successful transfers=1, Throughput 1003 MB/s") that NIXL+LIBFABRIC over EFA is fully working — the test was failing on a wrong proof metric. vllm:prompt_tokens_total counts prompt tokens regardless of whether the KV was reused, so it can never distinguish "KV pulled from remote" from "KV recomputed locally". vllm:nixl_xfer_time_seconds_count is a histogram counter that increments per successful NIXL transfer; > 0 proves blocks crossed the wire from prefill to decode. Pair with vllm:nixl_num_failed_transfers == 0 to fail loudly on transport errors. Drop the prefill.prompt_tokens assertion (no longer needed — the NIXL counter on decode is the authoritative proof). Signed-off-by: Yadan Wei <yadanwei@amazon.com> * test(efa): drop AMI pin, run PyTorch EFA test on test/efa/** changes Two related changes now that AWS DLAMI shipped a working AL2023 base image (matching nvidia-fabricmanager + driver pair as of 2026-05-21): 1. .github/scripts/efa/ec2_helpers.py — revert the ami-0d2923a2dd541bdeb us-west-2 pin and go back to aws_session.get_latest_ami() everywhere. The pin was a workaround for a 12-day window (May 8-19) when DLAMI bumped to driver 580.150 without a matching fabricmanager package, which broke cuInit on p4d (NVSwitch) and surfaced as misleading "aws-ofi-nccl is not working" failures. AWS fixed it in the 2026-05-21 build, verified by the 23 GB/s NCCL allreduce + working NIXL transfer in the passing CI run. 2. .github/workflows/pr-pytorch-ec2-cuda.yml — add efa-test-change to check-changes (paths: test/efa/**, .github/scripts/efa/**, .github/workflows/reusable-efa-tests.yml). Mirror the sanity-test fallback so efa-test runs against the prod PyTorch image when build-images is skipped (PRs that touch only test/efa/** without the docker context). This means changes to the EFA test fixture itself get verified on PyTorch too — not just vLLM. Signed-off-by: Yadan Wei <yadanwei@amazon.com> --------- Signed-off-by: Yadan Wei <yadanwei@amazon.com> Co-authored-by: Yadan Wei <yadanwei@amazon.com>
1 parent d1e6e28 commit fbfc1b9

11 files changed

Lines changed: 738 additions & 43 deletions

.github/scripts/efa/ec2_helpers.py

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -274,17 +274,37 @@ def launch_efa_instances(aws_session, ami_id, instance_type, key_name, sg_id, co
274274
)
275275

276276

277-
def setup_container(conn, image_uri, container_name):
278-
"""Pull image and start container with EFA devices and host networking."""
277+
def setup_container(conn, image_uri, container_name, entrypoint=None, cmd="bash"):
278+
"""Pull image and start container with EFA devices and host networking.
279+
280+
The container is launched detached (`-id`) and must stay alive long enough
281+
for the test to docker-exec into it. The right way to keep it alive depends
282+
on the image's entrypoint, which is image-specific — so the caller decides
283+
via `entrypoint` / `cmd` instead of this helper sniffing the URI:
284+
285+
- `entrypoint=None` (default): use the image's baked-in entrypoint and pass
286+
`cmd` as its argv. Suitable when the entrypoint sets up runtime state we
287+
need (e.g., PyTorch's entrypoint.sh sets LD_LIBRARY_PATH for CUDA
288+
forward-compat before `exec "$@"`, which NCCL+EFA later inherits). With
289+
`cmd="bash"`, `-id` keeps stdin open and bash blocks reading from it.
290+
- `entrypoint="/bin/bash"` + `cmd="-c 'sleep infinity'"`: override when the
291+
baked-in entrypoint can't accept a generic shell arg (e.g., vLLM's
292+
`dockerd_entrypoint.sh` execs `vllm serve "$@"` and would parse `bash`
293+
as a model tag). Once entrypoint is `/bin/bash`, the CMD becomes argv
294+
to bash, so an explicit `-c` payload is required — leaving CMD empty
295+
would inherit the image's CMD as bash args.
296+
"""
279297
devices = get_efa_devices(conn)
280298
device_args = " ".join(f"--device {d}" for d in devices)
299+
entrypoint_arg = f"--entrypoint {entrypoint}" if entrypoint else ""
281300

282301
conn.run(f"docker rm -f {container_name}", warn=True)
283302
conn.run(
284303
f"docker run --runtime=nvidia --gpus all -id "
285304
f"--name {container_name} --network host --ulimit memlock=-1:-1 "
305+
f"{entrypoint_arg} "
286306
f"{device_args} -v $HOME/test:/test -v /dev/shm:/dev/shm "
287-
f"{image_uri} bash"
307+
f"{image_uri} {cmd}"
288308
)
289309
LOGGER.info(f"Started container {container_name}")
290310

@@ -387,9 +407,18 @@ def release_eip(aws_session, alloc_id):
387407

388408

389409
@contextmanager
390-
def efa_instances(image_uri, instance_type="p4d.24xlarge", region=DEFAULT_REGION):
410+
def efa_instances(
411+
image_uri,
412+
instance_type="p4d.24xlarge",
413+
region=DEFAULT_REGION,
414+
container_entrypoint=None,
415+
container_cmd="bash",
416+
):
391417
"""Context manager that launches 2 EFA instances, sets up containers + SSH, and cleans up.
392418
419+
`container_entrypoint` / `container_cmd` are forwarded to setup_container — see
420+
its docstring for when overriding the entrypoint is appropriate.
421+
393422
Yields (master_conn, worker_conn, aws_session) where connections are to the EC2 hosts.
394423
"""
395424
aws_session = AWSSessionManager(region=region)
@@ -463,8 +492,20 @@ def efa_instances(image_uri, instance_type="p4d.24xlarge", region=DEFAULT_REGION
463492
master_conn.run(f"docker pull {image_uri}")
464493
worker_conn.run(f"docker pull {image_uri}")
465494

466-
setup_container(master_conn, image_uri, MASTER_CONTAINER_NAME)
467-
setup_container(worker_conn, image_uri, WORKER_CONTAINER_NAME)
495+
setup_container(
496+
master_conn,
497+
image_uri,
498+
MASTER_CONTAINER_NAME,
499+
entrypoint=container_entrypoint,
500+
cmd=container_cmd,
501+
)
502+
setup_container(
503+
worker_conn,
504+
image_uri,
505+
WORKER_CONTAINER_NAME,
506+
entrypoint=container_entrypoint,
507+
cmd=container_cmd,
508+
)
468509

469510
setup_master_ssh(master_conn)
470511
worker_private_ip = get_private_ip(aws_session, worker_id)

.github/workflows/pr-pytorch-ec2-cuda.yml

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ jobs:
105105
build-change: ${{ steps.changes.outputs.build-change }}
106106
sanity-test-change: ${{ steps.changes.outputs.sanity-test-change }}
107107
telemetry-test-change: ${{ steps.changes.outputs.telemetry-test-change }}
108+
efa-test-change: ${{ steps.changes.outputs.efa-test-change }}
108109
steps:
109110
- name: Checkout code
110111
uses: actions/checkout@v5
@@ -135,6 +136,10 @@ jobs:
135136
- "test/sanity/**"
136137
telemetry-test-change:
137138
- "test/telemetry/**"
139+
efa-test-change:
140+
- "test/efa/**"
141+
- ".github/scripts/efa/**"
142+
- ".github/workflows/reusable-efa-tests.yml"
138143
139144
# ============================================================
140145
# Build runtime image
@@ -355,12 +360,23 @@ jobs:
355360
# EFA integration test (2x p4d.24xlarge, NCCL over EFA)
356361
# ============================================================
357362
efa-test:
358-
needs: [build-images, sanity-test, security-test, unit-test]
359-
if: success()
363+
needs: [check-changes, build-images, sanity-test, security-test, unit-test, load-config]
364+
# Run when the docker context, EFA test code, or this workflow changed.
365+
# Falls back to the prod image when build-images is skipped, mirroring
366+
# sanity-test/single-gpu-test.
367+
if: |
368+
always() && !failure() && !cancelled() &&
369+
(needs.check-changes.outputs.build-change == 'true' || needs.check-changes.outputs.efa-test-change == 'true') &&
370+
(needs.build-images.result == 'success' || needs.build-images.result == 'skipped')
371+
# Per-caller queue: new pushes to this PR cancel its own pending efa-test;
372+
# different PRs / workflows don't displace each other on the global lock.
373+
concurrency:
374+
group: ${{ github.workflow }}-efa-${{ github.event.pull_request.number }}
375+
cancel-in-progress: true
360376
uses: ./.github/workflows/reusable-efa-tests.yml
361377
with:
362-
image-uri: ${{ needs.build-images.outputs.runtime-image-uri }}
363-
aws-account-id: ${{ vars.CI_AWS_ACCOUNT_ID }}
378+
image-uri: ${{ needs.build-images.result == 'success' && needs.build-images.outputs.runtime-image-uri || format('{0}.dkr.ecr.{1}.amazonaws.com/{2}', vars.PROD_AWS_ACCOUNT_ID, vars.AWS_REGION, needs.load-config.outputs.prod-image) }}
379+
aws-account-id: ${{ needs.build-images.result == 'success' && vars.CI_AWS_ACCOUNT_ID || vars.PROD_AWS_ACCOUNT_ID }}
364380
aws-region: ${{ vars.AWS_REGION }}
365381

366382
# ============================================================

.github/workflows/pr-vllm-ec2.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ on:
88
paths:
99
- ".github/config/image/vllm-ec2.yml"
1010
- ".github/workflows/pr-vllm-ec2.yml"
11+
- ".github/workflows/reusable-efa-tests.yml"
1112
- ".github/workflows/reusable-vllm-upstream-tests.yml"
1213
- "docker/vllm/Dockerfile"
1314
- "scripts/common/**"
1415
- "scripts/telemetry/**"
1516
- "scripts/vllm/**"
17+
- "test/efa/**"
1618
- "test/sanity/**"
1719
- "test/telemetry/**"
1820
- "test/vllm/**"
@@ -102,6 +104,7 @@ jobs:
102104
build-change: ${{ steps.changes.outputs.build-change }}
103105
sanity-test-change: ${{ steps.changes.outputs.sanity-test-change }}
104106
telemetry-test-change: ${{ steps.changes.outputs.telemetry-test-change }}
107+
efa-test-change: ${{ steps.changes.outputs.efa-test-change }}
105108
steps:
106109
- name: Checkout DLC source
107110
uses: actions/checkout@v5
@@ -133,6 +136,9 @@ jobs:
133136
- "test/sanity/**"
134137
telemetry-test-change:
135138
- "test/telemetry/**"
139+
efa-test-change:
140+
- ".github/workflows/reusable-efa-tests.yml"
141+
- "test/efa/**"
136142
137143
build-image:
138144
needs: [check-changes, load-config]
@@ -241,3 +247,33 @@ jobs:
241247
setup-script: test/vllm/scripts/vllm_test_setup.sh
242248
example-test-script: test/vllm/scripts/vllm_ec2_examples_test.sh
243249
secrets: inherit
250+
251+
# ============================================================
252+
# EFA integration test (2x p4d.24xlarge, NCCL over EFA).
253+
# Reuses the shared EFA test (test/efa/test_efa.py); the test's
254+
# setup_nccl_tests.sh builds nccl-tests in-container against the
255+
# nvidia-nccl-cu12 wheel that ships in vllm/vllm-openai.
256+
# Runs when the image was rebuilt OR when EFA test files changed
257+
# (in which case it tests against the prod image).
258+
# ============================================================
259+
efa-test:
260+
needs: [check-changes, build-image, sanity-test, security-test, load-config]
261+
if: |
262+
always() && !failure() && !cancelled() &&
263+
(needs.build-image.result == 'success' || needs.check-changes.outputs.efa-test-change == 'true')
264+
# Per-caller queue: new pushes to this PR cancel its own pending efa-test;
265+
# different PRs / workflows don't displace each other on the global lock.
266+
concurrency:
267+
group: ${{ github.workflow }}-efa-${{ github.event.pull_request.number }}
268+
cancel-in-progress: true
269+
uses: ./.github/workflows/reusable-efa-tests.yml
270+
with:
271+
image-uri: ${{ needs.build-image.result == 'success' && needs.build-image.outputs.ci-image || format('{0}.dkr.ecr.{1}.amazonaws.com/{2}', vars.PROD_AWS_ACCOUNT_ID, vars.AWS_REGION, needs.load-config.outputs.prod-image) }}
272+
aws-account-id: ${{ needs.build-image.result == 'success' && vars.CI_AWS_ACCOUNT_ID || vars.PROD_AWS_ACCOUNT_ID }}
273+
aws-region: ${{ vars.AWS_REGION }}
274+
run-nixl-tests: true
275+
# vLLM's dockerd_entrypoint.sh execs `vllm serve "$@"` and would parse
276+
# any CMD as a model tag — override entrypoint to bash and keep the
277+
# container alive for docker exec.
278+
container-entrypoint: /bin/bash
279+
container-cmd: -c 'sleep infinity'

.github/workflows/reusable-efa-tests.yml

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,29 @@ on:
2323
required: false
2424
type: string
2525
default: 'p4d.24xlarge'
26+
run-nixl-tests:
27+
description: 'Run NIXL libfabric smoke + disaggregated PD tests (vLLM only)'
28+
required: false
29+
type: boolean
30+
default: false
31+
container-entrypoint:
32+
description: |
33+
Override the test container's entrypoint (passed to `docker run --entrypoint`).
34+
Leave blank to use the image's baked-in entrypoint (PyTorch default).
35+
Set to `/bin/bash` for images whose entrypoint can't accept a generic
36+
shell arg (e.g., vLLM's `vllm serve "$@"`).
37+
required: false
38+
type: string
39+
default: ''
40+
container-cmd:
41+
description: |
42+
CMD passed to the test container. Defaults to `bash` (works with the
43+
image's entrypoint + `-id` stdin). When `container-entrypoint` is set
44+
to `/bin/bash`, this becomes argv to bash and must be an explicit
45+
payload such as `-c 'sleep infinity'`.
46+
required: false
47+
type: string
48+
default: 'bash'
2649

2750
# Serialize EFA tests across all PRs to avoid p4d capacity contention.
2851
# Only one EFA test runs at a time globally; subsequent runs queue (no cancel).
@@ -48,9 +71,16 @@ jobs:
4871
uv pip install -r test/requirements.txt
4972
5073
- name: Run EFA integration test
74+
env:
75+
TEST_IMAGE_URI: ${{ inputs.image-uri }}
76+
EFA_INSTANCE_TYPE: ${{ inputs.efa-instance-type }}
77+
RUN_NIXL_TESTS: ${{ inputs.run-nixl-tests && '1' || '0' }}
78+
# Quote-safe via env: container-cmd may contain spaces and quotes
79+
# (e.g., `-c 'sleep infinity'`), which would tokenize incorrectly
80+
# if assigned inline on the shell command line.
81+
EFA_CONTAINER_ENTRYPOINT: ${{ inputs.container-entrypoint }}
82+
EFA_CONTAINER_CMD: ${{ inputs.container-cmd }}
5183
run: |
5284
source .venv/bin/activate
5385
PYTHONPATH=$(pwd)/test:$(pwd)/.github/scripts:$PYTHONPATH \
54-
TEST_IMAGE_URI=${{ inputs.image-uri }} \
55-
EFA_INSTANCE_TYPE=${{ inputs.efa-instance-type }} \
5686
python -m pytest test/efa/test_efa.py -vs -rA --tb=long

test/efa/scripts/nccl_allreduce.sh

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ set -ex
66
NUM_HOSTS_FILE=$1
77
NUM_HOSTS=$2
88

9-
if [[ -z "${CUDA_HOME}" ]]; then
10-
echo "CUDA_HOME variable is empty, please define it in dockerfile"
11-
exit 1
12-
fi
9+
# Default CUDA_HOME for images that don't export it (vLLM Ubuntu).
10+
# PyTorch DLCs already set this in the Dockerfile so this is a no-op there.
11+
: "${CUDA_HOME:=/usr/local/cuda}"
12+
export CUDA_HOME
1313

1414
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
1515
INSTANCE_TYPE=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -v http://169.254.169.254/latest/meta-data/instance-type)
@@ -52,6 +52,28 @@ check_efa_nccl_all_reduce_performance(){
5252
fi
5353
}
5454

55+
# Capture diagnostics to a file we cat at the very end. invoke/Fabric truncate
56+
# the .stdout of a failing remote command to the last few KB, so anything
57+
# printed before mpirun gets dropped. Stage it through a file and dump after
58+
# the validators run.
59+
DIAG_LOG="/test/efa/logs/diagnostics.log"
60+
{
61+
echo "==================== EFA / NCCL diagnostics ===================="
62+
echo "--- nvidia-smi ---"
63+
nvidia-smi -L || true
64+
echo "--- libnccl resolution ---"
65+
ldconfig -p | grep libnccl || echo "(no libnccl in ldconfig)"
66+
echo "--- ldd all_reduce_perf ---"
67+
ldd /usr/local/bin/all_reduce_perf 2>&1 | grep -E "nccl|cuda|fabric|not found" || true
68+
echo "--- libfabric provider list ---"
69+
fi_info -p efa 2>&1 | head -20 || true
70+
echo "--- aws-ofi-nccl plugin ---"
71+
ls -la /opt/amazon/ofi-nccl/lib*/libnccl-net*.so 2>&1 | head -5 || true
72+
echo "--- /etc/ld.so.conf.d ---"
73+
ls /etc/ld.so.conf.d/ 2>&1
74+
echo "==================== end diagnostics ===================="
75+
} > "${DIAG_LOG}" 2>&1
76+
5577
echo "Running all_reduce_perf test"
5678
mpirun -x FI_PROVIDER="efa" -x FI_EFA_FORK_SAFE=1 -n $NODES -N $GPU_COUNT --hostfile $NUM_HOSTS_FILE \
5779
-x NCCL_DEBUG=INFO ${USE_DEVICE_RDMA_ARG} -x NCCL_PROTO=simple -x NCCL_ALGO=ring -x RDMAV_FORK_SAFE=1 \
@@ -66,5 +88,27 @@ else
6688
echo "check_efa_nccl_all_reduce failed"
6789
fi
6890

91+
# Dump training log first (it can be huge on success and we don't need it on
92+
# failure — mpirun's stdout was already captured), then the most actionable
93+
# diagnostics LAST so they survive Fabric's stdout truncation.
94+
echo "==================== BEGIN ${TRAINING_LOG} ===================="
95+
cat "${TRAINING_LOG}" 2>/dev/null || echo "(log file missing)"
96+
echo "==================== END ${TRAINING_LOG} ===================="
97+
98+
echo "==================== BEGIN ${DIAG_LOG} ===================="
99+
cat "${DIAG_LOG}" 2>/dev/null || echo "(diagnostics file missing)"
100+
echo "==================== END ${DIAG_LOG} ===================="
101+
102+
# These are the smallest, highest-signal probes — placed last so the
103+
# truncated tail Fabric retains will always show them.
104+
echo "==================== final probes ===================="
105+
echo "--- ldd all_reduce_perf (libnccl resolution) ---"
106+
ldd /usr/local/bin/all_reduce_perf 2>&1 | grep -E "nccl|cuda|fabric|not found" || true
107+
echo "--- libnccl SONAMES on system ---"
108+
find / -name 'libnccl.so*' 2>/dev/null | head -10
109+
echo "--- aws-ofi-nccl plugin paths ---"
110+
ls -la /opt/amazon/ofi-nccl/lib*/libnccl-net*.so 2>&1
111+
echo "==================== end ===================="
112+
69113
validate_all_reduce_performance_logs
70114
check_efa_nccl_all_reduce_performance

0 commit comments

Comments
 (0)