Skip to content

Commit 97509cf

Browse files
Abraham Sewillclaude
andcommitted
feat(scripts): one-shot diagnostic bundle for a GPU that will not plot
Five rounds of debugging an Intel Arc produced five rounds of me asking for one more piece of data, and three of those runs turned out to describe a binary that predated the fix being tested. This collects everything in one pass. The first section is the one that would have saved the whole exchange: it checks ~/.cargo/bin/xchplot2 and ./target/release/xchplot2 SEPARATELY, prints mtime and size for each, greps each for feature strings from recent fixes, and shouts if they differ. `cargo build --release` does not update a copy installed by `cargo install`, so a bare `xchplot2` keeps running the old code and every subsequent experiment silently tests the wrong binary. Verified against this dev box, where the installed copy is three days stale and reports MISSING on all four probes while the local build reports PRESENT. The rest: host memory and swap, GPU/driver identification, engine job and preempt timeouts, acpp-info, dmesg before and after, a small-k control to test whether failure is scale-dependent, an instrumented k=28 attempt with vmstat and peak-RSS sampling, and the parity tests to settle codegen. The k=28 attempt runs inside a systemd scope capped at 75% of RAM with swap off where available, so a host that runs out of memory kills the plotter rather than the desktop session — which is also the experiment, since dying at the cap and completing without it separates host memory from a GPU fault. Every probe is individually guarded; a missing tool or an unreadable sysfs node degrades to a note rather than ending the bundle. dmesg being root-restricted is called out explicitly with the two commands to run by hand, because a GPU engine reset and an OOM kill appear nowhere else. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f532a4f commit 97509cf

1 file changed

Lines changed: 244 additions & 0 deletions

File tree

scripts/collect-gpu-diag.sh

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
#!/usr/bin/env bash
2+
# collect-gpu-diag.sh — one-shot diagnostic bundle for a GPU that fails to plot.
3+
#
4+
# Runs a fixed sequence of probes and two instrumented plot attempts, and writes
5+
# everything to a single file. Nothing is uploaded; nothing is deleted. Intended
6+
# to be run once and the resulting file sent to whoever is debugging.
7+
#
8+
# ./scripts/collect-gpu-diag.sh [--devices N] [--k 28]
9+
#
10+
# Takes roughly 5-10 minutes. Safe to Ctrl-C; partial output is still useful.
11+
#
12+
# The k=28 attempt is run inside a memory-limited scope where systemd allows it,
13+
# so that a host that runs out of RAM kills the plotter instead of the desktop
14+
# session. That containment is also the experiment: if the plot dies at the cap
15+
# and completes without it, the problem is host memory, not the GPU.
16+
17+
set -u
18+
19+
DEVICES=""
20+
K=28
21+
while [ $# -gt 0 ]; do
22+
case "$1" in
23+
--devices) DEVICES="$2"; shift 2 ;;
24+
--k) K="$2"; shift 2 ;;
25+
*) echo "unknown argument: $1" >&2; exit 2 ;;
26+
esac
27+
done
28+
29+
OUT="xchplot2-diag-$(date +%Y%m%d-%H%M%S).txt"
30+
# Repo root relative to this script, so the local build is found no matter
31+
# which directory the script is invoked from.
32+
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
33+
LOCAL_BIN="$REPO/target/release/xchplot2"
34+
BIN="$(command -v xchplot2 || echo "$LOCAL_BIN")"
35+
DEVARG=""
36+
[ -n "$DEVICES" ] && DEVARG="--devices $DEVICES"
37+
38+
exec > >(tee "$OUT") 2>&1
39+
40+
sec() { printf '\n\n========== %s ==========\n' "$1"; }
41+
have() { command -v "$1" >/dev/null 2>&1; }
42+
# Never let one probe's failure end the bundle.
43+
try() { "$@" 2>&1 || echo " (command failed: $*)"; }
44+
45+
echo "xchplot2 diagnostic bundle"
46+
echo "date: $(date -Is)"
47+
echo "host: $(uname -a)"
48+
49+
sec "1. BINARY PROVENANCE (is the build actually current?)"
50+
cat <<'EOM'
51+
The most common cause of "I rebuilt and nothing changed":
52+
53+
cargo install --path . -> ~/.cargo/bin/xchplot2
54+
cargo build --release -> ./target/release/xchplot2
55+
56+
Those are DIFFERENT FILES. A rebuild does not update an installed copy, and a
57+
bare `xchplot2` runs whichever one is on PATH. To update an installed copy:
58+
59+
cargo install --path . --force
60+
61+
EOM
62+
63+
check_bin() {
64+
b="$1"; tag="$2"
65+
if [ -z "$b" ] || [ ! -x "$b" ]; then
66+
printf ' %-13s %s\n' "$tag" "(absent)"
67+
return
68+
fi
69+
printf ' %-13s %s\n' "$tag" "$b"
70+
printf ' %-13s mtime %s size %s\n' "" \
71+
"$(date -r "$b" '+%Y-%m-%d %H:%M' 2>/dev/null)" \
72+
"$(stat -c %s "$b" 2>/dev/null)"
73+
for s in "XCHPLOT2_HOST_RESERVE_MB:host-RAM gate" \
74+
"pinned host allocation of:pinned-alloc chokepoint" \
75+
"asynchronous backend error:device-fault attribution" \
76+
"plot compression failed after:writer attribution"; do
77+
pat="${s%%:*}"; label="${s#*:}"
78+
if strings "$b" 2>/dev/null | grep -qF "$pat"; then
79+
printf ' %-13s PRESENT %s\n' "" "$label"
80+
else
81+
printf ' %-13s MISSING %s\n' "" "$label"
82+
fi
83+
done
84+
}
85+
PATH_BIN="$(command -v xchplot2 2>/dev/null || true)"
86+
check_bin "$PATH_BIN" "on PATH:"
87+
check_bin "$LOCAL_BIN" "local build:"
88+
echo
89+
if [ -n "$PATH_BIN" ] && [ -x "$LOCAL_BIN" ] \
90+
&& ! cmp -s "$PATH_BIN" "$LOCAL_BIN"; then
91+
echo " >>> THE TWO BINARIES DIFFER. Everything below used: $BIN"
92+
echo " >>> If that is the stale one, this bundle describes the OLD code."
93+
fi
94+
echo
95+
if [ -d .git ]; then
96+
echo "repo HEAD: $(git rev-parse --short HEAD 2>/dev/null)"
97+
echo "repo dirty: $(git status --porcelain 2>/dev/null | wc -l) file(s)"
98+
fi
99+
100+
sec "2. HOST MEMORY"
101+
try free -h
102+
echo; try swapon --show
103+
echo; grep -E 'MemTotal|MemFree|MemAvailable|SwapTotal|SwapFree|Committed_AS|Dirty' /proc/meminfo
104+
echo; echo "top 8 RSS consumers:"
105+
try ps -eo rss,comm --sort=-rss --no-headers
106+
echo
107+
108+
sec "3. GPU / DRIVER ENVIRONMENT"
109+
for c in /sys/class/drm/card*; do
110+
[ -e "$c/device/vendor" ] || continue
111+
drv=$(basename "$(readlink -f "$c/device/driver" 2>/dev/null)" 2>/dev/null)
112+
printf '%s: vendor=%s device=%s driver=%s\n' \
113+
"$(basename "$c")" \
114+
"$(cat "$c/device/vendor" 2>/dev/null)" \
115+
"$(cat "$c/device/device" 2>/dev/null)" \
116+
"${drv:-unknown}"
117+
done
118+
echo
119+
echo "-- GPU engine timeouts (a kernel outrunning these is reset by the driver) --"
120+
found_to=0
121+
for f in /sys/class/drm/card*/device/tile*/gt*/engines/*/job_timeout_ms \
122+
/sys/class/drm/card*/device/tile*/gt*/engines/*/preempt_timeout_us \
123+
/sys/class/drm/card*/device/preempt_timeout_ms \
124+
/sys/class/drm/card*/device/enable_hangcheck; do
125+
[ -r "$f" ] || continue
126+
found_to=1
127+
printf ' %s = %s\n' "$f" "$(cat "$f" 2>/dev/null)"
128+
done
129+
[ "$found_to" = 0 ] && echo " (no engine timeout knobs readable)"
130+
echo
131+
have acpp-info && { echo "-- acpp-info --"; try acpp-info; } || echo "(acpp-info not on PATH)"
132+
echo
133+
have clinfo && try clinfo -l || true
134+
for v in ZES_ENABLE_SYSMAN NEOReadDebugKeys ACPP_TARGETS ACPP_VISIBILITY_MASK \
135+
XCHPLOT2_HOST_RESERVE_MB; do
136+
echo "env $v=${!v-<unset>}"
137+
done
138+
139+
sec "4. DMESG BASELINE (before any plotting)"
140+
try sh -c 'dmesg -T 2>/dev/null | tail -40 || sudo -n dmesg -T 2>/dev/null | tail -40'
141+
DMESG_MARK=$(date +%s)
142+
143+
# --- instrumented run helper -------------------------------------------------
144+
# Samples vmstat and peak RSS around a plot attempt. $1=label, rest=extra args.
145+
run_probe() {
146+
label="$1"; shift
147+
sec "$label"
148+
echo "command: $BIN bench -k $* $DEVARG -n 1"
149+
vmlog=$(mktemp); rsslog=$(mktemp)
150+
have vmstat && (vmstat 1 > "$vmlog" 2>&1 & echo $! > "$vmlog.pid")
151+
152+
start=$(date +%s.%N)
153+
# shellcheck disable=SC2086
154+
$BIN bench -k $* $DEVARG -n 1 &
155+
pid=$!
156+
hwm=0
157+
while kill -0 "$pid" 2>/dev/null; do
158+
v=$(awk '/^VmHWM:/{print $2}' "/proc/$pid/status" 2>/dev/null)
159+
[ -n "${v:-}" ] && [ "$v" -gt "$hwm" ] && hwm=$v
160+
sleep 0.2
161+
done
162+
wait "$pid"; rc=$?
163+
end=$(date +%s.%N)
164+
165+
[ -f "$vmlog.pid" ] && kill "$(cat "$vmlog.pid")" 2>/dev/null
166+
echo
167+
echo "exit code: $rc"
168+
awk -v s="$start" -v e="$end" 'BEGIN{ printf "wall: %.2f s\n", e-s }'
169+
awk -v h="$hwm" 'BEGIN{ printf "peak RSS: %.2f GiB\n", h/1048576 }'
170+
if [ -s "$vmlog" ]; then
171+
echo "-- vmstat: si/so nonzero = swapping; high sy = kernel-bound --"
172+
head -3 "$vmlog"
173+
awk 'NR>2{print}' "$vmlog" | awk '{print}' | tail -25
174+
fi
175+
rm -f "$vmlog" "$vmlog.pid" "$rsslog"
176+
}
177+
178+
sec "5. CONTROL — small k (short kernels, small footprint)"
179+
echo "If this SUCCEEDS and k=$K fails, the failure is scale-dependent."
180+
run_probe "5a. k=22 run" 22
181+
182+
sec "6. TARGET — k=$K, memory-capped"
183+
echo "Capped so an out-of-RAM host kills the plotter, not your session."
184+
if have systemd-run && systemd-run --user --scope true >/dev/null 2>&1; then
185+
cap_gib=$(awk '/MemTotal/{printf "%d", ($2/1048576)*0.75}' /proc/meminfo)
186+
echo "running under systemd scope, MemoryMax=${cap_gib}G, swap disabled"
187+
echo " -> killed at the cap = host memory is the constraint"
188+
echo " -> same failure well under the cap = not memory"
189+
# shellcheck disable=SC2086
190+
try systemd-run --user --scope -q \
191+
-p MemoryMax=${cap_gib}G -p MemorySwapMax=0 \
192+
$BIN bench -k $K $DEVARG -n 1
193+
else
194+
echo "(systemd-run --user unavailable; running uncapped)"
195+
fi
196+
197+
run_probe "6b. k=$K run, uncapped + instrumented" "$K"
198+
199+
sec "7. DMESG (GPU resets, hangcheck, OOM kills all land here)"
200+
dmesg_out=$(dmesg -T 2>/dev/null || sudo -n dmesg -T 2>/dev/null \
201+
|| dmesg 2>/dev/null || sudo -n dmesg 2>/dev/null || true)
202+
if [ -z "$dmesg_out" ]; then
203+
cat <<'EOM'
204+
UNREADABLE — kernel.dmesg_restrict is set and passwordless sudo is not
205+
available. This section is important: a GPU engine reset or an OOM kill
206+
appears ONLY here. Please re-run just this part and include the output:
207+
208+
sudo dmesg -T | tail -100
209+
sudo dmesg -T | grep -iE 'xe |i915|drm|reset|hang|oom|killed process'
210+
EOM
211+
else
212+
echo "-- last 60 lines --"
213+
printf '%s\n' "$dmesg_out" | tail -60
214+
echo
215+
echo "-- lines matching reset/hang/oom/gpu --"
216+
printf '%s\n' "$dmesg_out" \
217+
| grep -iE 'xe |i915|drm|reset|hang|oom|killed process|GPU' | tail -40 \
218+
|| echo " (none — no GPU reset and no OOM kill was logged)"
219+
fi
220+
: "$DMESG_MARK"
221+
222+
sec "8. PARITY TESTS (settles codegen: these run tiny, no memory pressure)"
223+
ran_any=0
224+
for t in sycl_g_x_parity sycl_sort_parity sycl_bucket_offsets_parity sycl_t1_parity; do
225+
for p in "./build/$t" "./$t"; do
226+
if [ -x "$p" ]; then
227+
ran_any=1
228+
printf '%-30s ' "$t"
229+
if timeout 300 "$p" >/dev/null 2>&1; then echo PASS; else echo "FAIL/ERROR"; fi
230+
break
231+
fi
232+
done
233+
done
234+
[ "$ran_any" = 0 ] && cat <<'EOM'
235+
(not built. To build them:
236+
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
237+
cmake --build build -j --target sycl_g_x_parity sycl_sort_parity \
238+
sycl_bucket_offsets_parity sycl_t1_parity
239+
If these PASS while k=28 fails, the kernels are correct and the fault is
240+
environmental — driver, timeout, or memory.)
241+
EOM
242+
243+
sec "DONE"
244+
echo "Bundle written to: $OUT"

0 commit comments

Comments
 (0)