Skip to content

Commit 5be7117

Browse files
authored
Merge branch 'master' into cont_damage_fix
2 parents 4767804 + cc20c94 commit 5be7117

32 files changed

Lines changed: 1299 additions & 536 deletions

.github/scripts/check_coverage_map_health.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from pathlib import Path
55

66
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "toolchain"))
7-
from mfc.test.coverage import COVERAGE_MAP_PATH, _git, load_map, map_health # noqa: E402
7+
from mfc.test.coverage import COVERAGE_MAP_PATH, load_map, map_health, run_git # noqa: E402
88
from mfc.test.cases import list_cases # noqa: E402 (returns the current test list)
99

1010
MAX_AGE_DAYS = 10
@@ -39,7 +39,7 @@ def verified_sha(cwd=None):
3939
caller must read that as undeterminable and fall back to the wall-clock age rule, not
4040
as a failure -- an absent ref is not evidence of a broken refresh.
4141
"""
42-
rev = _git(["rev-parse", "--verify", "--quiet", f"{VERIFIED_REF}^{{commit}}"], cwd)
42+
rev = run_git(["rev-parse", "--verify", "--quiet", f"{VERIFIED_REF}^{{commit}}"], cwd)
4343
return rev.stdout.strip() or None
4444

4545

@@ -52,10 +52,10 @@ def verified_after_last_change(git_sha, cwd=None):
5252
"""
5353
if not git_sha:
5454
return None
55-
last = _git(["log", "-1", "--format=%H", "--", *COVERAGE_RELEVANT_PATHS], cwd)
55+
last = run_git(["log", "-1", "--format=%H", "--", *COVERAGE_RELEVANT_PATHS], cwd)
5656
if last.returncode != 0 or not last.stdout.strip():
5757
return None # shallow clone or no such commit -> fall back to the age rule
58-
ancestor = _git(["merge-base", "--is-ancestor", last.stdout.strip(), git_sha], cwd)
58+
ancestor = run_git(["merge-base", "--is-ancestor", last.stdout.strip(), git_sha], cwd)
5959
return {0: True, 1: False}.get(ancestor.returncode) # anything else -> None (unknown sha, shallow history)
6060

6161

.github/scripts/ci-outage.sh

Lines changed: 0 additions & 118 deletions
This file was deleted.

.github/scripts/classify-build-failure.sh

Lines changed: 0 additions & 41 deletions
This file was deleted.

.github/scripts/monitor_slurm_job.sh

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ output_file="$2"
3535
echo "Submitted batch job $job_id"
3636
echo "Monitoring output file: $output_file"
3737

38+
# Put the one thing a reader needs on the run's summary page. Without this,
39+
# learning why a job failed means opening a log of tens of thousands of lines --
40+
# and an infrastructure fault looks exactly like a test failure until you do.
41+
# Silent when not running under Actions.
42+
ci_summary() {
43+
[ -n "${GITHUB_STEP_SUMMARY:-}" ] || return 0
44+
printf '%b\n' "$1" >> "$GITHUB_STEP_SUMMARY"
45+
}
46+
3847
# Robustly check SLURM job state using squeue with sacct fallback.
3948
# Returns the state string (PENDING, RUNNING, COMPLETED, FAILED, etc.)
4049
# or "UNKNOWN" if both commands fail.
@@ -213,8 +222,14 @@ while true; do
213222
sleep "$MFC_MONITOR_POLL_SECONDS"
214223
done
215224

216-
# Give tail a moment to flush the final lines, then stop streaming.
225+
# Give tail a moment to flush the final lines, then stop streaming. Whether it
226+
# was still alive decides how much needs reprinting below: if it streamed the
227+
# whole job, printing the file again just doubles every log.
217228
sleep 2
229+
streamed_ok=0
230+
if kill -0 "${tail_pid}" 2>/dev/null; then
231+
streamed_ok=1
232+
fi
218233
kill "${tail_pid}" 2>/dev/null || true
219234
tail_pid=""
220235

@@ -238,9 +253,20 @@ if [ -f "$output_file" ]; then
238253
done
239254
fi
240255

256+
# Reprint only what streaming may have missed. `tail -f` above already emitted
257+
# the whole file as it was written, so cat'ing it again duplicated every job's
258+
# output -- measured at 3 copies of each line on a GPU job, and 65,000 lines of
259+
# offload diagnostics repeated for a single fault. The reprint exists solely as
260+
# a safety net for a tail that died mid-job, so it is bounded when tail survived
261+
# and complete only when it did not.
241262
echo ""
242-
echo "=== Final output ==="
243-
cat "$output_file"
263+
if [ "${streamed_ok:-0}" -eq 1 ]; then
264+
echo "=== Final output (tail; the full log streamed above) ==="
265+
tail -n "${MFC_MONITOR_FINAL_LINES:-40}" "$output_file"
266+
else
267+
echo "=== Final output (streaming stopped early; reprinting in full) ==="
268+
cat "$output_file"
269+
fi
244270

245271
# Check exit status with sacct fallback
246272
exit_code=""
@@ -267,26 +293,32 @@ if [ -z "$exit_code" ]; then
267293
exit 1
268294
fi
269295

270-
# Infrastructure verdicts from the in-allocation preflight come back as the
271-
# job's own exit code. Relay them verbatim: flattening them to 1 would leave the
272-
# submit wrapper unable to tell "this node is unusable" (exclude it and try
273-
# again) from "the tests failed" (report it).
296+
# The preflight's node-fault verdict comes back as the job's own exit code.
297+
# Relay it verbatim: flattening it to 1 would leave the submit wrapper unable to
298+
# tell "this node is unusable" (exclude it and try again) from "the tests
299+
# failed" (report it).
300+
faulted_node=$(grep -oE 'MFC_FAULT_NODE=[^ ]+' "$output_file" 2>/dev/null | tail -n1 | cut -d= -f2 || true)
301+
274302
case "$exit_code" in
275303
77:*)
276304
echo "Job $job_id failed preflight: the node is unusable — signaling caller to exclude it and resubmit."
305+
ci_summary "### :warning: Infrastructure fault — not a code or test failure\n\nNode \`${faulted_node:-unknown}\` could not run MFC (job \`$job_id\`). It is excluded and the job resubmitted elsewhere.\n"
277306
monitor_success=1
278307
exit 77
279308
;;
280-
78:*)
281-
echo "Job $job_id skipped: a cluster-wide outage is already recorded."
282-
monitor_success=1
283-
exit 78
284-
;;
285309
esac
286310

287311
# Check if job succeeded
288312
if [ "$exit_code" != "0:0" ]; then
289313
echo "ERROR: Job $job_id failed with exit code $exit_code"
314+
# A GPU memory fault explains itself in a block the test harness prints; lift
315+
# it onto the summary page so the faulting kernel and source line are visible
316+
# without opening the log at all.
317+
if grep -q 'GPU fault summary' "$output_file" 2>/dev/null; then
318+
ci_summary "### GPU memory fault\n\n\`\`\`\n$(grep -A6 'GPU fault summary' "$output_file" | head -8 | sed 's/`/'"'"'/g')\n\`\`\`\n"
319+
else
320+
ci_summary "### Job \`$job_id\` failed (exit $exit_code)\n\n\`\`\`\n$(tail -n 15 "$output_file" | sed 's/`/'"'"'/g')\n\`\`\`\n"
321+
fi
290322
exit 1
291323
fi
292324

.github/scripts/preflight.sh

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
# Exit codes:
2020
# 0 node looks healthy, carry on
2121
# 77 node-local fault -- caller should exclude this node and resubmit
22-
# 78 cluster-wide outage already recorded -- caller should skip, not requeue
2322

2423
set -uo pipefail
2524

@@ -33,7 +32,6 @@ fi
3332

3433
EXIT_HEALTHY=0
3534
EXIT_NODE_FAULT=77
36-
EXIT_OUTAGE=78
3735

3836
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
3937
node="${SLURMD_NODENAME:-$(hostname -s 2>/dev/null || hostname)}"
@@ -50,19 +48,6 @@ if [ -z "${SLURM_JOB_ID:-}" ]; then
5048
exit $EXIT_HEALTHY
5149
fi
5250

53-
# --- Cluster-wide outage: requeuing cannot help, so skip rather than retry ---
54-
outage_rc=0
55-
bash "$SCRIPT_DIR/ci-outage.sh" check "$cluster" || outage_rc=$?
56-
if [ "$outage_rc" -eq 1 ]; then
57-
echo "Preflight: skipping on $node because $cluster is known to be down."
58-
exit $EXIT_OUTAGE
59-
elif [ "$outage_rc" -ne 0 ]; then
60-
# Only exit 1 means "tripped". Anything else means the breaker could not be
61-
# read at all (missing script, unreadable state dir), which says nothing
62-
# about the cluster -- treating it as an outage would halt CI on a bug here.
63-
echo "Preflight: could not read the outage breaker (exit $outage_rc); continuing."
64-
fi
65-
6651
# --- Node health ---
6752
# Pick the *newest* install matching this job's device (build/install is named
6853
# e.g. gpu-acc-<hash>, gpu-mp-<hash>). Both halves matter: the device filter
@@ -114,7 +99,12 @@ echo "Preflight: probing $node with $syscheck_bin"
11499
# one there fails 127 no matter how healthy the node is. See
115100
# toolchain/templates/{phoenix,frontier,frontier_amd}.mako.
116101
case "$cluster" in
117-
phoenix) launcher=(mpirun -np 1) ;;
102+
# --bind-to none: a single-rank health probe has nothing to bind against,
103+
# and Open MPI's default binding fails outright on some Phoenix nodes
104+
# ("hwloc_set_cpubind returned Error for bitmap 0"), killing the process
105+
# before the binary is even launched. That is a launcher problem, not a
106+
# node problem -- but it condemned three healthy nodes before being caught.
107+
phoenix) launcher=(mpirun --bind-to none -np 1) ;;
118108
frontier|frontier_amd) launcher=(srun -n1) ;;
119109
*) launcher=() ;;
120110
esac
@@ -131,18 +121,53 @@ fi
131121
# PMIX_ERR_NO_PERMISSIONS and friends from dstore_base.c are benign and appear
132122
# in more passing jobs than failing ones, so matching on log text would fail
133123
# healthy nodes.
134-
probe_rc=0
135-
if [ "${#launcher[@]}" -eq 0 ]; then
136-
"$syscheck_bin" 2>&1 || probe_rc=$?
137-
else
138-
"${launcher[@]}" "$syscheck_bin" 2>&1 || probe_rc=$?
139-
fi
124+
# Captured to a variable, not a temp file: this runs before any module set is
125+
# guaranteed and mktemp is not always on PATH here.
126+
run_probe() {
127+
probe_rc=0
128+
if [ "$#" -eq 0 ]; then
129+
probe_out=$("$syscheck_bin" 2>&1) || probe_rc=$?
130+
else
131+
probe_out=$("$@" "$syscheck_bin" 2>&1) || probe_rc=$?
132+
fi
133+
}
134+
135+
run_probe "${launcher[@]}"
136+
137+
# If this launcher does not take the flags we added, drop them and probe again
138+
# rather than reporting a verdict about the node. Otherwise a launcher that
139+
# rejects an option would fail every probe, and -- because a failed launch is
140+
# treated as inconclusive below -- would silently switch the preflight off
141+
# instead of failing loudly.
142+
case "$probe_out" in
143+
*"unrecognized option"*|*"unrecognized argument"*|*"Unknown option"*|*"invalid option"*)
144+
if [ "${#launcher[@]}" -gt 1 ]; then
145+
echo "Preflight: ${launcher[0]} rejected the probe's options; retrying with none of them."
146+
run_probe "${launcher[0]}"
147+
fi
148+
;;
149+
esac
150+
151+
printf '%s\n' "$probe_out"
140152

141153
if [ "$probe_rc" -eq 0 ]; then
142154
echo "Preflight: $node passed."
143155
exit $EXIT_HEALTHY
144156
fi
145157

158+
# Only a binary that RAN and failed says anything about this node. When the
159+
# launcher never got as far as starting it, the verdict is about mpirun or the
160+
# allocation, and excluding the node is both wrong and expensive -- three
161+
# healthy Phoenix nodes were excluded this way, two jobs deep, before the run
162+
# gave up. Judge nothing on a launch that never happened.
163+
case "$probe_out" in
164+
*"The specified application failed to start"*|*"unable to start the specified application"*|*"was killed without launching the target application"*)
165+
echo "Preflight: the launcher could not start $syscheck_bin on $node;"
166+
echo " that is a launcher or allocation problem, not evidence about the node. Continuing."
167+
exit $EXIT_HEALTHY
168+
;;
169+
esac
170+
146171
echo "::error::Preflight failed on $node: syscheck could not run MFC here."
147172
echo "This is an INFRASTRUCTURE fault, not a code or test failure."
148173
echo "MFC_FAULT_NODE=$node"

0 commit comments

Comments
 (0)