[CI][AMD] Improve Kubernetes failure diagnostics (#52264)
Signed-off-by: Andreas Karatzas <akaratza@amd.com> Signed-off-by: Andreas Karatzas <Andreas.Karatzas@amd.com> Co-authored-by: OpenAI Codex <codex@openai.com>
Andreas Karatzas committed
Aug 17, 2026 at 18:09 UTC
60c3a31b12b0a735baa79684885391747771ec64
1 file changed
+448
-110
.buildkite/scripts/hardware_ci/run-amd-test.sh
+448
-110
@@ -44,11 +44,14 @@ amd_diagnostics_dir="${VLLM_CI_DIAGNOSTICS_DIR:-artifacts/amd-gpu-diagnostics}"
44
amd_diagnostics_checkout_root="${BUILDKITE_BUILD_CHECKOUT_PATH:-$(pwd -P)}"
45
amd_diagnostics_execution_mode="${VLLM_CI_EXECUTION_MODE:-single-node}"
46
amd_diagnostics_test_group="${VLLM_TEST_GROUP_NAME:-${BUILDKITE_STEP_KEY:-unknown}}"
47
-amd_diagnostics_k8s_node_name="${VLLM_CI_K8S_NODE_NAME:-}"
47
+amd_diagnostics_workspace="${VLLM_CI_WORKSPACE:-/vllm-workspace}"
48
+amd_diagnostics_pod_name="${VLLM_CI_K8S_POD_NAME:-${POD_NAME:-${HOSTNAME:-}}}"
49
+amd_diagnostics_k8s_namespace="${VLLM_CI_K8S_NAMESPACE:-${POD_NAMESPACE:-unknown}}"
50
+amd_diagnostics_k8s_node_name="${VLLM_CI_K8S_NODE_NAME:-${NODE_NAME:-}}"
51
amd_diagnostics_collected=0
52
+amd_diagnostics_memory_events_path=""
53
amd_diagnostics_probe_budget_seconds=25
54
amd_diagnostics_command_timeout_seconds=5
51
-amd_diagnostics_upload_timeout_seconds=20
55
if [[ " ${PYTEST_ADDOPTS:-} " != *" --color"* ]]; then
56
PYTEST_ADDOPTS="${PYTEST_ADDOPTS:+${PYTEST_ADDOPTS} }--color=yes"
57
fi
@@ -101,6 +104,8 @@ clear_ci_orchestration_env() {
104
VLLM_CI_ARTIFACT_GLOB \
105
VLLM_CI_ARTIFACT_CHECKSUM_GLOB \
106
VLLM_CI_EXPECTED_GPU_COUNT \
107
+ VLLM_CI_K8S_POD_NAME \
108
+ VLLM_CI_K8S_NAMESPACE \
109
VLLM_CI_K8S_NODE_NAME \
110
VLLM_CI_USE_ARTIFACTS \
111
VLLM_CI_RESULTS_ROOT \
@@ -577,7 +582,24 @@ is_multi_node() {
582
return 1
583
}
584
580
-run_amd_diagnostic() {
585
+append_failure_diagnostic_section() {
586
+ local log_file=$1
587
+ local title=$2
588
+
589
+ printf '\n===============================================================================\n' \
590
+ >> "${log_file}"
591
+ printf '%s\n' "${title}" >> "${log_file}"
592
+ printf '===============================================================================\n' \
593
+ >> "${log_file}"
594
+}
595
+
596
+append_failure_diagnostic_note() {
597
+ local log_file=$1
598
+ shift
599
+ printf '%s\n' "$@" >> "${log_file}"
600
+}
601
+
602
+run_failure_diagnostic() {
603
local log_file=$1
604
local probe_deadline=$2
605
shift 2
@@ -589,23 +611,285 @@ run_amd_diagnostic() {
611
printf '\n$'
612
printf ' %q' "$@"
613
printf '\n'
592
- } | tee -a "${log_file}"
614
+ } >> "${log_file}"
615
616
+ if ! command -v timeout >/dev/null 2>&1; then
617
+ echo "[timeout unavailable; bounded diagnostic command skipped]" \
618
+ >> "${log_file}"
619
+ return 0
620
+ fi
621
if [[ "${remaining_seconds}" -le 0 ]]; then
622
echo "[diagnostic probe budget exhausted; command skipped]" \
596
- | tee -a "${log_file}"
623
+ >> "${log_file}"
624
return 0
625
fi
626
if [[ "${remaining_seconds}" -lt "${command_timeout_seconds}" ]]; then
627
command_timeout_seconds=${remaining_seconds}
628
fi
629
603
- timeout --kill-after=1s "${command_timeout_seconds}s" "$@" 2>&1 \
604
- | tee -a "${log_file}"
605
- command_status=${PIPESTATUS[0]}
630
+ timeout --kill-after=1s "${command_timeout_seconds}s" "$@" \
631
+ >> "${log_file}" 2>&1
632
+ command_status=$?
633
if [[ "${command_status}" -ne 0 ]]; then
634
echo "[diagnostic command exited with status ${command_status}]" \
608
- | tee -a "${log_file}"
635
+ >> "${log_file}"
636
+ fi
637
+ return 0
638
+}
639
+
640
+append_failure_diagnostic_file() {
641
+ local log_file=$1
642
+ local label=$2
643
+ local path=$3
644
+ local command_status=0
645
+
646
+ printf '\n%s:\n' "${label}" >> "${log_file}"
647
+ printf '$ cat %q\n' "${path}" >> "${log_file}"
648
+ if [[ ! -r "${path}" ]]; then
649
+ echo "[file unavailable]" >> "${log_file}"
650
+ return 0
651
+ fi
652
+
653
+ cat "${path}" >> "${log_file}" 2>&1
654
+ command_status=$?
655
+ if [[ "${command_status}" -ne 0 ]]; then
656
+ echo "[diagnostic file read exited with status ${command_status}]" \
657
+ >> "${log_file}"
658
+ fi
659
+ return 0
660
+}
661
+
662
+decode_mountinfo_path() {
663
+ local value=$1
664
+
665
+ value=${value//\\040/ }
666
+ value=${value//\\011/$'\t'}
667
+ value=${value//\\012/$'\n'}
668
+ value=${value//\\134/\\}
669
+ printf '%s\n' "${value}"
670
+}
671
+
672
+resolve_current_cgroup_v2_dir() {
673
+ local cgroup_path=""
674
+ local mount_root=""
675
+ local mount_point=""
676
+ local relative_path=""
677
+ local candidate=""
678
+
679
+ cgroup_path=$(awk -F: '$2 == "" { print $3; exit }' \
680
+ /proc/self/cgroup 2>/dev/null)
681
+ [[ -n "${cgroup_path}" ]] || return 1
682
+
683
+ while IFS=$'\t' read -r mount_root mount_point; do
684
+ mount_root=$(decode_mountinfo_path "${mount_root}")
685
+ mount_point=$(decode_mountinfo_path "${mount_point}")
686
+ if [[ "${cgroup_path}" == "/" ]]; then
687
+ relative_path=""
688
+ elif [[ "${mount_root}" == "/" ]]; then
689
+ relative_path="${cgroup_path}"
690
+ elif [[ "${cgroup_path}" == "${mount_root}" ]]; then
691
+ relative_path=""
692
+ elif [[ "${cgroup_path}" == "${mount_root}/"* ]]; then
693
+ relative_path="/${cgroup_path#"${mount_root}/"}"
694
+ else
695
+ continue
696
+ fi
697
+
698
+ candidate="${mount_point%/}${relative_path}"
699
+ if [[ -d "${candidate}" ]]; then
700
+ printf '%s\n' "${candidate}"
701
+ return 0
702
+ fi
703
+ done < <(
704
+ awk '
705
+ {
706
+ for (i = 6; i <= NF; i++) {
707
+ if ($i == "-" && $(i + 1) == "cgroup2") {
708
+ print $4 "\t" $5
709
+ break
710
+ }
711
+ }
712
+ }
713
+ ' /proc/self/mountinfo 2>/dev/null
714
+ )
715
+ return 1
716
+}
717
+
718
+collect_cgroup_files() {
719
+ local log_file=$1
720
+ local title=$2
721
+ local cgroup_dir=$3
722
+ shift 3
723
+ local cgroup_file=""
724
+ local files_collected=0
725
+
726
+ append_failure_diagnostic_section "${log_file}" "${title}"
727
+ if [[ -z "${cgroup_dir}" || ! -d "${cgroup_dir}" ]]; then
728
+ append_failure_diagnostic_note "${log_file}" \
729
+ "resolved_cgroup_path=unavailable"
730
+ return 0
731
+ fi
732
+
733
+ append_failure_diagnostic_note "${log_file}" \
734
+ "resolved_cgroup_path=${cgroup_dir}"
735
+ for cgroup_file in "$@"; do
736
+ if [[ -r "${cgroup_dir}/${cgroup_file}" ]]; then
737
+ append_failure_diagnostic_file "${log_file}" "${cgroup_file}" \
738
+ "${cgroup_dir}/${cgroup_file}"
739
+ files_collected=$((files_collected + 1))
740
+ fi
741
+ done
742
+ if [[ "${files_collected}" -eq 0 ]]; then
743
+ append_failure_diagnostic_note "${log_file}" \
744
+ "[no requested cgroup files were readable]"
745
+ fi
746
+}
747
+
748
+collect_cgroup_diagnostics() {
749
+ local log_file=$1
750
+ local cgroup_dir=""
751
+
752
+ append_failure_diagnostic_section "${log_file}" "Cgroup membership"
753
+ append_failure_diagnostic_file "${log_file}" "current_process" \
754
+ /proc/self/cgroup
755
+ append_failure_diagnostic_file "${log_file}" "pid_namespace_init_process" \
756
+ /proc/1/cgroup
757
+
758
+ cgroup_dir=$(resolve_current_cgroup_v2_dir 2>/dev/null || true)
759
+ collect_cgroup_files "${log_file}" \
760
+ "Cgroup v2 current-process resource state" "${cgroup_dir}" \
761
+ cgroup.type cgroup.events \
762
+ memory.current memory.peak memory.max memory.high memory.low memory.min \
763
+ memory.oom.group memory.swap.current memory.swap.max \
764
+ memory.events memory.events.local memory.swap.events memory.stat \
765
+ memory.pressure \
766
+ cpu.max cpu.max.burst cpu.weight cpu.stat cpu.pressure \
767
+ cpuset.cpus cpuset.cpus.effective cpuset.mems cpuset.mems.effective \
768
+ pids.current pids.max pids.events pids.events.local \
769
+ io.stat io.max io.weight io.pressure
770
+ if [[ -n "${cgroup_dir}" && -r "${cgroup_dir}/memory.events" ]]; then
771
+ amd_diagnostics_memory_events_path="${cgroup_dir}/memory.events"
772
+ fi
773
+}
774
+
775
+collect_process_diagnostics() {
776
+ local log_file=$1
777
+ local probe_deadline=$2
778
+ local shell_proc_dir="/proc/$$"
779
+
780
+ append_failure_diagnostic_section "${log_file}" \
781
+ "Runner process constraints and PID-namespace-visible processes"
782
+ append_failure_diagnostic_file "${log_file}" "diagnostic_shell_limits" \
783
+ "${shell_proc_dir}/limits"
784
+ if command -v awk >/dev/null 2>&1; then
785
+ # shellcheck disable=SC2016 # The expression is evaluated by awk.
786
+ run_failure_diagnostic "${log_file}" "${probe_deadline}" awk '
787
+ $1 ~ /^(Pid:|PPid:|Threads:|VmPeak:|VmSize:|VmHWM:|VmRSS:|RssAnon:|RssFile:|RssShmem:|Cpus_allowed_list:|Mems_allowed_list:|voluntary_ctxt_switches:|nonvoluntary_ctxt_switches:)$/ {
788
+ print
789
+ }
790
+ ' "${shell_proc_dir}/status"
791
+ fi
792
+ if command -v nproc >/dev/null 2>&1; then
793
+ run_failure_diagnostic "${log_file}" "${probe_deadline}" nproc
794
+ fi
795
+ run_failure_diagnostic "${log_file}" "${probe_deadline}" \
796
+ /bin/bash -c 'ulimit -a'
797
+ if command -v ps >/dev/null 2>&1 && command -v head >/dev/null 2>&1; then
798
+ append_failure_diagnostic_note "${log_file}" \
799
+ "process_snapshot=top_100_by_rss"
800
+ run_failure_diagnostic "${log_file}" "${probe_deadline}" \
801
+ /bin/bash -c \
802
+ 'ps -eo pid,ppid,stat,etimes,nlwp,pcpu,rss,vsz,comm --sort=-rss | head -n 101'
803
+ fi
804
+ return 0
805
+}
806
+
807
+collect_mount_diagnostic() {
808
+ local log_file=$1
809
+ local probe_deadline=$2
810
+ local label=$3
811
+ local path=$4
812
+
813
+ printf '\n%s (%s):\n' "${label}" "${path}" >> "${log_file}"
814
+ if [[ ! -e "${path}" ]]; then
815
+ echo "[path unavailable]" >> "${log_file}"
816
+ return 0
817
+ fi
818
+ if command -v findmnt >/dev/null 2>&1; then
819
+ run_failure_diagnostic "${log_file}" "${probe_deadline}" findmnt \
820
+ -n -T "${path}" -o TARGET,FSTYPE,VFS-OPTIONS
821
+ fi
822
+ if command -v df >/dev/null 2>&1; then
823
+ run_failure_diagnostic "${log_file}" "${probe_deadline}" df -h \
824
+ --output=fstype,size,used,avail,pcent,itotal,iused,iavail,ipcent,target \
825
+ -- "${path}"
826
+ fi
827
+ return 0
828
+}
829
+
830
+collect_execution_resource_diagnostics() {
831
+ local log_file=$1
832
+ local probe_deadline=$2
833
+
834
+ # free and /proc/meminfo generally describe the node from a pod. Cgroups are
835
+ # the live source for this container's resource limits, usage, and events.
836
+ collect_cgroup_diagnostics "${log_file}"
837
+ collect_process_diagnostics "${log_file}" "${probe_deadline}"
838
+
839
+ append_failure_diagnostic_section "${log_file}" \
840
+ "Mount-visible filesystem capacity"
841
+ append_failure_diagnostic_note "${log_file}" \
842
+ "These values are filesystem views, not Kubernetes ephemeral-storage quotas."
843
+ collect_mount_diagnostic "${log_file}" "${probe_deadline}" \
844
+ "checkout" "${amd_diagnostics_checkout_root}"
845
+ if is_native_runtime; then
846
+ collect_mount_diagnostic "${log_file}" "${probe_deadline}" \
847
+ "native workspace emptyDir" "${amd_diagnostics_workspace}"
848
+ else
849
+ collect_mount_diagnostic "${log_file}" "${probe_deadline}" \
850
+ "test workspace (if mounted)" "${amd_diagnostics_workspace}"
851
+ fi
852
+ collect_mount_diagnostic "${log_file}" "${probe_deadline}" \
853
+ "temporary directory" "${TMPDIR:-/tmp}"
854
+ if [[ "${TMPDIR:-/tmp}" != "/tmp" ]]; then
855
+ collect_mount_diagnostic "${log_file}" "${probe_deadline}" \
856
+ "system temporary directory" /tmp
857
+ fi
858
+ if is_native_runtime; then
859
+ collect_mount_diagnostic "${log_file}" "${probe_deadline}" \
860
+ "shared-memory emptyDir" /dev/shm
861
+ else
862
+ collect_mount_diagnostic "${log_file}" "${probe_deadline}" \
863
+ "shared-memory mount" /dev/shm
864
+ fi
865
+ if [[ -n "${HF_HOME:-}" ]]; then
866
+ collect_mount_diagnostic "${log_file}" "${probe_deadline}" \
867
+ "Hugging Face cache" "${HF_HOME}"
868
+ fi
869
+ return 0
870
+}
871
+
872
+collect_amd_device_nodes() {
873
+ local log_file=$1
874
+ local probe_deadline=$2
875
+ local device=""
876
+ local device_count=0
877
+
878
+ for device in /dev/kfd /dev/dri/renderD*; do
879
+ if [[ ! -e "${device}" ]]; then
880
+ continue
881
+ fi
882
+ device_count=$((device_count + 1))
883
+ if command -v stat >/dev/null 2>&1; then
884
+ run_failure_diagnostic "${log_file}" "${probe_deadline}" stat -Lc \
885
+ '%n type=%F mode=%a owner=%u:%g device=%t:%T' -- "${device}"
886
+ else
887
+ append_failure_diagnostic_note "${log_file}" "device=${device}"
888
+ fi
889
+ done
890
+ if [[ "${device_count}" -eq 0 ]]; then
891
+ append_failure_diagnostic_note "${log_file}" \
892
+ "No /dev/kfd or /dev/dri/renderD* device nodes are visible."
893
fi
894
return 0
895
}
@@ -617,30 +901,55 @@ collect_rocm_failure_diagnostics() {
901
local parallel_job="${BUILDKITE_PARALLEL_JOB:-0}"
902
local diagnostics_relative_path=""
903
local diagnostics_path=""
620
- local upload_root="${amd_diagnostics_checkout_root}"
621
- local upload_path=""
904
+ local diagnostics_parent=""
905
+ local checkout_real=""
906
+ local diagnostics_parent_real=""
907
local exit_signal=""
908
local probe_deadline=0
909
+ local runtime="single-node-docker"
910
+ local diagnostics_scope="outer-runner-after-test-container-exit"
911
+ local identity_label="runner"
912
+ local identity_value="${HOSTNAME:-unknown}"
913
+ local k8s_pod="unknown"
914
+ local k8s_namespace="${amd_diagnostics_k8s_namespace}"
915
+ local oom_kill_count=""
916
+ local summary_identity_label="Runner"
917
+ local -a summary_rows=()
918
919
if [[ "${amd_diagnostics_collected}" == "1" ]]; then
920
return 0
921
fi
922
amd_diagnostics_collected=1
923
630
- if [[ "${amd_diagnostics_expected_gpu_count}" == "0" ]]; then
631
- echo "Skipping AMD GPU diagnostics for a CPU-only job."
632
- return 0
924
+ if is_native_runtime; then
925
+ runtime="native-kubernetes"
926
+ diagnostics_scope="current-container-cgroup-and-namespaces"
927
+ if [[ -n "${amd_diagnostics_pod_name}" ]]; then
928
+ identity_label="pod"
929
+ identity_value="${amd_diagnostics_pod_name}"
930
+ k8s_pod="${amd_diagnostics_pod_name}"
931
+ else
932
+ identity_label="container_hostname"
933
+ fi
934
+ elif [[ "${amd_diagnostics_execution_mode}" == "multi-node" ]]; then
935
+ runtime="multi-node-docker"
936
fi
634
-
635
- if ! command -v timeout >/dev/null 2>&1; then
636
- echo "WARNING: timeout is unavailable; skipping failure diagnostics rather than risking a hung job."
637
- return 0
937
+ if [[ "${k8s_namespace}" == "unknown" \
938
+ && -r /var/run/secrets/kubernetes.io/serviceaccount/namespace ]]; then
939
+ k8s_namespace=$(
940
+ tr -d '\r\n' \
941
+ < /var/run/secrets/kubernetes.io/serviceaccount/namespace 2>/dev/null
942
+ )
943
fi
944
945
job_id="${job_id//[^A-Za-z0-9_.-]/_}"
946
retry_count="${retry_count//[^A-Za-z0-9_.-]/_}"
947
parallel_job="${parallel_job//[^A-Za-z0-9_.-]/_}"
948
949
+ if [[ "${exit_code}" -gt 128 && "${exit_code}" -le 192 ]]; then
950
+ exit_signal=$(kill -l "$((exit_code - 128))" 2>/dev/null || true)
951
+ fi
952
+
953
# Buildkite keeps the supplied artifact path. Restrict the configurable
954
# directory to a checkout-relative path so the UI shows a clean artifact key.
955
if [[ -z "${amd_diagnostics_dir}" \
@@ -649,26 +958,32 @@ collect_rocm_failure_diagnostics() {
958
echo "WARNING: ignoring unsafe VLLM_CI_DIAGNOSTICS_DIR"
959
amd_diagnostics_dir="artifacts/amd-gpu-diagnostics"
960
fi
652
- diagnostics_relative_path="${amd_diagnostics_dir}/${job_id}/amd-gpu-diagnostics.log"
961
+ diagnostics_relative_path="${amd_diagnostics_dir}/${job_id}/diagnostics.log"
962
diagnostics_path="${amd_diagnostics_checkout_root}/${diagnostics_relative_path}"
654
- upload_path="${diagnostics_relative_path}"
655
-
656
- if ! mkdir -p "$(dirname "${diagnostics_path}")" \
657
- || ! : > "${diagnostics_path}"; then
658
- diagnostics_path=$(mktemp -t amd-gpu-diagnostics.XXXXXX.log) || {
659
- echo "WARNING: unable to create an AMD GPU diagnostics log."
660
- return 0
661
- }
662
- upload_root=$(dirname "${diagnostics_path}")
663
- upload_path=$(basename "${diagnostics_path}")
664
- fi
665
-
666
- if [[ "${exit_code}" -gt 128 && "${exit_code}" -le 192 ]]; then
667
- exit_signal=$(kill -l "$((exit_code - 128))" 2>/dev/null || true)
963
+ diagnostics_parent=$(dirname "${diagnostics_path}")
964
+ checkout_real=$(readlink -m "${amd_diagnostics_checkout_root}" 2>/dev/null || true)
965
+ diagnostics_parent_real=$(readlink -m "${diagnostics_parent}" 2>/dev/null || true)
966
+ if [[ -z "${checkout_real}" || -z "${diagnostics_parent_real}" \
967
+ || ("${diagnostics_parent_real}" != "${checkout_real}" \
968
+ && "${diagnostics_parent_real}" != "${checkout_real}/"*) ]] \
969
+ || ! mkdir -p "${diagnostics_parent}" \
970
+ || [[ -L "${diagnostics_path}" ]] \
971
+ || ! (set -o noclobber; : > "${diagnostics_path}") 2>/dev/null; then
972
+ echo "WARNING: unable to create AMD CI diagnostics at ${diagnostics_relative_path}."
973
+ printf '\nAMD CI failure summary\n'
974
+ printf '%-22s | %s\n' \
975
+ "Field" "Value" \
976
+ "----------------------" "-----" \
977
+ "Exit code" "${exit_code}" \
978
+ "Signal" "${exit_signal:-none}" \
979
+ "Runtime" "${runtime}" \
980
+ "Pod/container" "${identity_value}" \
981
+ "Diagnostics artifact" "unavailable: ${diagnostics_relative_path}"
982
+ return 0
983
fi
984
985
{
671
- echo "AMD GPU failure diagnostics"
986
+ echo "AMD CI failure diagnostics"
987
echo "timestamp_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
988
echo "exit_code=${exit_code}"
989
echo "exit_signal=${exit_signal:-none}"
@@ -680,105 +995,130 @@ collect_rocm_failure_diagnostics() {
995
echo "retry_count=${retry_count}"
996
echo "parallel_job=${parallel_job}"
997
echo "execution_mode=${amd_diagnostics_execution_mode}"
998
+ echo "runtime=${runtime}"
999
+ echo "diagnostics_scope=${diagnostics_scope}"
1000
echo "expected_gpu_count=${amd_diagnostics_expected_gpu_count}"
1001
echo "probe_budget_seconds=${amd_diagnostics_probe_budget_seconds}"
1002
echo "command_timeout_seconds=${amd_diagnostics_command_timeout_seconds}"
686
- echo "upload_timeout_seconds=${amd_diagnostics_upload_timeout_seconds}"
1003
echo "agent_name=${BUILDKITE_AGENT_NAME:-unknown}"
1004
+ echo "container_hostname=${HOSTNAME:-unknown}"
1005
+ echo "k8s_pod=${k8s_pod}"
1006
+ echo "k8s_namespace=${k8s_namespace:-unknown}"
1007
echo "k8s_node=${amd_diagnostics_k8s_node_name:-unknown}"
1008
echo "kernel=$(uname -srmo 2>/dev/null || echo unknown)"
1009
} > "${diagnostics_path}"
1010
692
- echo "--- :rotating_light: AMD GPU failure diagnostics"
693
- cat "${diagnostics_path}"
694
-
1011
probe_deadline=$((SECONDS + amd_diagnostics_probe_budget_seconds))
1012
697
- # Collect the most useful evidence first so auxiliary probes cannot consume
698
- # the shared deadline before GPU health is captured.
699
- echo "AMD GPU diagnostics" | tee -a "${diagnostics_path}"
700
- if command -v amd-smi >/dev/null 2>&1; then
701
- run_amd_diagnostic "${diagnostics_path}" "${probe_deadline}" amd-smi version
702
- # Bus data identifies a card within the public node without publishing its
703
- # persistent UUID, serial number, or process list.
704
- run_amd_diagnostic "${diagnostics_path}" "${probe_deadline}" amd-smi static -b -g all
705
- run_amd_diagnostic "${diagnostics_path}" "${probe_deadline}" amd-smi metric -e -k -P -x -g all
706
- run_amd_diagnostic "${diagnostics_path}" "${probe_deadline}" amd-smi bad-pages -p -r -u -g all
707
- run_amd_diagnostic "${diagnostics_path}" "${probe_deadline}" amd-smi metric -p -t -u -m -v -g all
708
- run_amd_diagnostic "${diagnostics_path}" "${probe_deadline}" amd-smi xgmi -l -g all
709
- elif command -v rocm-smi >/dev/null 2>&1; then
710
- run_amd_diagnostic "${diagnostics_path}" "${probe_deadline}" rocm-smi \
711
- --showbus --showreplaycount --showrasinfo --showpagesinfo
712
- else
713
- echo "Neither amd-smi nor rocm-smi is available." \
714
- | tee -a "${diagnostics_path}"
715
- fi
1013
+ # Cgroup, process, and mount state is fast and most useful for explaining OOM,
1014
+ # throttling, PID exhaustion, and emptyDir pressure. Capture it before external
1015
+ # probes can consume the shared deadline.
1016
+ collect_execution_resource_diagnostics \
1017
+ "${diagnostics_path}" "${probe_deadline}"
1018
717
- echo "Node resource diagnostics (runner-visible scope)" \
718
- | tee -a "${diagnostics_path}"
719
- if command -v df >/dev/null 2>&1; then
720
- echo "checkout_filesystem:" | tee -a "${diagnostics_path}"
721
- if [[ -d "${amd_diagnostics_checkout_root}" ]]; then
722
- (
723
- cd "${amd_diagnostics_checkout_root}" || exit 0
724
- run_amd_diagnostic "${diagnostics_path}" "${probe_deadline}" df -h \
725
- --output=fstype,size,used,avail,pcent,itotal,iused,iavail,ipcent \
726
- -- .
727
- )
1019
+ append_failure_diagnostic_section "${diagnostics_path}" \
1020
+ "AMD GPU diagnostics (tool-visible devices)"
1021
+ if [[ "${amd_diagnostics_expected_gpu_count}" == "0" ]]; then
1022
+ append_failure_diagnostic_note "${diagnostics_path}" \
1023
+ "CPU-only job; AMD GPU probes skipped."
1024
+ else
1025
+ if [[ "${runtime}" == "native-kubernetes" ]]; then
1026
+ append_failure_diagnostic_note "${diagnostics_path}" \
1027
+ "SMI visibility may be broader than the Kubernetes device allocation."
1028
else
729
- echo "[checkout directory unavailable]" | tee -a "${diagnostics_path}"
1029
+ append_failure_diagnostic_note "${diagnostics_path}" \
1030
+ "Outer-runner SMI visibility may be broader than the test container."
1031
fi
731
- echo "tmp_filesystem:" | tee -a "${diagnostics_path}"
732
- run_amd_diagnostic "${diagnostics_path}" "${probe_deadline}" df -h \
733
- --output=fstype,size,used,avail,pcent,itotal,iused,iavail,ipcent \
734
- -- /tmp
1032
+ collect_amd_device_nodes "${diagnostics_path}" "${probe_deadline}"
1033
fi
736
- if command -v free >/dev/null 2>&1; then
737
- run_amd_diagnostic "${diagnostics_path}" "${probe_deadline}" free -h
738
- fi
739
- if [[ -r /sys/fs/cgroup/memory.events ]]; then
740
- run_amd_diagnostic "${diagnostics_path}" "${probe_deadline}" cat \
741
- /sys/fs/cgroup/memory.events
1034
+ if [[ "${amd_diagnostics_expected_gpu_count}" != "0" ]] \
1035
+ && command -v amd-smi >/dev/null 2>&1; then
1036
+ run_failure_diagnostic "${diagnostics_path}" "${probe_deadline}" \
1037
+ amd-smi version
1038
+ # Bus data identifies a card within the public node without publishing its
1039
+ # persistent UUID, serial number, or process list.
1040
+ run_failure_diagnostic "${diagnostics_path}" "${probe_deadline}" \
1041
+ amd-smi static -b -g all
1042
+ run_failure_diagnostic "${diagnostics_path}" "${probe_deadline}" \
1043
+ amd-smi metric -e -k -P -x -g all
1044
+ run_failure_diagnostic "${diagnostics_path}" "${probe_deadline}" \
1045
+ amd-smi bad-pages -p -r -u -g all
1046
+ run_failure_diagnostic "${diagnostics_path}" "${probe_deadline}" \
1047
+ amd-smi metric -p -t -u -m -v -g all
1048
+ run_failure_diagnostic "${diagnostics_path}" "${probe_deadline}" \
1049
+ amd-smi xgmi -l -g all
1050
+ elif [[ "${amd_diagnostics_expected_gpu_count}" != "0" ]] \
1051
+ && command -v rocm-smi >/dev/null 2>&1; then
1052
+ run_failure_diagnostic "${diagnostics_path}" "${probe_deadline}" rocm-smi \
1053
+ --showbus --showreplaycount --showrasinfo --showpagesinfo
1054
+ elif [[ "${amd_diagnostics_expected_gpu_count}" != "0" ]]; then
1055
+ append_failure_diagnostic_note "${diagnostics_path}" \
1056
+ "Neither amd-smi nor rocm-smi is available."
1057
fi
1058
744
- echo "Unauthenticated outer-runner network reachability diagnostics" \
745
- | tee -a "${diagnostics_path}"
1059
+ if [[ "${runtime}" == "native-kubernetes" ]]; then
1060
+ append_failure_diagnostic_section "${diagnostics_path}" \
1061
+ "Unauthenticated pod network reachability"
1062
+ else
1063
+ append_failure_diagnostic_section "${diagnostics_path}" \
1064
+ "Unauthenticated outer-runner network reachability"
1065
+ fi
1066
if command -v curl >/dev/null 2>&1; then
747
- run_amd_diagnostic "${diagnostics_path}" "${probe_deadline}" curl -q \
1067
+ run_failure_diagnostic "${diagnostics_path}" "${probe_deadline}" curl -q \
1068
--silent --location --proto '=https' --proto-redir '=https' \
1069
--max-redirs 3 --output /dev/null --connect-timeout 2 --max-time 4 \
1070
--write-out 'target=huggingface http_code=%{http_code} dns_done_s=%{time_namelookup} connect_done_s=%{time_connect} tls_done_s=%{time_appconnect} first_byte_s=%{time_starttransfer} total_s=%{time_total}\n' \
1071
https://huggingface.co/api/models/gpt2
752
- run_amd_diagnostic "${diagnostics_path}" "${probe_deadline}" curl -q \
1072
+ run_failure_diagnostic "${diagnostics_path}" "${probe_deadline}" curl -q \
1073
--silent --location --proto '=https' --proto-redir '=https' \
1074
--max-redirs 3 --output /dev/null --connect-timeout 2 --max-time 4 \
1075
--write-out 'target=github_git_smart_http http_code=%{http_code} dns_done_s=%{time_namelookup} connect_done_s=%{time_connect} tls_done_s=%{time_appconnect} first_byte_s=%{time_starttransfer} total_s=%{time_total}\n' \
1076
'https://github.com/vllm-project/ci-infra.git/info/refs?service=git-upload-pack'
1077
else
758
- echo "curl unavailable; reachability probes skipped." \
759
- | tee -a "${diagnostics_path}"
760
- fi
761
-
762
- if command -v buildkite-agent >/dev/null 2>&1 \
763
- && [[ -n "${BUILDKITE_JOB_ID:-}" ]]; then
764
- if (
765
- cd "${upload_root}" \
766
- && BUILDKITE_AGENT_DEBUG=false \
767
- BUILDKITE_AGENT_DEBUG_HTTP=false \
768
- BUILDKITE_AGENT_TRACE_HTTP=false \
769
- BUILDKITE_AGENT_LOG_LEVEL=error \
770
- timeout --kill-after=2s "${amd_diagnostics_upload_timeout_seconds}s" \
771
- buildkite-agent artifact upload "${upload_path}" \
772
- >/dev/null 2>&1
773
- ); then
774
- echo "Uploaded AMD GPU diagnostics artifact: ${upload_path}"
775
- else
776
- echo "WARNING: failed to upload AMD GPU diagnostics artifact: ${upload_path}"
1078
+ append_failure_diagnostic_note "${diagnostics_path}" \
1079
+ "curl unavailable; reachability probes skipped."
1080
+ fi
1081
+
1082
+ if [[ "${runtime}" == "native-kubernetes" \
1083
+ && -n "${amd_diagnostics_memory_events_path}" \
1084
+ && -r "${amd_diagnostics_memory_events_path}" ]]; then
1085
+ oom_kill_count=$(
1086
+ awk '$1 == "oom_kill" { print $2; exit }' \
1087
+ "${amd_diagnostics_memory_events_path}" 2>/dev/null
1088
+ )
1089
+ if [[ ! "${oom_kill_count}" =~ ^[0-9]+$ ]]; then
1090
+ oom_kill_count=""
1091
fi
778
- else
779
- echo "Buildkite agent unavailable; diagnostics artifact was not uploaded."
1092
fi
1093
1094
+ case "${identity_label}" in
1095
+ pod)
1096
+ summary_identity_label="Kubernetes pod"
1097
+ ;;
1098
+ container_hostname)
1099
+ summary_identity_label="Container hostname"
1100
+ ;;
1101
+ esac
1102
+
1103
+ summary_rows=(
1104
+ "Field" "Value"
1105
+ "----------------------" "-----"
1106
+ "Exit code" "${exit_code}"
1107
+ "Signal" "${exit_signal:-none}"
1108
+ "Runtime" "${runtime}"
1109
+ "${summary_identity_label}" "${identity_value}"
1110
+ )
1111
+ if [[ -n "${amd_diagnostics_k8s_node_name}" ]]; then
1112
+ summary_rows+=("Kubernetes node" "${amd_diagnostics_k8s_node_name}")
1113
+ fi
1114
+ if [[ -n "${oom_kill_count}" ]]; then
1115
+ summary_rows+=("Cgroup OOM kills" "${oom_kill_count}")
1116
+ fi
1117
+ summary_rows+=("Diagnostics artifact" "${diagnostics_relative_path}")
1118
+
1119
+ printf '\nAMD CI failure summary\n'
1120
+ printf '%-22s | %s\n' "${summary_rows[@]}"
1121
+
1122
return 0
1123
}
1124
@@ -788,9 +1128,6 @@ handle_pytest_exit() {
1128
echo "Pytest exit code 5 (no tests collected) - treating as success."
1129
exit 0
1130
fi
791
- if [[ "${exit_code}" -ne 0 ]]; then
792
- collect_rocm_failure_diagnostics "${exit_code}"
793
- fi
1131
exit "$exit_code"
1132
}
1133
@@ -959,7 +1296,7 @@ handle_amd_runner_exit() {
1296
}
1297
1298
# Catch both test failures and wrapper/setup failures. Runtime-specific cleanup
962
-# traps below replace this trap but call the same handler before cleaning up.
1299
+# traps below replace this trap and call the same handler after cleanup.
1300
trap handle_amd_runner_exit EXIT
1301
1302
###############################################################################
@@ -973,10 +1310,10 @@ if is_native_runtime; then
1310
# shellcheck disable=SC2317 # Called indirectly by the EXIT trap.
1311
cleanup_native_workspace() {
1312
local exit_code=$?
976
- handle_amd_runner_exit "${exit_code}"
1313
if [[ -n "${artifact_work_dir}" ]]; then
1314
rm -rf "${artifact_work_dir}"
1315
fi
1316
+ handle_amd_runner_exit "${exit_code}"
1317
}
1318
trap cleanup_native_workspace EXIT
1319
@@ -1020,6 +1357,7 @@ if is_native_runtime; then
1357
1358
echo "Native test commands: $commands"
1359
run_native_preflight || exit 1
1360
+ echo "--- Test log"
1361
# Keep AMD CI orchestration variables out of vLLM's runtime environment.
1362
clear_ci_orchestration_env
1363
/bin/bash -o pipefail -c "${commands}"
@@ -1042,7 +1380,6 @@ container_name="rocm_${BUILDKITE_COMMIT}_$(tr -dc A-Za-z0-9 < /dev/urandom | hea
1380
# shellcheck disable=SC2317 # Called indirectly by the EXIT trap.
1381
remove_docker_container() {
1382
local exit_code=$?
1045
- handle_amd_runner_exit "${exit_code}"
1383
if docker container inspect "${container_name}" >/dev/null 2>&1; then
1384
docker rm -f "${container_name}" || true
1385
fi
@@ -1055,6 +1392,7 @@ remove_docker_container() {
1392
if [[ -n "${artifact_work_dir}" ]]; then
1393
rm -rf "${artifact_work_dir}"
1394
fi
1395
+ handle_amd_runner_exit "${exit_code}"
1396
}
1397
trap remove_docker_container EXIT
1398