From c04c847e0a80e8cc324747636f1cdd73a549d5ac Mon Sep 17 00:00:00 2001 From: yoshifuminakamura Date: Tue, 15 Sep 2026 08:58:00 +0900 Subject: [PATCH] Record node status snapshots before runs Signed-off-by: yoshifuminakamura --- docs/guides/add-app.md | 13 + result_server/routes/results_detail_routes.py | 23 + result_server/templates/result_detail.html | 4 + .../tests/test_measurement_artifacts.py | 6 + .../tests/test_result_detail_template.py | 83 ++++ result_server/utils/result_detail_view.py | 188 ++++++++ scripts/collect_node_status_snapshot.sh | 422 ++++++++++++++++++ scripts/matrix_generate.sh | 2 + scripts/result.sh | 51 ++- scripts/result_server/send_results.sh | 3 +- scripts/tests/run_profile_data_shell_tests.sh | 1 + scripts/tests/test_node_status_snapshot.sh | 137 ++++++ .../tests/test_process_and_send_results.sh | 35 +- .../tests/test_result_common_json_contract.sh | 63 ++- scripts/tests/test_scheduler_extra_args.sh | 2 +- 15 files changed, 1028 insertions(+), 5 deletions(-) create mode 100644 scripts/collect_node_status_snapshot.sh create mode 100644 scripts/tests/test_node_status_snapshot.sh diff --git a/docs/guides/add-app.md b/docs/guides/add-app.md index 3e6f79d7..f223d1a7 100644 --- a/docs/guides/add-app.md +++ b/docs/guides/add-app.md @@ -243,6 +243,19 @@ CI の build job では `scripts/build_tool_wrappers/` を `PATH` の先頭に この actual build snapshot は、将来の build cache key や、同じ source から異なる binary が生じた場合の原因確認に使う前提の記録です。 +### run placement / node status snapshot の方針 + +CI の共通 job は、benchmark 本体の `run_start` より前に `results/node_status_snapshot_run.json` を記録します。 +app の `run.sh` からこの snapshot 用 helper を呼ぶ必要はありません。 + +この snapshot は scheduler-neutral な診断情報で、scheduler kind、scheduler が示す node list、観測できた host 数、CPU 数、memory、load average、GPU の軽量状態、run 前に見える GPU compute process の件数と memory 合計を記録します。 +process ID、process name、user name は記録しません。 +SLURM では可能なら allocation 内の各 node で軽い worker を動かします。 +PBS / PJM / unknown scheduler では、取れる範囲の nodefile / environment / local host 情報だけを partial または unsupported として残します。 + +Result JSON には hash、summary、Measurement Artifact への参照だけを入れ、詳細な node list や GPU 状態は `results/node_status_snapshot_run.json` 側に残します。 +Portal の public surface ではこの Measurement Artifact は表示しません。 + ### build cache の方針 cross build job と native `build_run` job の build phase では、共通 wrapper `scripts/build_with_cache.sh` が `build.sh` の前後で build artifact cache を扱います。 diff --git a/result_server/routes/results_detail_routes.py b/result_server/routes/results_detail_routes.py index 84dd90b3..552f11fb 100644 --- a/result_server/routes/results_detail_routes.py +++ b/result_server/routes/results_detail_routes.py @@ -383,6 +383,17 @@ def _list_result_measurement_artifact_filenames(result, artifact_dir, *, include seen.add(filename) if os.path.isfile(os.path.join(artifact_dir, filename)): filenames.append(filename) + for artifact_path in _iter_result_node_status_artifact_paths(result): + filename = stored_measurement_artifact_filename_from_path( + timestamp, + result_uuid, + artifact_path, + ) + if not filename or filename in seen: + continue + seen.add(filename) + if os.path.isfile(os.path.join(artifact_dir, filename)): + filenames.append(filename) return filenames @@ -417,5 +428,17 @@ def _iter_result_timing_artifact_paths(result): yield path +def _iter_result_node_status_artifact_paths(result): + node_status_snapshot = result.get("node_status_snapshot") + if not isinstance(node_status_snapshot, dict): + return + artifact = node_status_snapshot.get("artifact") + if not isinstance(artifact, dict) or artifact.get("type") != "file_reference": + return + path = _clean_result_value(artifact.get("path")) + if path: + yield path + + def _clean_result_value(value): return str(value or "").strip() diff --git a/result_server/templates/result_detail.html b/result_server/templates/result_detail.html index 396666a9..a554a265 100644 --- a/result_server/templates/result_detail.html +++ b/result_server/templates/result_detail.html @@ -73,6 +73,10 @@ {{ render_titled_key_value_table("PA Data Summary", profile_rows, "meta-table") }} {% endif %} +{% if node_status_rows %} +{{ render_titled_key_value_table("Node Status Snapshot", node_status_rows, "meta-table") }} +{% endif %} + {% if measurement_artifact_rows %}

Measurement Artifacts

diff --git a/result_server/tests/test_measurement_artifacts.py b/result_server/tests/test_measurement_artifacts.py index f7a26a8d..6f7a1a79 100644 --- a/result_server/tests/test_measurement_artifacts.py +++ b/result_server/tests/test_measurement_artifacts.py @@ -62,6 +62,11 @@ def test_builds_canonical_stored_names_for_legacy_profiles_and_timing_json(): UUID, "results/qws_timing_CASE0.json", ) == f"measurement_artifact_{TIMESTAMP}_{UUID}_qws_timing_CASE0.json" + assert stored_measurement_artifact_filename_from_path( + TIMESTAMP, + UUID, + "results/node_status_snapshot_run.json", + ) == f"measurement_artifact_{TIMESTAMP}_{UUID}_node_status_snapshot_run.json" def test_profile_archive_candidates_keep_legacy_and_generic_names(): @@ -81,6 +86,7 @@ def test_profile_archive_candidates_keep_legacy_and_generic_names(): f"padata_{TIMESTAMP}_{UUID}.tgz", f"padata_{TIMESTAMP}_{UUID}_padata_pairlist.tgz", f"measurement_artifact_{TIMESTAMP}_{UUID}_qws_timing_CASE0.json", + f"measurement_artifact_{TIMESTAMP}_{UUID}_node_status_snapshot_run.json", ], ) def test_recognizes_served_measurement_artifact_filenames(filename): diff --git a/result_server/tests/test_result_detail_template.py b/result_server/tests/test_result_detail_template.py index e34ceaa6..57baf9a2 100644 --- a/result_server/tests/test_result_detail_template.py +++ b/result_server/tests/test_result_detail_template.py @@ -141,6 +141,31 @@ def app(): } ], }, + "node_status_snapshot": { + "schema_version": 1, + "kind": "node_status_snapshot", + "collection_status": "ok", + "collection_warnings": [], + "scheduler_kind": "slurm", + "summary": { + "scheduler_host_count": 2, + "observed_host_count": 2, + "cpu_logical_counts": [64], + "memory_total_mib_min": 262144, + "memory_total_mib_max": 262144, + "memory_available_mib_min": 131072, + "load_average_1m_max": 0.5, + "load_average_5m_max": 0.8, + "observed_gpu_count": 4, + "gpu_memory_used_total_mib": 0, + "gpu_compute_process_count": 0, + "gpu_compute_memory_used_mib": 0, + }, + "artifact": { + "type": "file_reference", + "path": "results/node_status_snapshot_run.json", + }, + }, } FULL_QUALITY = { @@ -204,6 +229,20 @@ def test_meta_info_section(self, app): assert "build inputs hash matched" in html assert "rccs-cloud" in html assert "slurm" in html + assert "Node Status Snapshot" in html + assert "Hosts Observed" in html + assert "2/2" in html + assert "CPU Counts Observed" in html + assert "64" in html + assert "Host Memory Total" in html + assert "262144.000 MiB" in html + assert "Min Memory Available Before Run" in html + assert "131072.000 MiB" in html + assert "Max Load Average Before Run" in html + assert "1m=0.500; 5m=0.800" in html + assert "Compute Processes Before Run" in html + assert "0 process(es); 0.000 MiB" in html + assert "results/node_status_snapshot_run.json" in html assert "Timing Observations" in html assert "qws-case0-timers" in html assert "producer=qws" in html @@ -233,6 +272,8 @@ def test_public_surface_meta_omits_operator_fields(self, app): assert "Cached Binary Created At" not in html assert "Allocation Project ID" not in html assert "Runner" not in html + assert "Node Status Snapshot" not in html + assert "node_status_snapshot_run" not in html assert "Timing Observations" not in html assert "qws_timing_CASE0" not in html assert "rccs-cloud" not in html @@ -398,6 +439,48 @@ def test_timing_observation_artifact_is_hidden_on_public_surface(self, app): assert "Timing observation" not in html assert filename not in html + def test_node_status_snapshot_artifact_is_linked_on_console_surface(self, app): + result = { + **FULL_RESULT, + "_server_uuid": "12345678-1234-1234-1234-123456789abc", + "_server_timestamp": "20260819_161329", + } + filename = ( + "measurement_artifact_20260819_161329_" + "12345678-1234-1234-1234-123456789abc_node_status_snapshot_run.json" + ) + + with app.test_request_context(): + html = _render_result_detail(result, FULL_QUALITY, [filename]) + + assert "Measurement Artifacts" in html + assert "Node status snapshot" in html + assert "Run placement" in html + assert "results/node_status_snapshot_run.json" in html + assert f'href="/results/{filename}"' in html + + def test_node_status_snapshot_artifact_is_hidden_on_public_surface(self, app): + result = { + **FULL_RESULT, + "_server_uuid": "12345678-1234-1234-1234-123456789abc", + "_server_timestamp": "20260819_161329", + } + filename = ( + "measurement_artifact_20260819_161329_" + "12345678-1234-1234-1234-123456789abc_node_status_snapshot_run.json" + ) + + with app.test_request_context(): + html = _render_result_detail( + result, + FULL_QUALITY, + [filename], + public_surface=True, + ) + + assert "Node status snapshot" not in html + assert filename not in html + def test_vector_data_table(self, app): with app.test_request_context(): html = _render_result_detail(FULL_RESULT, FULL_QUALITY) diff --git a/result_server/utils/result_detail_view.py b/result_server/utils/result_detail_view.py index 90ae6c3a..81422048 100644 --- a/result_server/utils/result_detail_view.py +++ b/result_server/utils/result_detail_view.py @@ -115,6 +115,9 @@ def build_result_detail_context( measurement_artifact_filenames or [], include_timing=not public_surface, ), + "node_status_rows": ( + [] if public_surface else _build_node_status_rows(result.get("node_status_snapshot")) + ), "timing_observation_rows": ( [] if public_surface else _build_timing_observation_rows(result.get("timing_observations")) ), @@ -230,6 +233,11 @@ def _build_measurement_artifact_rows( rows.extend( _build_timing_measurement_artifact_rows(result, timestamp, result_uuid, uploaded) ) + rows.extend( + _build_node_status_measurement_artifact_rows( + result, timestamp, result_uuid, uploaded + ) + ) return rows @@ -310,6 +318,186 @@ def _build_timing_measurement_artifact_rows(result, timestamp, result_uuid, uplo return rows +def _build_node_status_measurement_artifact_rows(result, timestamp, result_uuid, uploaded): + node_status_snapshot = result.get("node_status_snapshot") + if not isinstance(node_status_snapshot, dict): + return [] + + artifact = node_status_snapshot.get("artifact") + artifact = artifact if isinstance(artifact, dict) else {} + if artifact.get("type") != "file_reference": + return [] + + artifact_path = str(artifact.get("path") or "").strip() + filename = stored_measurement_artifact_filename_from_path( + timestamp, + result_uuid, + artifact_path, + ) + if not filename: + return [] + + return [ + { + "kind": "Node status snapshot", + "source": "Run placement", + "artifact_path": artifact_path, + "filename": filename, + "link": ( + url_for("results.show_result", filename=filename) + if filename in uploaded + else None + ), + } + ] + + +def _build_node_status_rows(node_status_snapshot): + if not isinstance(node_status_snapshot, dict): + return [] + + summary = node_status_snapshot.get("summary") + summary = summary if isinstance(summary, dict) else {} + artifact = node_status_snapshot.get("artifact") + artifact = artifact if isinstance(artifact, dict) else {} + warnings = _unique_strings( + list(_list_values(node_status_snapshot.get("collection_warnings"))) + + list(_list_values(summary.get("warnings"))) + ) + + rows = build_labeled_value_rows( + [ + ("Status", node_status_snapshot.get("collection_status") or "unknown"), + ("Scheduler", node_status_snapshot.get("scheduler_kind") or "unknown"), + ( + "Hosts Observed", + _format_observed_total( + summary.get("observed_host_count"), + summary.get("scheduler_host_count"), + ), + ), + ( + "CPU Counts Observed", + _format_value_list(summary.get("cpu_logical_counts")), + ), + ( + "Host Memory Total", + _format_mib_range( + summary.get("memory_total_mib_min"), + summary.get("memory_total_mib_max"), + ), + ), + ( + "Min Memory Available Before Run", + _format_mib(summary.get("memory_available_mib_min")), + ), + ( + "Max Load Average Before Run", + _format_load_summary( + summary.get("load_average_1m_max"), + summary.get("load_average_5m_max"), + ), + ), + ("GPUs Observed", summary.get("observed_gpu_count")), + ( + "GPU Memory Used Before Run", + _format_mib(summary.get("gpu_memory_used_total_mib")), + ), + ( + "Compute Processes Before Run", + _format_process_summary( + summary.get("gpu_compute_process_count"), + summary.get("gpu_compute_memory_used_mib"), + ), + ), + ("Snapshot Hash", node_status_snapshot.get("hash")), + ] + ) + if warnings: + rows.append({"label": "Warnings", "value": ", ".join(str(item) for item in warnings)}) + if artifact.get("path"): + rows.append({"label": "Artifact", "value": artifact["path"]}) + return rows + + +def _list_values(value): + if isinstance(value, list): + return value + if value in (None, ""): + return [] + return [value] + + +def _unique_strings(values): + unique_values = [] + seen = set() + for value in values: + text = str(value) + if text in seen: + continue + seen.add(text) + unique_values.append(text) + return unique_values + + +def _format_observed_total(observed, total): + if observed in (None, "") and total in (None, ""): + return "not recorded" + if total in (None, ""): + return str(observed) + return f"{observed or 0}/{total}" + + +def _format_mib(value): + if value in (None, ""): + return "not recorded" + return f"{format_numeric_value(value)} MiB" + + +def _format_mib_range(min_value, max_value): + if min_value in (None, "") and max_value in (None, ""): + return "not recorded" + if min_value == max_value or max_value in (None, ""): + return _format_mib(min_value) + if min_value in (None, ""): + return _format_mib(max_value) + return f"{format_numeric_value(min_value)}-{format_numeric_value(max_value)} MiB" + + +def _format_value_list(values): + if not isinstance(values, list) or not values: + return "not recorded" + return ", ".join(_format_count_value(value) for value in values) + + +def _format_load_summary(one_minute, five_minutes): + if one_minute in (None, "") and five_minutes in (None, ""): + return "not recorded" + values = [] + if one_minute not in (None, ""): + values.append(f"1m={format_numeric_value(one_minute)}") + if five_minutes not in (None, ""): + values.append(f"5m={format_numeric_value(five_minutes)}") + return "; ".join(values) + + +def _format_count_value(value): + try: + numeric_value = float(value) + except (TypeError, ValueError): + return str(value) + if numeric_value.is_integer(): + return str(int(numeric_value)) + return format_numeric_value(value) + + +def _format_process_summary(count, memory_mib): + if count in (None, ""): + return "not recorded" + memory_text = _format_mib(memory_mib) + return f"{count} process(es); {memory_text}" + + def _choose_uploaded_filename(candidates, uploaded): for filename in candidates: if filename in uploaded: diff --git a/scripts/collect_node_status_snapshot.sh b/scripts/collect_node_status_snapshot.sh new file mode 100644 index 00000000..12a2dfb5 --- /dev/null +++ b/scripts/collect_node_status_snapshot.sh @@ -0,0 +1,422 @@ +#!/bin/bash +set -euo pipefail + +out_file="${1:-results/node_status_snapshot_run.json}" +snapshot_stage="${BK_NODE_STATUS_SNAPSHOT_STAGE:-run}" +script_path="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")" + +json_string_array() { + jq -R -s -c 'split("\n") | map(select(length > 0))' +} + +json_lines_to_array() { + jq -s -c 'unique_by(.hostname // "")' +} + +detect_scheduler_kind() { + if [ -n "${SLURM_JOB_ID:-}" ] || [ -n "${SLURM_JOBID:-}" ]; then + printf '%s\n' "slurm" + elif [ -n "${PJM_JOBID:-}" ] || [ -n "${PJM_JOB_ID:-}" ]; then + printf '%s\n' "pjm" + elif [ -n "${PBS_JOBID:-}" ]; then + printf '%s\n' "pbs" + else + printf '%s\n' "unknown" + fi +} + +collect_scheduler_hosts() { + local kind="$1" + local nodelist="" + local nodefile="" + + case "$kind" in + slurm) + nodelist="${SLURM_JOB_NODELIST:-${SLURM_NODELIST:-}}" + if [ -n "$nodelist" ] && command -v scontrol >/dev/null 2>&1; then + scontrol show hostnames "$nodelist" 2>/dev/null || true + elif [ -n "$nodelist" ]; then + printf '%s\n' "$nodelist" + fi + ;; + pbs) + nodefile="${PBS_NODEFILE:-}" + if [ -n "$nodefile" ] && [ -r "$nodefile" ]; then + awk 'NF {print $1}' "$nodefile" | sort -u + fi + ;; + pjm) + nodefile="${PJM_NODEINF:-${PJM_O_NODEINF:-}}" + if [ -n "$nodefile" ] && [ -r "$nodefile" ]; then + awk 'NF {print $1}' "$nodefile" | sort -u + elif [ -n "${PJM_NODE:-}" ]; then + printf '%s\n' "${PJM_NODE}" | tr ',' '\n' + fi + ;; + esac +} + +trimmed_number_or_zero() { + awk ' + { + value = $0 + gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) + if (value ~ /^[0-9]+$/) { + total += value + } + } + END { print total + 0 } + ' +} + +count_compute_app_rows() { + awk -F, ' + { + pid = $1 + gsub(/^[[:space:]]+|[[:space:]]+$/, "", pid) + if (pid ~ /^[0-9]+$/) { + count += 1 + } + } + END {print count + 0} + ' +} + +collect_gpu_state_json() { + local gpu_csv="" + local gpus_json="[]" + local gpu_query_status="unavailable" + local compute_apps_csv="" + local compute_process_count=0 + local compute_memory_used_mib=0 + + if command -v nvidia-smi >/dev/null 2>&1; then + if gpu_csv=$(nvidia-smi \ + --query-gpu=index,name,memory.used,memory.total,utilization.gpu,temperature.gpu,clocks.sm,clocks.mem,pstate \ + --format=csv,noheader,nounits 2>/dev/null); then + gpu_query_status="ok" + gpus_json=$(printf '%s\n' "$gpu_csv" | jq -R -s -c ' + def trim: sub("^[[:space:]]+"; "") | sub("[[:space:]]+$"; ""); + def maybe_number: tonumber? // null; + split("\n") + | map(select(length > 0)) + | map( + split(",") + | map(trim) + | { + index: (.[0] // ""), + name: (.[1] // ""), + memory_used_mib: ((.[2] // "") | maybe_number), + memory_total_mib: ((.[3] // "") | maybe_number), + utilization_gpu_percent: ((.[4] // "") | maybe_number), + temperature_c: ((.[5] // "") | maybe_number), + clocks_sm_mhz: ((.[6] // "") | maybe_number), + clocks_mem_mhz: ((.[7] // "") | maybe_number), + pstate: (.[8] // "") + } + ) + ') + else + gpu_query_status="failed" + fi + + compute_apps_csv=$(nvidia-smi \ + --query-compute-apps=pid,used_memory \ + --format=csv,noheader,nounits 2>/dev/null || true) + compute_process_count=$(printf '%s\n' "$compute_apps_csv" | count_compute_app_rows) + compute_memory_used_mib=$( + printf '%s\n' "$compute_apps_csv" \ + | awk -F, '{print $2}' \ + | trimmed_number_or_zero + ) + fi + + jq -n -c \ + --arg query_status "$gpu_query_status" \ + --argjson gpus "$gpus_json" \ + --argjson compute_process_count "$compute_process_count" \ + --argjson compute_memory_used_mib "$compute_memory_used_mib" \ + '{ + available: ($query_status == "ok"), + query_status: $query_status, + gpus: $gpus, + process_summary: { + compute_process_count: $compute_process_count, + memory_used_mib: $compute_memory_used_mib + } + }' +} + +collect_host_state_json() { + local cpu_logical_count="" + local memory_total_kib="" + local memory_available_kib="" + local load_average_1m="" + local load_average_5m="" + local load_average_15m="" + + cpu_logical_count=$(getconf _NPROCESSORS_ONLN 2>/dev/null || nproc 2>/dev/null || true) + if [ -r /proc/meminfo ]; then + memory_total_kib=$(awk '/^MemTotal:/ {print $2; exit}' /proc/meminfo) + memory_available_kib=$(awk '/^MemAvailable:/ {print $2; exit}' /proc/meminfo) + fi + if [ -r /proc/loadavg ]; then + read -r load_average_1m load_average_5m load_average_15m _ < /proc/loadavg || true + fi + + jq -n -c \ + --arg cpu_logical_count "$cpu_logical_count" \ + --arg memory_total_kib "$memory_total_kib" \ + --arg memory_available_kib "$memory_available_kib" \ + --arg load_average_1m "$load_average_1m" \ + --arg load_average_5m "$load_average_5m" \ + --arg load_average_15m "$load_average_15m" \ + '{ + cpu_logical_count: ($cpu_logical_count | tonumber?), + memory_total_mib: (($memory_total_kib | tonumber?) as $v | if $v == null then null else $v / 1024 end), + memory_available_mib: (($memory_available_kib | tonumber?) as $v | if $v == null then null else $v / 1024 end), + load_average: { + one_minute: ($load_average_1m | tonumber?), + five_minutes: ($load_average_5m | tonumber?), + fifteen_minutes: ($load_average_15m | tonumber?) + } + }' +} + +collect_worker_json() { + local hostname_value="" + local host_state_json="{}" + local gpu_state_json="{}" + + hostname_value=$(hostname 2>/dev/null || printf '%s' "") + host_state_json=$(collect_host_state_json) + gpu_state_json=$(collect_gpu_state_json) + + jq -n -c \ + --arg hostname "$hostname_value" \ + --arg scheduler_kind "$(detect_scheduler_kind)" \ + --arg slurm_procid "${SLURM_PROCID:-}" \ + --arg slurm_localid "${SLURM_LOCALID:-}" \ + --arg ompi_rank "${OMPI_COMM_WORLD_RANK:-}" \ + --arg ompi_local_rank "${OMPI_COMM_WORLD_LOCAL_RANK:-}" \ + --arg cuda_visible_devices "${CUDA_VISIBLE_DEVICES:-}" \ + --argjson host_state "$host_state_json" \ + --argjson gpu_state "$gpu_state_json" \ + '{ + hostname: $hostname, + scheduler_kind: $scheduler_kind, + host_state: $host_state, + rank_environment: { + slurm_procid: $slurm_procid, + slurm_localid: $slurm_localid, + ompi_rank: $ompi_rank, + ompi_local_rank: $ompi_local_rank, + cuda_visible_devices: $cuda_visible_devices + }, + gpu_state: $gpu_state + }' +} + +remote_snapshot_enabled() { + case "${BK_NODE_STATUS_SNAPSHOT_REMOTE:-auto}" in + 0|false|False|FALSE|no|No|NO|off|Off|OFF) + return 1 + ;; + *) + return 0 + ;; + esac +} + +collect_slurm_remote_workers() { + local scheduler_host_count="$1" + local timeout_seconds="${BK_NODE_STATUS_SNAPSHOT_TIMEOUT_SECONDS:-20}" + local stdout_file="" + local stderr_file="" + local status=0 + + if ! remote_snapshot_enabled; then + return 1 + fi + if [ "$scheduler_host_count" -le 1 ]; then + return 1 + fi + if ! command -v srun >/dev/null 2>&1; then + return 1 + fi + + stdout_file=$(mktemp) + stderr_file=$(mktemp) + if command -v timeout >/dev/null 2>&1; then + timeout "${timeout_seconds}s" srun -N "$scheduler_host_count" -n "$scheduler_host_count" \ + --ntasks-per-node=1 bash "$script_path" --worker >"$stdout_file" 2>"$stderr_file" || status=$? + else + srun -N "$scheduler_host_count" -n "$scheduler_host_count" \ + --ntasks-per-node=1 bash "$script_path" --worker >"$stdout_file" 2>"$stderr_file" || status=$? + fi + + rm -f "$stderr_file" + if [ "$status" -ne 0 ]; then + rm -f "$stdout_file" + return 1 + fi + + awk '/^[[:space:]]*\{/ {print}' "$stdout_file" + rm -f "$stdout_file" +} + +build_summary_json() { + local scheduler_hosts_json="$1" + local hosts_json="$2" + + jq -n -c \ + --argjson scheduler_hosts "$scheduler_hosts_json" \ + --argjson hosts "$hosts_json" \ + ' + ($hosts | map(.gpu_state.gpus // []) | add // []) as $gpus + | ($hosts | map(.gpu_state.process_summary.compute_process_count // 0) | add // 0) as $process_count + | ($hosts | map(.gpu_state.process_summary.memory_used_mib // 0) | add // 0) as $process_memory + | ($gpus | map(.memory_used_mib // 0) | add // 0) as $gpu_memory + | ($hosts | map(.host_state.cpu_logical_count) | map(select(. != null)) | unique) as $cpu_counts + | ($hosts | map(.host_state.memory_total_mib) | map(select(. != null)) | unique) as $memory_totals + | ($hosts | map(.host_state.memory_available_mib) | map(select(. != null))) as $memory_available + | ($hosts | map(.host_state.load_average.one_minute) | map(select(. != null))) as $load_1m + | ($hosts | map(.host_state.load_average.five_minutes) | map(select(. != null))) as $load_5m + | { + scheduler_host_count: ($scheduler_hosts | length), + observed_host_count: ($hosts | length), + cpu_logical_counts: $cpu_counts, + memory_total_mib_min: ($memory_totals | min), + memory_total_mib_max: ($memory_totals | max), + memory_available_mib_min: ($memory_available | min), + load_average_1m_max: ($load_1m | max), + load_average_5m_max: ($load_5m | max), + observed_gpu_count: ($gpus | length), + gpu_memory_used_total_mib: $gpu_memory, + gpu_compute_process_count: $process_count, + gpu_compute_memory_used_mib: $process_memory, + gpu_query_statuses: ($hosts | map(.gpu_state.query_status // "unknown") | unique), + warnings: ( + [] + + (if (($scheduler_hosts | length) > 0 and ($hosts | length) != ($scheduler_hosts | length)) + then ["observed_host_count_differs_from_scheduler_host_count"] else [] end) + + (if ($cpu_counts | length) > 1 + then ["cpu_count_differs_across_observed_hosts"] else [] end) + + (if ($memory_totals | length) > 1 + then ["memory_total_differs_across_observed_hosts"] else [] end) + + (if $process_count > 0 + then ["gpu_compute_processes_present_before_run"] else [] end) + ) + }' +} + +if [ "${1:-}" = "--worker" ]; then + collect_worker_json + exit 0 +fi + +mkdir -p "$(dirname "$out_file")" + +scheduler_kind=$(detect_scheduler_kind) +scheduler_hosts_json=$(collect_scheduler_hosts "$scheduler_kind" | json_string_array) +scheduler_host_count=$(printf '%s' "$scheduler_hosts_json" | jq 'length') +collection_status="ok" +remote_collection_status="not_attempted" +collection_warnings_json="[]" + +worker_lines="" +if [ "$scheduler_kind" = "slurm" ]; then + if worker_lines=$(collect_slurm_remote_workers "$scheduler_host_count"); then + remote_collection_status="ok" + elif [ "$scheduler_host_count" -gt 1 ] && remote_snapshot_enabled; then + remote_collection_status="failed" + collection_status="partial" + collection_warnings_json='["slurm_remote_collection_failed"]' + fi +fi + +if [ -z "$worker_lines" ]; then + worker_lines=$(collect_worker_json) +fi + +if ! hosts_json=$(printf '%s\n' "$worker_lines" | json_lines_to_array 2>/dev/null); then + hosts_json=$(collect_worker_json | json_lines_to_array) + collection_status="partial" + collection_warnings_json='["worker_output_parse_failed"]' +fi + +if [ "$scheduler_kind" = "unknown" ]; then + collection_status="unsupported" +fi + +summary_json=$(build_summary_json "$scheduler_hosts_json" "$hosts_json") + +jq -n \ + --argjson schema_version 1 \ + --arg kind "node_status_snapshot" \ + --arg stage "$snapshot_stage" \ + --arg collected_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + --arg collection_status "$collection_status" \ + --arg remote_collection_status "$remote_collection_status" \ + --arg scheduler_kind "$scheduler_kind" \ + --arg slurm_job_id "${SLURM_JOB_ID:-${SLURM_JOBID:-}}" \ + --arg slurm_partition "${SLURM_JOB_PARTITION:-}" \ + --arg slurm_job_nodelist "${SLURM_JOB_NODELIST:-${SLURM_NODELIST:-}}" \ + --arg slurm_job_num_nodes "${SLURM_JOB_NUM_NODES:-${SLURM_NNODES:-}}" \ + --arg slurm_ntasks "${SLURM_NTASKS:-}" \ + --arg slurm_tasks_per_node "${SLURM_TASKS_PER_NODE:-}" \ + --arg slurm_cpus_per_task "${SLURM_CPUS_PER_TASK:-}" \ + --arg slurm_job_gpus "${SLURM_JOB_GPUS:-}" \ + --arg slurm_gpus_on_node "${SLURM_GPUS_ON_NODE:-}" \ + --arg slurm_gpus_per_node "${SLURM_GPUS_PER_NODE:-}" \ + --arg slurm_gpus_per_task "${SLURM_GPUS_PER_TASK:-}" \ + --arg pbs_jobid "${PBS_JOBID:-}" \ + --arg pbs_nodefile "${PBS_NODEFILE:-}" \ + --arg pjm_jobid "${PJM_JOBID:-${PJM_JOB_ID:-}}" \ + --arg pjm_nodeinf "${PJM_NODEINF:-${PJM_O_NODEINF:-}}" \ + --argjson scheduler_hosts "$scheduler_hosts_json" \ + --argjson observed_hosts "$hosts_json" \ + --argjson summary "$summary_json" \ + --argjson collection_warnings "$collection_warnings_json" \ + '{ + schema_version: $schema_version, + kind: $kind, + stage: $stage, + collected_at: $collected_at, + collection_status: $collection_status, + collection_warnings: $collection_warnings, + scheduler: { + kind: $scheduler_kind, + slurm: { + job_id: $slurm_job_id, + partition: $slurm_partition, + job_nodelist: $slurm_job_nodelist, + job_num_nodes: $slurm_job_num_nodes, + ntasks: $slurm_ntasks, + tasks_per_node: $slurm_tasks_per_node, + cpus_per_task: $slurm_cpus_per_task, + job_gpus: $slurm_job_gpus, + gpus_on_node: $slurm_gpus_on_node, + gpus_per_node: $slurm_gpus_per_node, + gpus_per_task: $slurm_gpus_per_task, + remote_collection_status: $remote_collection_status + }, + pbs: { + job_id: $pbs_jobid, + nodefile: $pbs_nodefile + }, + pjm: { + job_id: $pjm_jobid, + nodeinf: $pjm_nodeinf + } + }, + allocation: { + scheduler_hosts: $scheduler_hosts + }, + observed: { + hosts: $observed_hosts + }, + summary: $summary + }' > "$out_file" + +echo "Wrote node status snapshot: $out_file" diff --git a/scripts/matrix_generate.sh b/scripts/matrix_generate.sh index 829475ce..3459cdba 100644 --- a/scripts/matrix_generate.sh +++ b/scripts/matrix_generate.sh @@ -161,6 +161,7 @@ ${job_prefix}_run: - bash scripts/record_ci_timing_context.sh run - ls -la $program_path/ - BK_SYSTEM=\"$system\" BK_SNAPSHOT_STAGE=run bash scripts/collect_environment_snapshot.sh results/environment_snapshot_run.json + - BK_SYSTEM=\"$system\" BK_NODE_STATUS_SNAPSHOT_STAGE=run bash scripts/collect_node_status_snapshot.sh results/node_status_snapshot_run.json - bash scripts/record_timestamp.sh results/run_start - bash $program_path/run.sh $system $nodes ${numproc_node} ${nthreads} - bash scripts/record_timestamp.sh results/run_end @@ -215,6 +216,7 @@ ${job_prefix}_build_run: - bash scripts/record_timestamp.sh results/build_start - bash scripts/build_with_cache.sh $program $system $program_path - bash scripts/record_timestamp.sh results/build_end + - BK_SYSTEM=\"$system\" BK_NODE_STATUS_SNAPSHOT_STAGE=run bash scripts/collect_node_status_snapshot.sh results/node_status_snapshot_run.json - bash scripts/record_timestamp.sh results/run_start - bash $program_path/run.sh $system $nodes ${numproc_node} ${nthreads} - bash scripts/record_timestamp.sh results/run_end diff --git a/scripts/result.sh b/scripts/result.sh index 2690c8e7..3d3173c9 100644 --- a/scripts/result.sh +++ b/scripts/result.sh @@ -730,6 +730,49 @@ build_environment_snapshot_block() { environment_snapshot_block=$(build_environment_snapshot_block) +build_node_status_snapshot_block() { + local snapshot_file="results/node_status_snapshot_run.json" + + if [ ! -f "$snapshot_file" ]; then + printf '%s' "" + return 0 + fi + + local snapshot_json + if ! snapshot_json=$(jq -cS 'if type == "object" then . else error("node snapshot must be an object") end' "$snapshot_file" 2>/dev/null); then + echo "WARNING: results/node_status_snapshot_run.json is invalid; omitting node status snapshot summary" >&2 + printf '%s' "" + return 0 + fi + + local snapshot_hash + snapshot_hash=$(printf '%s' "$snapshot_json" | sha256_text 2>/dev/null || true) + if [ -z "$snapshot_hash" ]; then + printf '%s' "" + return 0 + fi + + jq -n -c \ + --arg hash "sha256:${snapshot_hash}" \ + --arg artifact_path "$snapshot_file" \ + --argjson snapshot "$snapshot_json" \ + '{ + schema_version: ($snapshot.schema_version // 1), + kind: ($snapshot.kind // "node_status_snapshot"), + hash: $hash, + collection_status: ($snapshot.collection_status // "unknown"), + collection_warnings: ($snapshot.collection_warnings // []), + scheduler_kind: ($snapshot.scheduler.kind // "unknown"), + summary: ($snapshot.summary // {}), + artifact: { + type: "file_reference", + path: $artifact_path + } + }' +} + +node_status_snapshot_block=$(build_node_status_snapshot_block) + build_input_info_block() { local input_info_file="results/input_info.json" @@ -988,6 +1031,12 @@ write_result_json() { \"build_cache\": ${build_cache_block}" fi + local node_status_snapshot_json_block="" + if [ -n "$node_status_snapshot_block" ]; then + node_status_snapshot_json_block=", + \"node_status_snapshot\": ${node_status_snapshot_block}" + fi + local input_info_json_block="" local result_input_info_block="" result_input_info_block=$(filter_input_info_block_for_result "$exp") @@ -1055,7 +1104,7 @@ write_result_json() { "nthreads": "$nthreads", "description": "$description", "confidential": "$confidential", - "source_info": $source_info_block${input_info_json_block}${timing_observations_json_block}${profile_data_block}${fom_breakdown_block}${timing_block}${mode_block}${trigger_block}${build_job_block}${run_job_block}${pipeline_id_block}${parent_pipeline_id_block}${execution_trigger_block}${environment_snapshot_json_block}${build_cache_json_block} + "source_info": $source_info_block${input_info_json_block}${timing_observations_json_block}${profile_data_block}${fom_breakdown_block}${timing_block}${mode_block}${trigger_block}${build_job_block}${run_job_block}${pipeline_id_block}${parent_pipeline_id_block}${execution_trigger_block}${environment_snapshot_json_block}${build_cache_json_block}${node_status_snapshot_json_block} } EOF diff --git a/scripts/result_server/send_results.sh b/scripts/result_server/send_results.sh index 57dbedd4..e83633fd 100644 --- a/scripts/result_server/send_results.sh +++ b/scripts/result_server/send_results.sh @@ -163,7 +163,8 @@ collect_measurement_artifacts_for_result() { def file_reference_paths: ((.fom_breakdown.sections // [])[]? | (.artifacts // [])[]? | select(.type == "file_reference") | .path // empty), ((.fom_breakdown.overlaps // [])[]? | (.artifacts // [])[]? | select(.type == "file_reference") | .path // empty), - ((.timing_observations.observations // [])[]? | .artifact? | select(.type == "file_reference") | .path // empty); + ((.timing_observations.observations // [])[]? | .artifact? | select(.type == "file_reference") | .path // empty), + (.node_status_snapshot.artifact? | select(.type == "file_reference") | .path // empty); file_reference_paths ' "$json_file" 2>/dev/null || true) } diff --git a/scripts/tests/run_profile_data_shell_tests.sh b/scripts/tests/run_profile_data_shell_tests.sh index 4dd9a272..4eefbd28 100644 --- a/scripts/tests/run_profile_data_shell_tests.sh +++ b/scripts/tests/run_profile_data_shell_tests.sh @@ -45,6 +45,7 @@ scripts/tests/test_bk_input_info.sh scripts/tests/test_bk_timing_observations.sh scripts/tests/test_qws_timing_artifact.sh scripts/tests/test_build_environment_snapshot.sh +scripts/tests/test_node_status_snapshot.sh scripts/tests/test_ci_timing_context.sh scripts/tests/test_ncu_plan_generation.sh scripts/tests/test_scheduler_extra_args.sh diff --git a/scripts/tests/test_node_status_snapshot.sh b/scripts/tests/test_node_status_snapshot.sh new file mode 100644 index 00000000..be99ccb3 --- /dev/null +++ b/scripts/tests/test_node_status_snapshot.sh @@ -0,0 +1,137 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +REPO_DIR=$(cd "${SCRIPT_DIR}/../.." && pwd) + +if ! command -v jq >/dev/null 2>&1; then + echo "jq not found; skipping node status snapshot test" + exit 0 +fi + +TMP_DIR=$(mktemp -d) +trap 'rm -rf "${TMP_DIR}"' EXIT + +mkdir -p "${TMP_DIR}/unknown/results" +pushd "${TMP_DIR}/unknown" >/dev/null +BK_NODE_STATUS_SNAPSHOT_REMOTE=0 \ + bash "${REPO_DIR}/scripts/collect_node_status_snapshot.sh" results/node_status_snapshot_run.json >/dev/null +jq -e ' + .schema_version == 1 and + .kind == "node_status_snapshot" and + .collection_status == "unsupported" and + .scheduler.kind == "unknown" and + .summary.observed_host_count == 1 and + (.summary.gpu_query_statuses | type) == "array" +' results/node_status_snapshot_run.json >/dev/null +popd >/dev/null + +mkdir -p "${TMP_DIR}/slurm/results" "${TMP_DIR}/bin" + +cat > "${TMP_DIR}/bin/hostname" <<'EOF' +#!/bin/bash +printf '%s\n' "${BK_TEST_HOSTNAME:-node-a}" +EOF + +cat > "${TMP_DIR}/bin/scontrol" <<'EOF' +#!/bin/bash +set -euo pipefail +if [ "${1:-}" = "show" ] && [ "${2:-}" = "hostnames" ]; then + printf '%s\n' node-a node-b + exit 0 +fi +exit 1 +EOF + +cat > "${TMP_DIR}/bin/srun" <<'EOF' +#!/bin/bash +set -euo pipefail +while [ "$#" -gt 0 ]; do + case "$1" in + -N|-n|--nodes|--ntasks|--ntasks-per-node) + shift 2 + ;; + --nodes=*|--ntasks=*|--ntasks-per-node=*) + shift + ;; + --) + shift + break + ;; + -*) + shift + ;; + *) + break + ;; + esac +done +BK_TEST_HOSTNAME=node-a "$@" +BK_TEST_HOSTNAME=node-b "$@" +EOF + +cat > "${TMP_DIR}/bin/nvidia-smi" <<'EOF' +#!/bin/bash +set -euo pipefail +case "$*" in + *--query-gpu=*) + if [ "${BK_TEST_HOSTNAME:-node-a}" = "node-b" ]; then + printf '%s\n' \ + "0, NVIDIA B200, 128, 183000, 0, 35, 1230, 2000, P0" \ + "1, NVIDIA B200, 256, 183000, 0, 36, 1230, 2000, P0" + else + printf '%s\n' \ + "0, NVIDIA B200, 0, 183000, 0, 33, 1230, 2000, P0" \ + "1, NVIDIA B200, 0, 183000, 0, 34, 1230, 2000, P0" + fi + ;; + *--query-compute-apps=*) + if [ "${BK_TEST_HOSTNAME:-node-a}" = "node-b" ]; then + printf '%s\n' "1234, 512" + fi + ;; + *) + exit 1 + ;; +esac +EOF + +chmod +x "${TMP_DIR}/bin/hostname" "${TMP_DIR}/bin/scontrol" "${TMP_DIR}/bin/srun" "${TMP_DIR}/bin/nvidia-smi" + +pushd "${TMP_DIR}/slurm" >/dev/null +PATH="${TMP_DIR}/bin:${PATH}" \ +SLURM_JOB_ID=123 \ +SLURM_JOB_PARTITION=gpu \ +SLURM_JOB_NODELIST="node-[a-b]" \ +SLURM_JOB_NUM_NODES=2 \ +SLURM_NTASKS=4 \ +SLURM_TASKS_PER_NODE="2(x2)" \ +SLURM_CPUS_PER_TASK=32 \ +SLURM_JOB_GPUS="0,1,2,3" \ +BK_NODE_STATUS_SNAPSHOT_TIMEOUT_SECONDS=5 \ + bash "${REPO_DIR}/scripts/collect_node_status_snapshot.sh" results/node_status_snapshot_run.json >/dev/null + +jq -e ' + .schema_version == 1 and + .kind == "node_status_snapshot" and + .collection_status == "ok" and + .scheduler.kind == "slurm" and + .scheduler.slurm.remote_collection_status == "ok" and + .allocation.scheduler_hosts == ["node-a", "node-b"] and + (.observed.hosts | length) == 2 and + .summary.scheduler_host_count == 2 and + .summary.observed_host_count == 2 and + .summary.observed_gpu_count == 4 and + .summary.gpu_memory_used_total_mib == 384 and + .summary.gpu_compute_process_count == 1 and + .summary.gpu_compute_memory_used_mib == 512 and + (.summary.warnings | index("gpu_compute_processes_present_before_run") != null) +' results/node_status_snapshot_run.json >/dev/null + +if jq -e 'tostring | contains("1234")' results/node_status_snapshot_run.json >/dev/null; then + echo "node status snapshot should not record process identifiers" >&2 + exit 1 +fi +popd >/dev/null + +echo "node status snapshot test passed" diff --git a/scripts/tests/test_process_and_send_results.sh b/scripts/tests/test_process_and_send_results.sh index 8e55678c..843e8aac 100644 --- a/scripts/tests/test_process_and_send_results.sh +++ b/scripts/tests/test_process_and_send_results.sh @@ -61,6 +61,29 @@ cat > "${TMP_DIR}/project/results/environment_snapshot_run.json" <<'EOF' } EOF +cat > "${TMP_DIR}/project/results/node_status_snapshot_run.json" <<'EOF' +{ + "schema_version": 1, + "kind": "node_status_snapshot", + "stage": "run", + "collected_at": "2026-09-14T00:01:30Z", + "collection_status": "ok", + "scheduler": { + "kind": "slurm" + }, + "summary": { + "scheduler_host_count": 2, + "observed_host_count": 2, + "observed_gpu_count": 4, + "gpu_memory_used_total_mib": 0, + "gpu_compute_process_count": 0, + "gpu_compute_memory_used_mib": 0, + "gpu_query_statuses": ["ok"], + "warnings": [] + } +} +EOF + cat > "${TMP_DIR}/project/results/input_info.json" <<'EOF' { "schema_version": 1, @@ -95,6 +118,9 @@ EOF cat > "${TMP_DIR}/bin/curl" <<'EOF' #!/bin/bash set -euo pipefail +if [ -n "${CURL_LOG:-}" ]; then + printf '%s\n' "$*" >> "$CURL_LOG" +fi if printf '%s\n' "$*" | grep -q '/api/ingest/result'; then printf '%s\n' '{"id":"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee","timestamp":"20260728_070000"}' exit 0 @@ -115,6 +141,7 @@ export BK_TRIGGER_ID="qws-fugaku-1400" export BK_TRIGGER_TYPE="scheduled" export BK_TRIGGER_REASON="cron:0 14 * * *@2026-08-07T14:00+09:00" export PARENT_PIPELINE_ID="54321" +export CURL_LOG="${TMP_DIR}/curl.log" pushd "${TMP_DIR}/project" >/dev/null bash scripts/result_server/process_and_send_results.sh qws Fugaku cross qws_Fugaku_build qws_Fugaku_N1_P2_T3_run 12345 > "${TMP_DIR}/process.log" @@ -165,7 +192,11 @@ jq -e ' (.pipeline_timing.run_time | type) == "number" and .pipeline_timing.run_time_scope == "job" and (.pipeline_timing | has("profiled_run_included") | not) and - (.execution_trigger | type) == "object" + (.execution_trigger | type) == "object" and + .node_status_snapshot.collection_status == "ok" and + .node_status_snapshot.scheduler_kind == "slurm" and + .node_status_snapshot.summary.observed_gpu_count == 4 and + .node_status_snapshot.artifact.path == "results/node_status_snapshot_run.json" ' "${TMP_DIR}/project/send_results_workspace/results/result0.json" >/dev/null jq -e ' .input_info.schema_version == 1 and @@ -184,6 +215,8 @@ jq -e ' ' "${TMP_DIR}/project/send_results_workspace/results/result0.json" >/dev/null jq -e '."result0.json".uuid == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"' \ "${TMP_DIR}/project/send_results_workspace/results/server_result_meta.json" >/dev/null +grep -q '/api/ingest/measurement-artifact' "${TMP_DIR}/curl.log" +grep -q 'file=@results/node_status_snapshot_run.json' "${TMP_DIR}/curl.log" grep -q "Result summary: code=qws system=Fugaku mode=cross exp=CASE0 fom=1.25 pipeline=12345" "${TMP_DIR}/process.log" if grep -q '"environment_snapshot"' "${TMP_DIR}/process.log"; then echo "process log should not include full result JSON" >&2 diff --git a/scripts/tests/test_result_common_json_contract.sh b/scripts/tests/test_result_common_json_contract.sh index 4bcabbf2..9247bcbb 100644 --- a/scripts/tests/test_result_common_json_contract.sh +++ b/scripts/tests/test_result_common_json_contract.sh @@ -159,6 +159,55 @@ cat > "${TMP_DIR}/results/environment_snapshot_run.json" <<'EOF' } EOF +cat > "${TMP_DIR}/results/node_status_snapshot_run.json" <<'EOF' +{ + "schema_version": 1, + "kind": "node_status_snapshot", + "stage": "run", + "collected_at": "2026-09-14T00:01:30Z", + "collection_status": "ok", + "collection_warnings": ["remote_collection_not_used"], + "scheduler": { + "kind": "slurm", + "slurm": { + "job_id": "123", + "partition": "gpu", + "remote_collection_status": "ok" + } + }, + "allocation": { + "scheduler_hosts": ["node-a", "node-b"] + }, + "observed": { + "hosts": [ + { + "hostname": "node-a", + "gpu_state": { + "query_status": "ok", + "gpus": [ + {"index": "0", "memory_used_mib": 0} + ], + "process_summary": { + "compute_process_count": 0, + "memory_used_mib": 0 + } + } + } + ] + }, + "summary": { + "scheduler_host_count": 2, + "observed_host_count": 1, + "observed_gpu_count": 1, + "gpu_memory_used_total_mib": 0, + "gpu_compute_process_count": 0, + "gpu_compute_memory_used_mib": 0, + "gpu_query_statuses": ["ok"], + "warnings": ["observed_host_count_differs_from_scheduler_host_count"] + } +} +EOF + cat > "${TMP_DIR}/results/build_cache.env" <<'EOF' BK_BUILD_CACHE_STATUS=hit BK_BUILD_CACHE_REASON=restored cached build artifacts @@ -230,7 +279,19 @@ jq -e ' .pipeline_timing.scheduler_queue_time_source == "runner_metadata" and .pipeline_timing.run_time == 34 and .pipeline_timing.run_time_scope == "job" and - (.pipeline_timing | has("profiled_run_included") | not) + (.pipeline_timing | has("profiled_run_included") | not) and + .node_status_snapshot.schema_version == 1 and + .node_status_snapshot.kind == "node_status_snapshot" and + (.node_status_snapshot.hash | startswith("sha256:")) and + .node_status_snapshot.collection_status == "ok" and + .node_status_snapshot.collection_warnings == ["remote_collection_not_used"] and + .node_status_snapshot.scheduler_kind == "slurm" and + .node_status_snapshot.summary.scheduler_host_count == 2 and + .node_status_snapshot.summary.observed_host_count == 1 and + .node_status_snapshot.artifact.type == "file_reference" and + .node_status_snapshot.artifact.path == "results/node_status_snapshot_run.json" and + (.node_status_snapshot | has("observed") | not) and + (.node_status_snapshot | has("allocation") | not) ' "${RESULT_JSON}" >/dev/null jq -e ' diff --git a/scripts/tests/test_scheduler_extra_args.sh b/scripts/tests/test_scheduler_extra_args.sh index 9fa79d51..ff1fb79c 100644 --- a/scripts/tests/test_scheduler_extra_args.sh +++ b/scripts/tests/test_scheduler_extra_args.sh @@ -34,7 +34,7 @@ test "$(get_scheduler_extra_args Fugaku)" = "--account=global" tmpdir="" estimate_tmpdir=$(mktemp -d) -trap 'rm -rf "$tmpdir" "$estimate_tmpdir"' EXIT +trap 'rm -rf "$tmpdir" "$estimate_tmpdir"; rm -f "${REPO_DIR}/script.sh"' EXIT mkdir -p "$estimate_tmpdir/app" if has_estimate_script "$estimate_tmpdir/app"; then