Skip to content

Commit e090d56

Browse files
authored
ci: self-hosted reliability — case-opt timeout/clean, monitor crash, compiler labels (#1643)
1 parent e56a0a1 commit e090d56

6 files changed

Lines changed: 128 additions & 119 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/bin/bash
2+
3+
# Single source of truth for the benchmark cases exercised by the
4+
# case-optimization CI. The pre-build (compiles the case-optimized binaries)
5+
# and the run (executes them) MUST iterate the same list in the same order:
6+
# sharding partitions this list by index, so any drift silently pre-builds or
7+
# runs the wrong cases. Sourced by prebuild-case-optimization.sh and
8+
# run_case_optimization.sh.
9+
benchmarks=(
10+
benchmarks/5eq_rk3_weno3_hllc/case.py
11+
benchmarks/viscous_weno5_sgb_acoustic/case.py
12+
benchmarks/hypo_hll/case.py
13+
benchmarks/ibm/case.py
14+
benchmarks/igr/case.py
15+
)
16+
17+
# Parse an optional "$job_shard" of the form "i/N" (e.g. "2/3"). On success
18+
# sets caseopt_shard_idx / caseopt_shard_count (both 1 when unset — a single
19+
# shard covering every case). Aborts on a malformed value.
20+
caseopt_parse_shard() {
21+
caseopt_shard_idx=1
22+
caseopt_shard_count=1
23+
[ -z "${job_shard:-}" ] && return 0
24+
caseopt_shard_idx="${job_shard%%/*}"
25+
caseopt_shard_count="${job_shard##*/}"
26+
case "$caseopt_shard_idx" in ''|*[!0-9]*|0*) echo "ERROR: bad shard '$job_shard' (expected i/N)"; exit 1 ;; esac
27+
case "$caseopt_shard_count" in ''|*[!0-9]*|0*) echo "ERROR: bad shard '$job_shard' (expected i/N)"; exit 1 ;; esac
28+
if [ "$job_shard" != "$caseopt_shard_idx/$caseopt_shard_count" ] \
29+
|| [ "$caseopt_shard_idx" -lt 1 ] || [ "$caseopt_shard_idx" -gt "$caseopt_shard_count" ]; then
30+
echo "ERROR: bad shard '$job_shard' (expected i/N with 1 <= i <= N)"; exit 1
31+
fi
32+
}
33+
34+
# 0 (true) if the 1-based case index $1 belongs to this shard. Shard i owns
35+
# every Nth case: indices i, i+N, i+2N, ...
36+
caseopt_case_in_shard() {
37+
[ $((($1 - 1) % caseopt_shard_count)) -eq $((caseopt_shard_idx - 1)) ]
38+
}

.github/scripts/monitor_slurm_job.sh

Lines changed: 19 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -122,62 +122,39 @@ done
122122

123123
echo "=== Streaming output for job $job_id ==="
124124

125-
# Start tail and redirect its output to file descriptor 3 for multiplexing
126-
# This allows us to stream tail output while also printing heartbeat messages
127-
exec 3< <(stdbuf -oL -eL tail -f "$output_file" 2>&1)
125+
# Stream the job's output to the step log with a plain backgrounded `tail`.
126+
# This previously used a timed read (`read -t 1`) over a `tail -f` process
127+
# substitution; when that pipe broke, bash could crash in the read-builtin's
128+
# alarm/longjmp unwind path (SIGSEGV), and the EXIT trap would then cancel a
129+
# still-healthy job. A backgrounded tail avoids that construct entirely, and
130+
# the final `cat` below reprints the whole file so nothing is lost if tail is
131+
# killed mid-flush.
132+
stdbuf -oL -eL tail -f "$output_file" 2>&1 &
128133
tail_pid=$!
129134

130-
# Monitor job status and stream output simultaneously
135+
# Poll job status until it reaches a terminal state; streaming happens
136+
# independently in the background tail above.
131137
last_heartbeat=$(date +%s)
132-
133138
while true; do
134-
# Try to read from tail output (non-blocking via timeout)
135-
# Read multiple lines if available to avoid falling behind
136-
lines_read=0
137-
while IFS= read -r -t 1 line <&3 2>/dev/null; do
138-
echo "$line"
139-
lines_read=$((lines_read + 1))
140-
last_heartbeat=$(date +%s)
141-
# Limit burst reads to avoid starving the status check
142-
if [ $lines_read -ge 100 ]; then
143-
break
144-
fi
145-
done
146-
147-
# Check job status
148-
current_time=$(date +%s)
149139
state=$(get_job_state "$job_id")
150140

151141
if is_terminal_state "$state"; then
152142
echo "[$(date +%H:%M:%S)] Job $job_id reached terminal state: $state"
153143
break
154-
else
155-
# Print heartbeat if no output for 60 seconds
156-
if [ $((current_time - last_heartbeat)) -ge 60 ]; then
157-
echo "[$(date +%H:%M:%S)] Job $job_id state=$state (no new output for 60s)..."
158-
last_heartbeat=$current_time
159-
fi
160144
fi
161145

162-
# Sleep briefly between status checks
163-
sleep 1
164-
done
165-
166-
# Drain any remaining output from tail after job completes
167-
echo "Draining remaining output..."
168-
drain_count=0
169-
while IFS= read -r -t 1 line <&3 2>/dev/null; do
170-
echo "$line"
171-
drain_count=$((drain_count + 1))
172-
# Safety limit to avoid infinite loop
173-
if [ $drain_count -ge 10000 ]; then
174-
echo "Warning: Truncating remaining output after 10000 lines"
175-
break
146+
# Periodic heartbeat so the CI log never looks stalled during quiet phases.
147+
current_time=$(date +%s)
148+
if [ $((current_time - last_heartbeat)) -ge 60 ]; then
149+
echo "[$(date +%H:%M:%S)] Job $job_id state=$state..."
150+
last_heartbeat=$current_time
176151
fi
152+
153+
sleep 5
177154
done
178155

179-
# Close the file descriptor and kill tail
180-
exec 3<&-
156+
# Give tail a moment to flush the final lines, then stop streaming.
157+
sleep 2
181158
kill "${tail_pid}" 2>/dev/null || true
182159
tail_pid=""
183160

.github/scripts/prebuild-case-optimization.sh

Lines changed: 14 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -22,34 +22,16 @@ case "$cluster" in
2222
*) echo "ERROR: Unknown cluster '$cluster'"; exit 1 ;;
2323
esac
2424

25-
# Optional sharding (format "i/N", e.g. "1/2"), set by submit-slurm-job.sh's
26-
# [shard] argument via $job_shard: shard i builds every Nth case of the sorted
27-
# case list. Unset = build all cases in one job (default; other clusters).
25+
# Benchmark list + optional sharding ("i/N", e.g. "1/3", set by
26+
# submit-slurm-job.sh's [shard] argument via $job_shard): shard i builds every
27+
# Nth case of the shared list. Unset = build all cases in one job (default;
28+
# other clusters). The list is the single source of truth shared with
29+
# run_case_optimization.sh so pre-built binaries and run cases never drift.
30+
source .github/scripts/case-optimization-benchmarks.sh
31+
caseopt_parse_shard
2832
shard="${job_shard:-}"
29-
if [ -n "$shard" ]; then
30-
# Validate full shape: must be exactly "digits/digits" — one slash with
31-
# non-empty, purely numeric, non-leading-zero parts on both sides.
32-
# Split first, then validate each part independently so that inputs like
33-
# "1/" "/2" "//" "1/2/3" "a/b" "12" are all caught before any arithmetic.
34-
shard_idx="${shard%%/*}"
35-
shard_count="${shard##*/}"
36-
# Reject if no slash (idx and count are equal and equal to the whole string)
37-
case "$shard_idx" in
38-
''|*[!0-9]*|0*) echo "ERROR: bad shard '$shard' (expected i/N)"; exit 1 ;;
39-
esac
40-
case "$shard_count" in
41-
''|*[!0-9]*|0*) echo "ERROR: bad shard '$shard' (expected i/N)"; exit 1 ;;
42-
esac
43-
# Confirm the string is exactly "idx/count" — catches "12" (no slash) and
44-
# "1/2/3" (extra slash, where idx=1 and count=2/3 would have failed above,
45-
# but this is an extra safety net).
46-
if [ "$shard" != "$shard_idx/$shard_count" ]; then
47-
echo "ERROR: bad shard '$shard' (expected i/N)"; exit 1
48-
fi
49-
if [ "$shard_idx" -lt 1 ] || [ "$shard_idx" -gt "$shard_count" ]; then
50-
echo "ERROR: bad shard '$shard' (expected i/N with 1 <= i <= N)"; exit 1
51-
fi
52-
fi
33+
shard_idx="$caseopt_shard_idx"
34+
shard_count="$caseopt_shard_count"
5335

5436
# Phoenix starts fresh (no prior dep build); other clusters pre-build deps via
5537
# build.sh first, so we must preserve them and only clean MFC target staging.
@@ -60,8 +42,8 @@ if [ "$cluster" = "phoenix" ]; then
6042
source .github/scripts/clean-build.sh
6143
clean_build
6244
elif [ -z "$shard" ]; then
63-
find build/staging -maxdepth 1 -regex '.*/[0-9a-f]+' -type d -exec rm -rf {} + 2>/dev/null || true
64-
find build/install -maxdepth 1 -regex '.*/[0-9a-f]+' -type d -exec rm -rf {} + 2>/dev/null || true
45+
find build/staging -maxdepth 1 -regex '.*/\(gpu-acc\|gpu-mp\|cpu\)-.*' -type d -exec rm -rf {} + 2>/dev/null || true
46+
find build/install -maxdepth 1 -regex '.*/\(gpu-acc\|gpu-mp\|cpu\)-.*' -type d -exec rm -rf {} + 2>/dev/null || true
6547
fi
6648

6749
. ./mfc.sh load -c "$flag" -m g
@@ -80,8 +62,7 @@ esac
8062
if [ -n "$shard" ] && [ "$shard_count" -gt 1 ]; then
8163
shared_marker_done="build/.prebuild-shared-targets-done"
8264
shared_marker_failed="build/.prebuild-shared-targets-failed"
83-
set -- benchmarks/*/case.py
84-
first_case="$1"
65+
first_case="${benchmarks[0]}"
8566
if [ "$shard_idx" -eq 1 ]; then
8667
# Remove both markers at the start so reruns and manual invocations
8768
# never observe stale state from a prior run.
@@ -110,11 +91,9 @@ if [ -n "$shard" ] && [ "$shard_count" -gt 1 ]; then
11091
fi
11192

11293
idx=0
113-
for case in benchmarks/*/case.py; do
94+
for case in "${benchmarks[@]}"; do
11495
idx=$((idx + 1))
115-
if [ -n "$shard" ] && [ $(((idx - 1) % shard_count)) -ne $((shard_idx - 1)) ]; then
116-
continue
117-
fi
96+
caseopt_case_in_shard "$idx" || continue
11897
echo "=== Pre-building: $case ==="
11998
./mfc.sh run "$case" --case-optimization $gpu_opts -j 8 --dry-run
12099
done

.github/scripts/run_case_optimization.sh

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,11 @@ if [ "$job_device" = "gpu" ] && [ "$ngpus" -eq 0 ]; then
1313
ngpus=1
1414
fi
1515

16-
benchmarks=(
17-
benchmarks/5eq_rk3_weno3_hllc/case.py
18-
benchmarks/viscous_weno5_sgb_acoustic/case.py
19-
benchmarks/hypo_hll/case.py
20-
benchmarks/ibm/case.py
21-
benchmarks/igr/case.py
22-
)
16+
# Benchmark list (single source of truth shared with the pre-build) + optional
17+
# sharding: a sharded run executes only the subset it has pre-built binaries
18+
# for. $job_shard mirrors the pre-build shard so the two stay aligned.
19+
source .github/scripts/case-optimization-benchmarks.sh
20+
caseopt_parse_shard
2321

2422
# For Frontier/Frontier AMD: deps were fetched on the login node via --deps-only;
2523
# build case-optimized binaries here on the compute node before running.
@@ -35,11 +33,14 @@ if [ "$job_cluster" != "phoenix" ] && [ "$job_cluster" != "frontier_amd" ]; then
3533
# preserve dependency dirs (hipfort, fftw, etc.) since the compute
3634
# node has no internet to re-fetch them.
3735
echo "=== Cleaning stale MFC target staging/install ==="
38-
find build/staging -maxdepth 1 -regex '.*/[0-9a-f]+' -type d -exec rm -rf {} + 2>/dev/null || true
39-
find build/install -maxdepth 1 -regex '.*/[0-9a-f]+' -type d -exec rm -rf {} + 2>/dev/null || true
36+
find build/staging -maxdepth 1 -regex '.*/\(gpu-acc\|gpu-mp\|cpu\)-.*' -type d -exec rm -rf {} + 2>/dev/null || true
37+
find build/install -maxdepth 1 -regex '.*/\(gpu-acc\|gpu-mp\|cpu\)-.*' -type d -exec rm -rf {} + 2>/dev/null || true
4038

4139
echo "=== Building case-optimized binaries on compute node ==="
40+
idx=0
4241
for case in "${benchmarks[@]}"; do
42+
idx=$((idx + 1))
43+
caseopt_case_in_shard "$idx" || continue
4344
echo "--- Building: $case ---"
4445
./mfc.sh build -i "$case" --case-optimization $gpu_opts -j 8
4546
done
@@ -50,7 +51,10 @@ passed=0
5051
failed=0
5152
failed_cases=""
5253

54+
idx=0
5355
for case in "${benchmarks[@]}"; do
56+
idx=$((idx + 1))
57+
caseopt_case_in_shard "$idx" || continue
5458
case_dir="$(dirname "$case")"
5559
case_name="$(basename "$case_dir")"
5660
echo ""

.github/workflows/bench.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
matrix:
3939
include:
4040
- cluster: phoenix
41-
name: Georgia Tech | Phoenix (NVHPC)
41+
name: Georgia Tech | Phoenix (GNU)
4242
group: phoenix
4343
labels: gt
4444
flag: p

0 commit comments

Comments
 (0)