Skip to content

Commit 78c4b60

Browse files
committed
Merge upstream/master into up/mega: adopt the centralized Riemann EOS expressions
Upstream #1762 moved the equation-of-state arithmetic out of the Riemann solvers into m_variables_conversion/m_riemann_state helpers and deleted inline_riemann.fpp and inline_capillary.fpp; #1808 added the fluid_pp(i)%eos selector. Our side had re-shaped the same kernels (case-optimization solver gating, flat Riemann buffers, idwbuff_alloc), so upstream's delta is re-applied on top of our structure rather than either side being taken whole. Three AMR test cases gain fluid_pp(1)%eos = stiffened_gas, the value upstream set on its own equivalent cases, because the new validator rejects pi_inf under the ideal-gas default; no golden regenerated.
2 parents ed2d06f + 511cda5 commit 78c4b60

386 files changed

Lines changed: 11677 additions & 6322 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/rules/common-pitfalls.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,35 @@ covered in `docs/documentation/contributing.md`.
7474
sub-box, pack/unpack it with a device kernel (`s_l0_pack_unpack_block`,
7575
`s_amr_restrict_pack_device`) — that is why those exist. Measured: 10 of 60 covered
7676
cells delivered in the AMR cross-rank restrict, mass off 1.4e-5 per regrid.
77+
- An array whose bound is a device global (`dimension(num_fluids)`, `dimension(num_species)`) may be
78+
passed to a device routine **from a parallel-loop body, but not from inside another
79+
`GPU_ROUTINE(parallelism='[seq]')`**. CCE OpenACC rejects the second form with
80+
`ftn-7066 ... Global in accelerator routine without declare -- num_fluids`, and reports it at
81+
whatever line it gave up on: remove one trigger and the message *walks forward* to the next call,
82+
so the reported line is not the cause. Only the plain lanes fail - under `--case-optimization`
83+
those bounds are `parameter`s, so a green Case Opt lane beside a failing plain one is the
84+
signature. Every accepted call site in the tree already obeys this (`m_cbc`, `m_ibm`,
85+
`m_bubbles_EL`, `s_compute_cell_state`): form such a call in the loop body and pass scalars
86+
deeper. Neither `cray_inline` nor a `num_fluids_max` bound nor dropping optional dummies helps -
87+
all three were measured.
88+
- nvfortran 23.11/24.1 segfault (`fort2 TERMINATED by signal 11`) on a caller that passes a
89+
`parameter` array from `m_thermochem` (e.g. `molecular_weights`) into a declare-target routine.
90+
Read such arrays directly in the kernel, or pass a plain local computed from them.
91+
- The `USING_AMD` fypp guards (86 sites, `#:set` in `src/common/include/shared_parallel_macros.fpp`) are
92+
load-bearing, not a stale workaround - do not "modernize" them away. They swap a device-global array
93+
bound for a literal: `dimension(3)` for `num_dims`/`num_fluids` when case optimization is off (64
94+
sites), and `dimension(20)` for `sys_size` in `m_compute_cbc` (21 sites, with a matching
95+
`@:PROHIBIT` in `m_start_up` capping `sys_size <= 20` under AMD+CBC). Setting `USING_AMD = False`
96+
and rebuilding amdflang `--gpu mp` without case optimization compiles CLEAN - 728 s, zero
97+
diagnostics - and then NaNs at step 50 in CBC, riemann `wave_speeds=2`, IBM, surface tension,
98+
QBMM/viscous and MHD HLLD, while both Lagrange bubble cases *complete* with out-of-tolerance
99+
answers. Measured 2026-08-29 on MI210. A compile-only check returns green, so any future attempt to
100+
drop these must run the tests, not just build.
101+
- The same "call it from the loop body" rule covers `m_thermochem`: calling `get_species_*` from
102+
inside a `GPU_ROUTINE` rather than from the kernel gave CCE OpenMP a runtime
103+
`Memory access fault by GPU node-N ... Reason: Unknown` on the first step (exit 134), while every
104+
other backend ran. Evaluate them at the call site and pass the arrays in. Note this one only shows
105+
at runtime, and only on a case that reaches the path - the build is clean.
77106

78107
## Parameters
79108

@@ -143,6 +172,27 @@ covered in `docs/documentation/contributing.md`.
143172
- Tests are generated programmatically in `toolchain/mfc/test/cases.py` (parameter
144173
modifications on `BASE_CFG` via the `CaseGeneratorStack` push/pop pattern); test UUID =
145174
CRC32 of the trace string; `./mfc.sh test -l` lists all.
175+
- `--only` matches whole trace *elements*, not substrings, and `_filter_only`
176+
(`toolchain/mfc/test/test.py`) **ANDs labels while ORing UUIDs**. So `--only bubbles` matches
177+
nothing (the element is `Bubbles`), and `--only low_Mach=1 low_Mach=2` asks for cases carrying
178+
both and also matches nothing. It then exits **143**, which reads like an external kill rather
179+
than an empty filter. Pass UUIDs whenever you want the union of several groups.
180+
- Sibling `define_case_d` calls off the same stack level are never *combined*. Two switches that
181+
only matter together (`avg_state=1` needs `wave_speeds=2` to be read at all) therefore get zero
182+
effective coverage unless something pushes one and defines the other beneath it. Check
183+
reachability before trusting that a flag is tested.
184+
- `--no-build` silently runs whatever binary is on disk for a configuration it did not build.
185+
Chemistry has its own config (`gpu-mp-chem-*`) that a plain `./mfc.sh build` never produces, so
186+
a `--no-build` run reports failures from stale binaries and hides real compile breaks. Run
187+
chemistry-touching sets without it.
188+
- Pick the newest binary by the *binary's* mtime (`ls -t build/install/*/bin/simulation`), not the
189+
install directory's - a stale config's directory can be newer than a fresh build's.
190+
- The pre-commit hook lives in the main repo's `.git/hooks/` and git exports `GIT_DIR` there
191+
during a commit, so from a worktree the toolchain lint enumerates the *other* checkout and
192+
fails. Reproduce with `GIT_DIR=<main>/.git ./mfc.sh precheck`. Run precheck by hand and commit
193+
with `--no-verify`.
194+
- `/tmp` is node-local: scratch does not survive a compute-node change, and its absence is
195+
silence, not an error. Keep patches and resource baselines on a shared filesystem.
146196
- Golden files are tolerance-compared. Regenerate only the affected tests
147197
(`./mfc.sh test --generate --only <tests>`) — an unexplained golden-file diff is a bug
148198
report, not noise to be regenerated away.

.github/scripts/check_coverage_map_health.py

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
"""Fail loudly if the committed coverage map is stale or under-covers. Used by coverage-health.yml."""
22
import datetime
3-
import os
4-
import subprocess
53
import sys
64
from pathlib import Path
75

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

1210
MAX_AGE_DAYS = 10
@@ -33,18 +31,6 @@
3331
COVERAGE_RELEVANT_PATHS = [":(glob)src/**/*.fpp", "toolchain/mfc/test/cases.py"]
3432

3533

36-
def _git_env():
37-
"""The environment minus every GIT_* variable.
38-
39-
`cwd` is how callers select the repository here, but git honors an inherited GIT_DIR /
40-
GIT_INDEX_FILE over the working directory -- and git exports both while running a hook.
41-
MFC's pre-commit hook runs precheck, whose toolchain lint imports this script and calls
42-
these functions against throwaway test repos; without the scrub those calls silently
43-
answer for the developer's real repository instead.
44-
"""
45-
return {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
46-
47-
4834
def verified_sha(cwd=None):
4935
"""Commit the last successful refresh verified the map against, or None if unknown.
5036
@@ -53,7 +39,7 @@ def verified_sha(cwd=None):
5339
caller must read that as undeterminable and fall back to the wall-clock age rule, not
5440
as a failure -- an absent ref is not evidence of a broken refresh.
5541
"""
56-
rev = subprocess.run(["git", "rev-parse", "--verify", "--quiet", f"{VERIFIED_REF}^{{commit}}"], capture_output=True, text=True, check=False, cwd=cwd, env=_git_env())
42+
rev = _git(["rev-parse", "--verify", "--quiet", f"{VERIFIED_REF}^{{commit}}"], cwd)
5743
return rev.stdout.strip() or None
5844

5945

@@ -66,10 +52,10 @@ def verified_after_last_change(git_sha, cwd=None):
6652
"""
6753
if not git_sha:
6854
return None
69-
last = subprocess.run(["git", "log", "-1", "--format=%H", "--", *COVERAGE_RELEVANT_PATHS], capture_output=True, text=True, check=False, cwd=cwd, env=_git_env())
55+
last = _git(["log", "-1", "--format=%H", "--", *COVERAGE_RELEVANT_PATHS], cwd)
7056
if last.returncode != 0 or not last.stdout.strip():
7157
return None # shallow clone or no such commit -> fall back to the age rule
72-
ancestor = subprocess.run(["git", "merge-base", "--is-ancestor", last.stdout.strip(), git_sha], capture_output=True, check=False, cwd=cwd, env=_git_env())
58+
ancestor = _git(["merge-base", "--is-ancestor", last.stdout.strip(), git_sha], cwd)
7359
return {0: True, 1: False}.get(ancestor.returncode) # anything else -> None (unknown sha, shallow history)
7460

7561

.github/scripts/ci-outage.sh

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#!/bin/bash
2+
# Per-cluster circuit breaker for environment-wide outages.
3+
#
4+
# Some failures are not the node's fault and not the code's fault: pypi.org
5+
# unreachable from a login node, a module tree mid-upgrade, a full project
6+
# filesystem. Requeuing elsewhere cannot help, and every job that starts pays
7+
# the same discovery cost -- on 2026-08-28, 17 Frontier jobs each spent ~33
8+
# minutes learning that PyPI was down.
9+
#
10+
# The first job to notice records a marker on the shared filesystem (every
11+
# self-hosted runner for a cluster shares $HOME); later jobs check it and exit
12+
# immediately instead of submitting a SLURM job that is going to fail.
13+
#
14+
# The breaker is deliberately self-healing. A marker expires after
15+
# MFC_CI_OUTAGE_TTL_SECONDS, and a marker that cannot be parsed is ignored, so
16+
# neither a stale file nor a truncated write can wedge CI. Only jobs that
17+
# actually observe the outage re-mark it, so once the outage clears the breaker
18+
# closes on its own.
19+
#
20+
# Usage:
21+
# ci-outage.sh mark <cluster> <reason> record an outage
22+
# ci-outage.sh check <cluster> exit 0 = clear, 1 = outage active
23+
# ci-outage.sh clear <cluster> reset the breaker
24+
#
25+
# Env:
26+
# MFC_CI_STATE_DIR where markers live (default ~/.mfc-ci-state)
27+
# MFC_CI_OUTAGE_TTL_SECONDS marker lifetime in seconds (default 1200)
28+
29+
set -uo pipefail
30+
31+
STATE_DIR="${MFC_CI_STATE_DIR:-$HOME/.mfc-ci-state}"
32+
TTL="${MFC_CI_OUTAGE_TTL_SECONDS:-1200}"
33+
34+
# A non-numeric TTL would make the age comparison below emit "integer expression
35+
# expected" and exit with a code the caller reads as neither clear nor tripped.
36+
# Fall back to the default rather than letting a typo gate CI.
37+
case "$TTL" in
38+
''|*[!0-9]*)
39+
echo "Ignoring non-numeric MFC_CI_OUTAGE_TTL_SECONDS='$TTL'; using 1200." >&2
40+
TTL=1200
41+
;;
42+
esac
43+
44+
EXIT_CLEAR=0
45+
EXIT_TRIPPED=1
46+
EXIT_USAGE=2
47+
48+
usage() {
49+
echo "Usage: $0 {mark <cluster> <reason>|check <cluster>|clear <cluster>}" >&2
50+
}
51+
52+
# Keep the marker name filesystem-safe regardless of what the caller passes.
53+
marker_for() {
54+
local cluster
55+
cluster=$(printf '%s' "$1" | tr -c 'A-Za-z0-9_.-' '_')
56+
printf '%s/outage-%s' "$STATE_DIR" "$cluster"
57+
}
58+
59+
cmd="${1:-}"
60+
cluster="${2:-}"
61+
62+
if [ -z "$cmd" ] || [ -z "$cluster" ]; then
63+
usage
64+
exit $EXIT_USAGE
65+
fi
66+
67+
marker=$(marker_for "$cluster")
68+
69+
case "$cmd" in
70+
mark)
71+
reason="${3:-unspecified}"
72+
mkdir -p "$STATE_DIR" || exit $EXIT_USAGE
73+
# Write to a temporary file and rename so a concurrent `check` never
74+
# observes a half-written marker.
75+
tmp="${marker}.$$.tmp"
76+
{
77+
date +%s
78+
printf '%s\n' "$reason"
79+
} > "$tmp" && mv -f "$tmp" "$marker"
80+
echo "Recorded $cluster outage: $reason"
81+
echo " marker: $marker (expires after ${TTL}s)"
82+
;;
83+
84+
check)
85+
[ -f "$marker" ] || exit $EXIT_CLEAR
86+
87+
stamp=$(head -n1 "$marker" 2>/dev/null)
88+
reason=$(tail -n +2 "$marker" 2>/dev/null)
89+
90+
# A marker we cannot parse is treated as absent: an unreadable breaker
91+
# must never be an un-clearable one.
92+
case "$stamp" in
93+
''|*[!0-9]*)
94+
echo "Ignoring unparseable outage marker $marker"
95+
exit $EXIT_CLEAR
96+
;;
97+
esac
98+
99+
age=$(( $(date +%s) - stamp ))
100+
if [ "$age" -ge "$TTL" ] || [ "$age" -lt 0 ]; then
101+
exit $EXIT_CLEAR
102+
fi
103+
104+
echo "::warning::Skipping: known $cluster outage recorded ${age}s ago: ${reason:-unspecified}"
105+
echo "Clear it early by deleting $marker"
106+
exit $EXIT_TRIPPED
107+
;;
108+
109+
clear)
110+
rm -f "$marker"
111+
echo "Cleared any $cluster outage marker ($marker)"
112+
;;
113+
114+
*)
115+
usage
116+
exit $EXIT_USAGE
117+
;;
118+
esac
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/bin/bash
2+
# Decide whether a failed build was a cluster-wide dependency outage.
3+
#
4+
# MFC bootstraps its Python toolchain into build/venv on the first ./mfc.sh call
5+
# of a job, pulling from pypi.org. On Phoenix clean_build has just moved build/
6+
# aside, so that happens every time; on Frontier it happens in the login-node
7+
# "Fetch Dependencies" step. When the index is unreachable the build fails for a
8+
# reason no other node improves on, so it is worth recording once and skipping
9+
# the rest of the matrix rather than having each job spend ~33 minutes
10+
# rediscovering it (17 Frontier jobs did exactly that on 2026-08-28).
11+
#
12+
# Usage: classify-build-failure.sh <logfile> <cluster>
13+
#
14+
# Exit codes:
15+
# 78 cluster-wide dependency outage; it has been recorded
16+
# 0 ordinary build failure, caller should keep its own exit code
17+
18+
set -uo pipefail
19+
20+
log="${1:-}"
21+
cluster="${2:-}"
22+
23+
if [ -z "$log" ] || [ -z "$cluster" ]; then
24+
echo "Usage: $0 <logfile> <cluster>" >&2
25+
exit 0
26+
fi
27+
28+
# No log means nothing to classify. Never claim an outage on absent evidence.
29+
[ -f "$log" ] || exit 0
30+
31+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
32+
33+
# The URL may or may not be wrapped (uv quotes it in backticks, plain pip does
34+
# not), so do not require a character between the colon and the scheme.
35+
if grep -qE "Failed to fetch:[^h]*https?://pypi|uv install failed|\(venv\) Installation failed" "$log"; then
36+
bash "$SCRIPT_DIR/ci-outage.sh" mark "$cluster" \
37+
"PyPI/uv dependency install failed during build"
38+
exit 78
39+
fi
40+
41+
exit 0

.github/scripts/monitor_slurm_job.sh

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ if ! [[ "$SLURM_MAX_QUEUE_SECONDS" =~ ^[0-9]+$ ]]; then
9999
echo "ERROR: SLURM_MAX_QUEUE_SECONDS must be a non-negative integer (seconds), got '$SLURM_MAX_QUEUE_SECONDS'" >&2
100100
exit 1
101101
fi
102+
# How long to wait between status polls and between output-stabilization
103+
# checks. Overridable so tests can exercise this script without sleeping
104+
# through it; CI leaves it at the default.
105+
: "${MFC_MONITOR_POLL_SECONDS:=5}"
106+
102107
queue_start=$(date +%s)
103108

104109
abort_queue_starvation() {
@@ -142,7 +147,7 @@ while [ ! -f "$output_file" ]; do
142147
;;
143148
PENDING|CONFIGURING)
144149
unknown_count=0
145-
sleep 5
150+
sleep "$MFC_MONITOR_POLL_SECONDS"
146151
;;
147152
RUNNING|COMPLETING)
148153
unknown_count=0
@@ -155,7 +160,7 @@ while [ ! -f "$output_file" ]; do
155160
if [ $((unknown_count % 12)) -eq 1 ]; then
156161
echo "Warning: Could not query job $job_id state (SLURM may be temporarily unavailable)..."
157162
fi
158-
sleep 5
163+
sleep "$MFC_MONITOR_POLL_SECONDS"
159164
;;
160165
*)
161166
# Terminal state — job finished without creating output
@@ -164,7 +169,7 @@ while [ ! -f "$output_file" ]; do
164169
exit 1
165170
fi
166171
# Unrecognized state, keep waiting
167-
sleep 5
172+
sleep "$MFC_MONITOR_POLL_SECONDS"
168173
;;
169174
esac
170175
done
@@ -205,7 +210,7 @@ while true; do
205210
last_heartbeat=$current_time
206211
fi
207212

208-
sleep 5
213+
sleep "$MFC_MONITOR_POLL_SECONDS"
209214
done
210215

211216
# Give tail a moment to flush the final lines, then stop streaming.
@@ -229,7 +234,7 @@ if [ -f "$output_file" ]; then
229234
if [ $same_count -ge 2 ]; then
230235
break
231236
fi
232-
sleep 5
237+
sleep "$MFC_MONITOR_POLL_SECONDS"
233238
done
234239
fi
235240

@@ -262,6 +267,23 @@ if [ -z "$exit_code" ]; then
262267
exit 1
263268
fi
264269

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).
274+
case "$exit_code" in
275+
77:*)
276+
echo "Job $job_id failed preflight: the node is unusable — signaling caller to exclude it and resubmit."
277+
monitor_success=1
278+
exit 77
279+
;;
280+
78:*)
281+
echo "Job $job_id skipped: a cluster-wide outage is already recorded."
282+
monitor_success=1
283+
exit 78
284+
;;
285+
esac
286+
265287
# Check if job succeeded
266288
if [ "$exit_code" != "0:0" ]; then
267289
echo "ERROR: Job $job_id failed with exit code $exit_code"

0 commit comments

Comments
 (0)