
Hyperpod Slurm Debugger
- 64 installs
- 850 repo stars
- Updated August 3, 2026
- awslabs/agent-plugins
hyperpod-slurm-debugger is a diagnostic-only Claude skill that classifies Slurm scheduler and node-daemon issues on SageMaker HyperPod Slurm clusters.
About
This skill diagnoses Slurm scheduler and node-daemon issues on Amazon SageMaker HyperPod Slurm clusters, such as nodes stuck in down or drain, jobs stuck PENDING or COMPLETING, wrong GPU counts, and unresponsive slurmctld. It runs a diagnostic script, relays output verbatim, and maps each finding to official AWS or Slurm docs without printing any state-mutating command. A developer uses it to triage a stuck Slurm node or job on HyperPod.
- Diagnostic-only for Slurm scheduler and node-daemon issues on HyperPod Slurm clusters
- Classifies nodes stuck in down/drain, stuck PENDING/COMPLETING jobs, and slurmctld issues
- Never prints state-mutating commands; links to official AWS or Slurm docs for remediation
Hyperpod Slurm Debugger by the numbers
- 64 all-time installs (skills.sh)
- Ranked #279 of 596 Debugging skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
hyperpod-slurm-debugger capabilities & compatibility
- Capabilities
- hyperpod node debugger · hyperpod nccl · hyperpod cluster debugger · hyperpod ssm
- Works with
- aws
- Use cases
- debugging · devops
What hyperpod-slurm-debugger says it does
Diagnostic-only. Identify and classify Slurm scheduler and node-daemon issues on HyperPod Slurm clusters. Do not run, recommend, or print any state-mutating command.
For any remediation, link to AWS or Slurm docs. The user authorizes and executes.
npx skills add https://github.com/awslabs/agent-plugins --skill hyperpod-slurm-debuggerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 64 |
|---|---|
| repo stars | ★ 850 |
| Last updated | August 3, 2026 |
| Repository | awslabs/agent-plugins ↗ |
What it does
Diagnose Slurm scheduler and node-daemon issues on HyperPod Slurm clusters and link to remediation docs.
Who is it for?
Operators triaging stuck Slurm nodes or jobs on HyperPod Slurm clusters
Skip if: EKS clusters, single-node hardware faults, or NCCL training hangs (routes to sibling skills)
When should I use this skill?
A Slurm node is stuck in down/drain or jobs are stuck PENDING or COMPLETING
What you get
Each Slurm finding is classified and linked to the authoritative AWS or Slurm recovery doc.
- Classified Slurm findings mapped to AWS/Slurm recovery docs
By the numbers
- 5 decision-table sections (A-E)
- Read-only mode only, no remediation flag
Files
HyperPod Slurm Debugger
Diagnostic-only. Identify and classify Slurm scheduler and node-daemon issues on HyperPod Slurm clusters. Do not run, recommend, or print any state-mutating command. For remediation, link to the official AWS or Slurm documentation.
When to invoke
Invoke when the user reports any of the symptoms in the decision table.
When NOT to invoke
- Cluster has
Orchestrator.Eks— invokehyperpod-node-debuggerorhyperpod-nccl. - Single-node hardware fault with healthy Slurm scheduler — invoke
hyperpod-node-debugger. - NCCL training-hang investigation — invoke
hyperpod-nccl. - Node unreachable via SSM — invoke
hyperpod-ssm.
Constraints
- Read-only. Do not run, recommend, or print state-mutating commands.
- For any remediation, link to AWS or Slurm docs. The user authorizes and executes.
- IaC-managed cluster (Terraform / CloudFormation / CDK): warn that direct mutation
drifts the live state from the IaC plan.
Canonical recovery URLs: references/slurm-details.md → Authoritative recovery documentation.
Prerequisites
- AWS CLI v2, authenticated for the target account and region with permissions:
sagemaker:DescribeCluster,sagemaker:ListClusterNodesssm:StartSessionon the HyperPod-created SSM document- Session Manager plugin
installed locally.
jq≥ 1.6.unbuffer(from theexpectpackage). Required — without itaws ssm start-session
returns empty stdout intermittently with Cannot perform start session: EOF and every check silently misreports. Install: expect package on Amazon Linux / RHEL / Debian / Ubuntu / macOS. Script exits at prerequisite check if missing.
Procedure
Step 1 — Collect inputs
Ask the user for:
1. HyperPod cluster name (not Slurm partition name). 2. AWS region. 3. Optional: a specific Slurm node name.
Step 2 — Confirm orchestrator
aws sagemaker describe-cluster --cluster-name <NAME/ARN> --region <REGION> \
--query 'Orchestrator' --output jsonIf Orchestrator.Eks is present, stop. Route per When NOT to invoke.
Step 3 — Run the diagnostic script
bash scripts/slurm-diagnose.sh --cluster <NAME> --region <REGION>
# Scope to a node:
bash scripts/slurm-diagnose.sh --cluster <NAME> --region <REGION> --node <SLURM_NODE>Relay the script output to the user verbatim.
Step 4 — Map findings → docs
For each finding, look up the section in the decision table and link the user to the corresponding AWS / Slurm doc. Do not type out remediation commands.
Decision table
Symptom (sinfo -o "%N %T %30E" or script finding) | Section |
|---|---|
Node state = down or down*, reason other than below | A: Node Down |
Node state = down*, Reason = Node unexpectedly rebooted | B: Unexpected Reboot |
Jobs PENDING with REASON=Resources while nodes are idle | C: Controller State |
Jobs stuck COMPLETING after node replacement | C: Controller State |
scontrol ping returns DOWN for the controller | C: Controller State |
| GRES (GPU) counts incorrect or not released | C: Controller State |
state=fail issued but no recovery occurred | D: Action Reason Mismatch |
Accounting errors or RPC errors mentioning dbd | C: Controller State (slurmdbd) |
slurm.conf edited; new partitions or nodes not visible | C: Controller State (config) |
| Job exited on a hardware failure but did not restart | E: Auto-resume |
Defaults
| Behavior | Default | Override |
|---|---|---|
| Mode | read-only — always; no remediation flag exists | n/a |
| Region | $AWS_DEFAULT_REGION, falling back to us-east-1 | --region <R> |
| Scope | all nodes in down / drain / fail / "unexpectedly rebooted" | --node <SLURM_NODE_NAME> |
| Output | colorized terminal | --no-color |
| SSM target format | sagemaker-cluster:<clusterId>_<instanceGroupName>-<instanceId> (derived) | n/a |
| Controller discovery | --controller-group (if set) → SlurmConfig.NodeType=Controller → provisioning_parameters.json | --controller-group <N> |
Error handling
| Failure | Skill behavior | Required user action |
|---|---|---|
describe-cluster fails | Print AWS error; exit 1 | Fix credentials/region; verify cluster name |
Cluster has Orchestrator.Eks | Exit 1 with pointer to EKS-side skills | Use hyperpod-node-debugger or hyperpod-nccl |
session-manager-plugin missing / SSM unreachable | sinfo returns empty; exit 1 | Install plugin; verify node InService |
Disk ≥ 95 % full on a down node | Report finding disk-full-<node> | Refer to AWS troubleshooting docs |
Missing jq or aws | Exit 1 at prerequisite check | Install per Prerequisites |
---
A: Node Down
Node is down because slurmd stopped responding. Causes: slurmd crash, disk full, OOM, network partition, hardware fault.
Script checks: systemctl is-active slurmd, srun -w <NODE> hostname (RPC layer), disk, memory.
Link: <https://github.com/aws/sagemaker-hyperpod-cluster-setup/blob/troubleshooting-doc-20250917/troubleshoot/index.md>
If node returns to down after a manual resume → escalate to hyperpod-node-debugger.
Context: references/slurm-details.md § A.
---
B: Unexpected Reboot
Node is down* with Reason "Node unexpectedly rebooted" because slurmd re-registered after an out-of-band reboot. Upstream Slurm behavior, not HyperPod. Node is typically healthy.
Links:
- <https://github.com/aws/sagemaker-hyperpod-cluster-setup/blob/troubleshooting-doc-20250917/troubleshoot/index.md>
- <https://slurm.schedmd.com/scontrol.html> (
state=resumesemantics)
If node reboots again within minutes → escalate to hyperpod-node-debugger.
Context: references/slurm-details.md § B.
---
C: Controller State
slurmctld in-memory state can desync from the on-disk state. A controller restart reloads from StateSaveLocation and clears bad caches. User decides and executes.
Restart may help:
| Symptom | Why |
|---|---|
PENDING with REASON=Resources, idle nodes | Re-evaluates the queue |
Jobs stuck COMPLETING after node replacement | Controller held a reference to the old node |
| GRES (GPU, EFA) not released after a job ends | Resource accounting de-synced |
Nodes stuck Unknown after reboot, slurmd is up | Re-registration was not processed |
scontrol ping times out | Controller event loop is hung |
Lost connection to slurmdbd / RPC errors | DBD connection wedged |
Do NOT restart when:
- HyperPod replacement (
Action:Replace) in progress on any node — concurrent changes
fail the replacement.
- Only one compute node is bad — restart
slurmdon that node. sinfoandsqueueare responsive — problem is elsewhere.journalctl -u slurmctldnot reviewed yet — panic / OOM will reproduce.slurm.confwas just edited — tryscontrol reconfigurefirst.
Folded triggers
- slurmdbd disconnected —
sacctfails, accounting fields showUnknown,
controller log spams Unable to contact slurmdbd. Restore slurmdbd before considering controller restart. <https://slurm.schedmd.com/accounting.html> · details.
- Stale config —
slurm.conf/topology.confmtime > slurmctld start.
scontrol reconfigure first; restart is fallback. <https://slurm.schedmd.com/scontrol.html> · details.
Restart procedure / what's preserved:
- <https://slurm.schedmd.com/slurmctld.html>
- <https://github.com/aws/sagemaker-hyperpod-cluster-setup/blob/troubleshooting-doc-20250917/troubleshoot/index.md>
Context: references/slurm-details.md § C.
---
D: Action Reason Mismatch
scontrol update state=fail reason=... was issued with a reason that does not match Action:Reboot or Action:Replace exactly. HyperPod silently ignores anything else. Script detects near-misses on nodes in fail state.
Required strings (case-sensitive, no whitespace, no punctuation):
Action:RebootAction:Replace
Link: <https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-hyperpod-resiliency-slurm-replace-faulty-instance.html>
Context: references/slurm-details.md § Action reason-string validation.
---
E: Auto-resume
--auto-resume=1 is an srun step option. It re-runs the step after HMA (the Health Monitoring Agent) flags a node and Automatic node recovery replaces it.
Why it didn't restart the job:
- Flag on
sbatchnotsrun— per-step;sbatchdirectives are silently ignored. - HMA did not flag the node — failure was application/transient, not hardware. Step
exits as a normal Slurm failure.
- Cluster
NodeRecoveryisNone— faulty nodes are labeled but not replaced. - No checkpointing — step restarts from process zero each iteration.
- AMI predates HMA support (released 2025-09-11) — needs AMI / cluster-software update.
Link: <https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-hyperpod-resiliency-slurm-auto-resume.html>
Context: references/slurm-details.md § HyperPod auto-resume.
---
Escalation
| Condition | Next skill |
|---|---|
Node returns to down shortly after a manual resume | hyperpod-node-debugger (hardware) |
slurmd logs contain CUDA / NVIDIA / XID errors | hyperpod-node-debugger § G |
Disk full or /dev/shm exhausted | hyperpod-node-debugger § I |
| Node unreachable via SSM | hyperpod-ssm |
Controller restart does not clear COMPLETING after 2 attempts | hyperpod-issue-report + AWS Support |
Slurm Details
Diagnostic context for hyperpod-slurm-debugger. Diagnostic-only — do not run, recommend, or print state-mutating commands. Link to AWS / Slurm docs for remediation.
Table of contents
- Authoritative recovery documentation
- HyperPod auto-resume
- Action reason-string validation
- § A: Node down — diagnostic context
- § B: Unexpected reboot — diagnostic context
- § C: Controller state — diagnostic context
- scontrol reconfigure vs restart
- slurmdbd connectivity
---
Authoritative recovery documentation
- HyperPod Slurm troubleshooting:
<https://github.com/aws/sagemaker-hyperpod-cluster-setup/blob/troubleshooting-doc-20250917/troubleshoot/index.md>
- Replace a faulty Slurm instance:
<https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-hyperpod-resiliency-slurm-replace-faulty-instance.html>
- HyperPod auto-resume:
<https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-hyperpod-resiliency-slurm-auto-resume.html>
BatchRebootClusterNodes:
<https://docs.aws.amazon.com/cli/latest/reference/sagemaker/batch-reboot-cluster-nodes.html>
BatchReplaceClusterNodes:
<https://docs.aws.amazon.com/cli/latest/reference/sagemaker/batch-replace-cluster-nodes.html>
scontrol(1): <https://slurm.schedmd.com/scontrol.html>slurmctld(8): <https://slurm.schedmd.com/slurmctld.html>slurm.conf(5): <https://slurm.schedmd.com/slurm.conf.html>- Slurm accounting: <https://slurm.schedmd.com/accounting.html>
- Slurm authentication (munge): <https://slurm.schedmd.com/authentication.html>
---
HyperPod auto-resume
Three separate features that compose:
- HMA (Health Monitoring Agent) — runs hardware checks (NVIDIA SMI, Neuron sysfs,
EFA) continuously, independent of jobs. Marks faulty nodes for drain.
- Automatic node recovery (cluster
NodeRecoverysetting;AutomaticorNone) —
when Automatic, replaces drained nodes after their jobs exit.
- `--auto-resume=1` (
srunstep option) — re-runs the step after HMA + node
recovery replace a node in its allocation.
Auto-resume itself does not run health checks. HMA does. Auto-resume reacts to HMA-triggered replacements. The AWS doc's "How auto-resume works" section is misleading on this point — the authoritative description is in the "How automatic node recovery and auto-resume work together" section, which states: _"If the HMA detects a hardware fault, the node is marked for drain regardless of job-level status. With node automatic recovery enabled, the nodes are automatically replaced once all the jobs running in the nodes exit. In this scenario, for jobs with auto-resume enabled, if there is a non-zero exit status in the step, the auto resume kicks in."_
If HMA does not flag a node, auto-resume does not fire — the step exits as a normal Slurm failure.
<https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-hyperpod-resiliency-slurm-auto-resume.html>
Verify auto-resume ran (read-only)
# Replace events in slurmctld log:
sudo journalctl -u slurmctld --since "2 hours ago" | grep -E 'auto.?resume|Action:Replace|replac'
# Last reason and boot time on the node:
scontrol show node <NODE> | grep -i 'reason\|boot'
# Job-step events from accounting:
sacct -j <JOBID> -o JobID,JobName,State,ExitCode,NodeList,Start,End -XSame JOBID after NodeList change → auto-resume succeeded.
Why auto-resume didn't restart
- Flag on `sbatch` not `srun` — per-step option;
sbatchdirectives ignored. - HMA did not flag the node — auto-resume only reacts to HMA-triggered
replacements. Inspect dmesg and journalctl -k for hardware signals (XID, MCE, PCIe AER, EFA driver errors). None → not hardware; failure was application or transient and auto-resume cannot fire.
- Cluster `NodeRecovery` is `None` — HMA labels faulty nodes but nothing replaces
them. Confirm: aws sagemaker describe-cluster ... --query NodeRecovery.
- AMI predates HMA support (released 2025-09-11). Script flags this by checking for
--auto-resume in srun --help.
- Concurrent manual `Action:Replace` racing with the automatic replacement.
---
Action reason-string validation
HyperPod auto-recovery matches the Slurm node Reason field exactly, case-sensitive:
| Intent | Required reason |
|---|---|
| Reboot | Action:Reboot |
| Replace | Action:Replace |
Any mismatch is silently ignored. Common near-misses:
action:replace— wrong caseAction: Reboot— extra space after colonAction:Reboot⎵(where⎵is whitespace) — trailing whitespaceAction:Reboot.— trailing punctuationReboot/replace this— wrong format
Verify (read-only):
sinfo -o "%N %T %30E" | grep <NODE>
scontrol show node <NODE> | grep -i reasonCanonical command form per AWS docs (do not run from this skill — operator-executed):
scontrol update node=<ip-ipv4> state=fail reason="Action:Reboot"
scontrol update node=<ip-ipv4> state=fail reason="Action:Replace"Re-issue procedure: <https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-hyperpod-resiliency-slurm-replace-faulty-instance.html>
---
§ A: Node down — diagnostic context
slurmd stopped responding. Causes: slurmd crash/stop, disk full, OOM, network partition, hardware fault.
Inspection (read-only)
# Head node:
sinfo -o "%N %T %30E" | grep -E 'down|drain'
scontrol show node <NODE> # Reason, LastBusyTime, Boot
# Reachability per layer:
ping <NODE> # L3
srun -w <NODE> hostname # Slurm RPC
ssh <NODE> true # SSH (if configured)
# Affected node (via SSM):
systemctl status slurmd
journalctl -u slurmd -n 200 --no-pager
journalctl -xe -n 100 --no-pager # kernel errors, OOM kills
free -h
df -h
df -h /dev/shmFindings → docs
| Finding | Link |
|---|---|
slurmd stopped, logs clean | HyperPod Slurm troubleshooting (Authoritative recovery) |
slurmd crashing, munge errors | <https://slurm.schedmd.com/authentication.html> |
| Disk full | HyperPod storage layout (/opt/sagemaker, /opt/dlami/nvme, FSx) |
OOM in dmesg | Right-size workload — AWS instance-type docs |
| Kernel panic / recent reboot | § B: Unexpected reboot |
GPU XID / ECC errors in dmesg | hyperpod-node-debugger § G |
If node returns to down after manual recovery → hyperpod-node-debugger (hardware).
---
§ B: Unexpected reboot — diagnostic context
slurmd re-registered after an out-of-band reboot (kernel panic, watchdog, manual reboot, HyperPod auto-repair). Slurm marks the node down* with reason Node unexpectedly rebooted and refuses scheduling. Upstream Slurm behavior, not HyperPod-specific — protects pending jobs from landing on a node with potentially corrupt local state (partial checkpoints, half-written scratch).
Node is usually fine. Resume procedure:
- <https://github.com/aws/sagemaker-hyperpod-cluster-setup/blob/troubleshooting-doc-20250917/troubleshoot/index.md>
- <https://slurm.schedmd.com/scontrol.html> (
state=resumesemantics)
If the node loops through reboots → kernel / hardware issue. Inspect dmesg and journalctl -b -1 (previous boot) before any further action. Route to hyperpod-node-debugger.
---
§ C: Controller state — diagnostic context
slurmctld in-memory state desynced from disk-persisted state. Standard restart reloads from StateSaveLocation (typically /var/spool/slurmctld/ on HyperPod, but admin-configured — confirm with scontrol show config | grep StateSaveLocation).
What's preserved across a restart
Per `slurmctld(8)`, without -c the restart preserves running jobs plus node state of DOWN, DRAINED, and DRAINING nodes with their Reason field.
Recovered from `StateSaveLocation`:
- Running jobs (continue executing on compute nodes; reconnect when controller is back).
- Pending queue (
squeuereturns the same queue). DOWN,DRAINED,DRAININGnode states + Reason field.- Accounting records (via
slurmdbd).
Re-read from `slurm.conf` on startup:
- Partition definitions,
NodeNamedefinitions, scheduling parameters.
Reset (this is what fixes the symptoms):
- In-memory scheduling decisions and priority calculations.
- GRES / TRES accounting caches.
- Hung RPC connections to compute nodes.
- Stale
REASON=Resourceson pending jobs. - Stuck
COMPLETINGtracking.
Pre-restart inspection (read-only)
scontrol show config | grep StateSaveLocation
STATE=$(scontrol show config | awk -F= '/^StateSaveLocation/ {gsub(/ /,"",$2); print $2; exit}')
sudo ls -la "$STATE" # should have recent state filesIf the directory is missing or empty, do NOT restart — recover state file from backup first. slurmctld -c (clean start) purges every job from the controller.
Restart procedure:
- <https://slurm.schedmd.com/slurmctld.html>
- <https://github.com/aws/sagemaker-hyperpod-cluster-setup/blob/troubleshooting-doc-20250917/troubleshoot/index.md>
When-to-restart vs when-not-to: see SKILL.md § C.
scontrol reconfigure vs restart
slurm.conf / topology.conf / gres.conf was edited; controller has stale config in memory. Two reload paths:
`scontrol reconfigure` — no downtime. Reloads slurm.conf in place. Per `scontrol(1)`, cannot change daemons' listening TCP port or AuthType; changing AuthType requires terminating all Slurm daemons + commands per `slurm.conf(5)`.
`systemctl restart slurmctld` — ~5–30s scheduling pause. Required for changes that scontrol reconfigure rejects. In practice operators also restart for structural changes (adding/removing nodes, NodeName changes, topology rewrites) since reconfigure isn't guaranteed to apply them cleanly.
Pre-reload inspection (read-only):
# HyperPod installs to /opt/slurm-<version>/etc/, not /etc/slurm/:
CONF=$(scontrol show config | awk -F= '/^SLURM_CONF/ {gsub(/ /,"",$2); print $2; exit}')
ls -la "$CONF"
# After reload, watch for parse errors:
journalctl -u slurmctld -n 50 --no-pagerNo syntax-check flag exists for slurmctld or slurmdbd. Errors surface in journalctl after reload.
scontrol reconfigure only reloads the controller's view. Compute nodes read their own copy of slurm.conf from disk. If the lifecycle script doesn't push slurm.conf to every node (via shared FSx mount or explicit copy step), node-side slurmd runs with stale config until restarted.
slurmdbd connectivity
slurmctld cannot reach slurmdbd. Scheduler keeps running; accounting fails. Symptoms look like a controller hang but aren't.
Symptoms:
sacctmgr show statsreturnsUnable to contact slurmdbdorConnection refused.sacct -j <JOBID>returnsSockets disabledor no rows.journalctl -u slurmctld | grep -i dbdshows repeated reconnect attempts.- New jobs complete but accounting records never appear in
sacct.
Diagnose (read-only):
systemctl status slurmdbd
journalctl -u slurmdbd -n 100 --no-pager
journalctl -u slurmctld -n 100 --no-pager | grep -iE 'dbd|accounting'
# slurmdbd.conf path — HyperPod uses /opt/slurm-<version>/etc/:
SLURMDBD_CONF=$(find /opt/slurm*/etc /etc/slurm -name slurmdbd.conf 2>/dev/null | head -1)
sudo grep -E 'StorageHost|StoragePort|StorageUser' "$SLURMDBD_CONF"
nc -vz <StorageHost> <StoragePort> # default port 3306Common causes:
| Cause | Link |
|---|---|
slurmdbd daemon stopped or crashed | <https://slurm.schedmd.com/accounting.html> |
| MySQL / MariaDB endpoint unreachable | Restore SG / VPC route; slurmdbd self-recovers |
slurmdbd.conf StoragePass wrong / rotated | <https://slurm.schedmd.com/slurmdbd.conf.html> |
| Disk full on slurmdbd host | Daemon won't start without log-file write access |
| Schema migration pending after Slurm upgrade | <https://slurm.schedmd.com/upgrades.html> |
Recovery order:
1. Restore slurmdbd. Running jobs are unaffected — no time pressure. 2. Verify with sacctmgr show stats (rollup counters, no errors). 3. Only then evaluate whether slurmctld itself needs a restart. If slurmctld recovered the DBD connection on its own, no restart is needed. If the controller log still shows stuck DBD-RPC threads, see § C.
If the database is RDS / Aurora / managed, check snapshot windows and maintenance events — a brief failover can leave slurmctld with a wedged connection.
#!/usr/bin/env bash
# slurm-diagnose.sh
#
# Read-only diagnostic for Slurm node-management issues on Amazon SageMaker HyperPod
# Slurm clusters. Covers the scenarios documented in the HyperPod troubleshooting guide:
#
# A. Node DOWN / not responding
# B. Node DOWN with reason "Node unexpectedly rebooted"
# C. Controller state — slurmctld desync, plus the two folded triggers:
# C (slurmdbd): accounting daemon connectivity
# C (config): pending slurm.conf reconfiguration
# D. Auto-recovery reason-string mismatches (Action:Reboot / Action:Replace)
# E. HyperPod --auto-resume support and recent missed-resume detection
#
# Security model:
# - All CLI inputs are validated against strict regexes at parse time.
# - All AWS-derived values (instance IDs, group names, node names) are validated before
# they reach any shell context — invalid values cause an immediate exit.
# - Remote SSM payloads are base64-encoded literals; server-derived values are
# prepended to the remote script as `export VAR='<jq @sh-quoted VALUE>'` lines so
# they are never string-interpolated into shell commands.
# - Local printf calls use `%s` with the data as a separate argument; format-string
# attacks via server values are not possible.
#
# The script never mutates cluster state.
#
# Usage:
# bash slurm-diagnose.sh --cluster <NAME-or-ARN> --region <REGION>
# bash slurm-diagnose.sh --cluster <N> --region <R> --node <SLURM_NODE>
# bash slurm-diagnose.sh --cluster <N> --region <R> --controller-group <NAME>
#
# Optional flags:
# --node <SLURM_NODE> Scope inspection to a single Slurm node.
# --controller-group <N> Override controller-group discovery (for self-managed
# Slurm clusters where SlurmConfig is not set).
# --no-color Plain output (no ANSI colors).
set -euo pipefail
CLUSTER=""
REGION="${AWS_DEFAULT_REGION:-us-east-1}"
TARGET_NODE=""
CONTROLLER_GROUP_OVERRIDE=""
USE_COLOR=true
# --- Input-validation helpers -------------------------------------------------
# Each validator prints the value if valid, exits non-zero if not. All callsites
# capture into a local variable; failure aborts the script via `set -e`.
# AWS region: lowercase letters, digits, dashes only.
validate_region() {
local v="${1-}"
[[ "$v" =~ ^[a-z]{2,3}-[a-z]+-[0-9]+$ ]] || { echo "Error: invalid region: $v" >&2; exit 2; }
printf '%s' "$v"
}
# HyperPod cluster name OR ARN. Names are 1-63 chars of [a-zA-Z0-9_-]; ARNs match the
# documented SageMaker cluster ARN shape.
validate_cluster() {
local v="${1-}"
if [[ "$v" =~ ^arn:aws[a-zA-Z-]*:sagemaker:[a-z0-9-]+:[0-9]{12}:cluster/[a-zA-Z0-9-]+$ ]]; then
printf '%s' "$v"
elif [[ "$v" =~ ^[a-zA-Z0-9_-]{1,63}$ ]]; then
printf '%s' "$v"
else
echo "Error: invalid cluster name/ARN: $v" >&2
exit 2
fi
}
# Slurm node names on HyperPod follow the `ip-x-x-x-x` form, but admins may rename.
# Allow [a-zA-Z0-9._-]+ with length 1..253; reject anything that could escape a shell.
validate_node_name() {
local v="${1-}"
[[ "$v" =~ ^[a-zA-Z0-9._-]{1,253}$ ]] || { echo "Error: invalid node name: $v" >&2; exit 2; }
printf '%s' "$v"
}
# EC2 instance IDs: i- followed by 8 or 17 hex characters. Documented and stable.
validate_instance_id() {
local v="${1-}"
[[ "$v" =~ ^i-[a-f0-9]{8}([a-f0-9]{9})?$ ]] || { echo "Error: invalid instance ID: $v" >&2; exit 2; }
printf '%s' "$v"
}
# Cluster ID (from ARN): lowercase alphanumeric, currently 12 chars (e.g.
# qrmv6xhralg4). Allow 4..32 to be future-tolerant.
validate_cluster_id() {
local v="${1-}"
[[ "$v" =~ ^[a-z0-9]{4,32}$ ]] || { echo "Error: invalid cluster ID: $v" >&2; exit 2; }
printf '%s' "$v"
}
# Instance group name: SageMaker allows 1..63 chars [a-zA-Z0-9_-] per the
# CreateCluster API.
validate_group_name() {
local v="${1-}"
[[ "$v" =~ ^[a-zA-Z0-9_-]{1,63}$ ]] || { echo "Error: invalid instance group name: $v" >&2; exit 2; }
printf '%s' "$v"
}
# --- Argument parsing ---------------------------------------------------------
while [[ $# -gt 0 ]]; do
case "$1" in
--cluster)
[[ $# -lt 2 ]] && { echo "Error: --cluster requires a value" >&2; exit 2; }
CLUSTER=$(validate_cluster "$2"); shift 2 ;;
--region)
[[ $# -lt 2 ]] && { echo "Error: --region requires a value" >&2; exit 2; }
REGION=$(validate_region "$2"); shift 2 ;;
--node)
[[ $# -lt 2 ]] && { echo "Error: --node requires a value" >&2; exit 2; }
TARGET_NODE=$(validate_node_name "$2"); shift 2 ;;
--controller-group)
[[ $# -lt 2 ]] && { echo "Error: --controller-group requires a value" >&2; exit 2; }
CONTROLLER_GROUP_OVERRIDE=$(validate_group_name "$2"); shift 2 ;;
--no-color) USE_COLOR=false; shift ;;
-h|--help)
# Print every leading-comment line at the top of this file (lines 2..N until the
# first non-comment line). Robust against future header edits.
awk 'NR==1{next} /^#/{sub(/^# ?/,""); print; next} {exit}' "$0"
exit 0 ;;
--*) echo "Error: unknown flag: $1" >&2; exit 2 ;;
*) echo "Error: unexpected positional argument: $1" >&2; exit 2 ;;
esac
done
[[ -z "$CLUSTER" ]] && { echo "Error: --cluster is required" >&2; exit 2; }
REGION=$(validate_region "$REGION") # validate even when sourced from env default
# --- Prerequisite checks ------------------------------------------------------
command -v aws >/dev/null 2>&1 || { echo "Error: aws CLI is required (v2 recommended)." >&2; exit 1; }
command -v jq >/dev/null 2>&1 || { echo "Error: jq is required. Install with your package manager." >&2; exit 1; }
# `unbuffer` (from the `expect` package) attaches a PTY to aws ssm start-session, which
# avoids a known race where session-manager-plugin closes stdout before flushing and the
# caller sees "Cannot perform start session: EOF" with empty output. Without it, every
# SSM command silently returns empty, causing every downstream check to misreport.
command -v unbuffer >/dev/null 2>&1 || {
echo "Error: unbuffer (from the 'expect' package) is required." >&2
echo " Install: sudo yum install expect | sudo apt install expect | brew install expect" >&2
exit 1
}
# --- Output formatting --------------------------------------------------------
if "$USE_COLOR"; then
RED=$'\033[0;31m'; GREEN=$'\033[0;32m'; YELLOW=$'\033[1;33m'
CYAN=$'\033[0;36m'; BOLD=$'\033[1m'; NC=$'\033[0m'
else
RED=''; GREEN=''; YELLOW=''; CYAN=''; BOLD=''; NC=''
fi
# All status helpers use %s with the message as a separate arg — never embed message
# text into the format string. Strip ANSI escape sequences from incoming server data
# so a malicious or buggy upstream cannot rewrite the operator's terminal.
_sanitize() {
# Drop ANSI CSI sequences and bell, but leave printable UTF-8 alone.
sed -e 's/\x1b\[[0-9;?]*[a-zA-Z]//g' -e 's/\x07//g' -e 's/\r$//' <<< "${1-}"
}
section() { printf '\n%s%s=== %s ===%s\n' "$BOLD" "$CYAN" "$(_sanitize "$1")" "$NC"; }
ok() { printf ' %s[PASS]%s %s\n' "$GREEN" "$NC" "$(_sanitize "$1")"; }
warn() { printf ' %s[WARN]%s %s\n' "$YELLOW" "$NC" "$(_sanitize "$1")"; }
bad() { printf ' %s[FAIL]%s %s\n' "$RED" "$NC" "$(_sanitize "$1")"; }
info() { printf ' %s\n' "$(_sanitize "$1")"; }
hint() { printf ' %s[NEXT]%s %s\n' "$CYAN" "$NC" "$(_sanitize "$1")"; }
ISSUES=()
NEXT_STEPS=()
# --- Verify cluster + orchestrator --------------------------------------------
section "1. Cluster identity"
DESC=$(aws sagemaker describe-cluster --cluster-name "$CLUSTER" --region "$REGION" \
--output json 2>&1) || { bad "cannot describe cluster: $DESC"; exit 1; }
ORCH=$(jq -r '.Orchestrator // {} | keys[0] // "Slurm"' <<< "$DESC")
if [[ "$ORCH" == "Eks" ]]; then
bad "cluster uses EKS orchestrator - this skill is for Slurm only"
info "use hyperpod-node-debugger or hyperpod-nccl instead"
exit 1
fi
# Managed Slurm vs self-managed Slurm:
# - Managed: DescribeCluster.Orchestrator.Slurm is present AND the cluster was created
# with the SlurmConfig API parameter — InstanceGroups[].SlurmConfig.NodeType identifies
# controllers, login nodes, workers. AWS docs treat this as the authoritative source.
# - Self-managed: anything else. The customer brought their own Slurm setup via the
# lifecycle scripts and InstanceGroups[].SlurmConfig is empty. The controller-group
# name lives in /opt/ml/config/provisioning_parameters.json on every node, or the
# customer can pass --controller-group <NAME>.
HAS_SLURM_CONFIG=$(jq -r '
any(.InstanceGroups[]?; (.SlurmConfig // {}) != {})
' <<< "$DESC")
CLUSTER_NAME=$(jq -r '.ClusterName // "unknown"' <<< "$DESC")
CLUSTER_STATUS=$(jq -r '.ClusterStatus // "unknown"' <<< "$DESC")
if [[ "$HAS_SLURM_CONFIG" == "true" ]]; then
ok "Managed Slurm cluster: $CLUSTER_NAME status=$CLUSTER_STATUS"
else
ok "Self-managed Slurm cluster: $CLUSTER_NAME status=$CLUSTER_STATUS"
fi
# Cluster ID from ARN. Validate before it gets embedded into SSM target strings.
CLUSTER_ID=$(jq -r '.ClusterArn // "" | split("/") | last' <<< "$DESC")
[[ -n "$CLUSTER_ID" ]] || { bad "cannot extract cluster ID from ARN"; exit 1; }
CLUSTER_ID=$(validate_cluster_id "$CLUSTER_ID")
# --- SSM remote-execution helper ----------------------------------------------
#
# `ssm_run` runs a command on a HyperPod node via SSM (read-only).
#
# Design notes:
# 1. The remote script is base64-encoded locally and decoded remotely. The agent's
# command parameter is a fixed `sh -c "echo <BASE64> | base64 -d | bash"`; the
# base64 string contains only [A-Za-z0-9+/=] and is safe inside double quotes.
# Nothing from the script's caller appears unescaped in the SSM-agent's argv.
# 2. Server-derived values that need to be visible to the remote script are passed
# as named environment variables (`VAR=VALUE` trailing args). Each value is run
# through `jq @sh` (single-quoted shell-safe encoding with `'\''` escapes) and
# prepended to the remote script as `export VAR='<safely-quoted>'; ...`. The remote
# shell reads them as `$NODE`, `$NODELIST`, etc. — values never reach a remote
# shell-eval context as raw interpolated text.
# 3. `unbuffer` is required to defeat the SSM "Cannot perform start session: EOF"
# race; the prerequisite check above guarantees it's present.
# 4. Returns the underlying aws-cli exit code so callers can distinguish transport
# failures from successful empty output.
#
# Usage:
# ssm_run TARGET REMOTE_SCRIPT [VAR=VALUE ...]
ssm_run() {
local target="$1"; shift
local script="$1"; shift
local export_block="" raw_kv key val safe_val
for raw_kv in "$@"; do
[[ "$raw_kv" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]] || {
echo "ssm_run: invalid VAR=VALUE: $raw_kv" >&2
return 2
}
key="${BASH_REMATCH[1]}"
val="${BASH_REMATCH[2]}"
# jq's @sh produces single-quoted shell-safe text with embedded `'\''` escapes.
safe_val=$(jq -nr --arg v "$val" '$v | @sh')
export_block+="export ${key}=${safe_val}; "
done
local full_script="${export_block}${script}"
local b64
if base64 --help 2>&1 | grep -q '\-w'; then
b64=$(printf '%s' "$full_script" | base64 -w0)
else
b64=$(printf '%s' "$full_script" | base64 -b0)
fi
local wrapper="sh -c \"echo $b64 | base64 -d | bash\""
local params
params=$(jq -nc --arg c "$wrapper" '{command: [$c]}')
local out rc=0
out=$(unbuffer aws ssm start-session --region "$REGION" --target "$target" \
--document-name AWS-StartNonInteractiveCommand \
--parameters "$params" 2>&1) || rc=$?
# NOTE: do NOT strip 'Cannot perform start session' here — that line is the
# SSM transport-failure signal that ssm_transport_failed() detects. Only filter
# benign session chrome ('Starting session' / 'Exiting session') and ANSI escapes.
printf '%s' "$out" \
| sed -e 's/\x1b\[[0-9;?]*[a-zA-Z]//g' \
-e '/^Starting session/d' \
-e '/^Exiting session/d'
return "$rc"
}
# Returns 0 if the SSM raw output indicates a transport-layer failure (no command
# output, session refused, EOF before flush) — distinct from "command ran and returned
# nothing." Used to bail out early rather than misreport every downstream check.
ssm_transport_failed() {
local raw="${1-}"
grep -qiE 'Cannot perform start session|TargetNotConnected|InvalidTarget|AccessDeniedException|UnauthorizedOperation' <<< "$raw"
}
# --- Find controller node -----------------------------------------------------
NODES_JSON=$(aws sagemaker list-cluster-nodes --cluster-name "$CLUSTER" --region "$REGION" \
--output json 2>&1) || { bad "list-cluster-nodes failed: $NODES_JSON"; exit 1; }
# Discovery priority:
# 1. --controller-group <NAME> (operator override — always wins)
# 2. InstanceGroups[].SlurmConfig.NodeType == "Controller" (managed-Slurm authoritative)
# 3. /opt/ml/config/provisioning_parameters.json on a probe node (self-managed fallback)
# 4. Refuse to guess — print available groups and exit.
# We never guess based on instance-group naming — that's a lifecycle-script convention,
# not a guarantee, and getting it wrong sends every command to a non-controller.
CONTROLLER_GROUP=""
CONTROLLER_DISCOVERY_METHOD=""
# (1) Operator override — always wins.
if [[ -n "$CONTROLLER_GROUP_OVERRIDE" ]]; then
CONTROLLER_GROUP="$CONTROLLER_GROUP_OVERRIDE"
CONTROLLER_DISCOVERY_METHOD="--controller-group flag"
fi
# (2) Managed-Slurm authoritative source.
if [[ -z "$CONTROLLER_GROUP" && "$HAS_SLURM_CONFIG" == "true" ]]; then
CONTROLLER_GROUP=$(jq -r '
.InstanceGroups[]?
| select((.SlurmConfig.NodeType // "") == "Controller")
| .InstanceGroupName' <<< "$DESC" | head -1)
if [[ -n "$CONTROLLER_GROUP" ]]; then
CONTROLLER_DISCOVERY_METHOD="DescribeCluster.SlurmConfig"
fi
fi
# (3) Self-managed: read provisioning_parameters.json from any node.
# The lifecycle-script convention is that this file is dropped at the same path on every
# node, so we pick any node arbitrarily, SSM in, and read the controller_group field.
if [[ -z "$CONTROLLER_GROUP" ]]; then
PROBE_ID=$(jq -r '.ClusterNodeSummaries[0].InstanceId // ""' <<< "$NODES_JSON")
PROBE_GROUP=$(jq -r '.ClusterNodeSummaries[0].InstanceGroupName // ""' <<< "$NODES_JSON")
if [[ -n "$PROBE_ID" && -n "$PROBE_GROUP" ]]; then
PROBE_ID_V=$(validate_instance_id "$PROBE_ID")
PROBE_GROUP_V=$(validate_group_name "$PROBE_GROUP")
PROBE_TARGET="sagemaker-cluster:${CLUSTER_ID}_${PROBE_GROUP_V}-${PROBE_ID_V}"
# Field name varies between lifecycle-script generations — try both.
PROV_GROUP=$(ssm_run "$PROBE_TARGET" \
'jq -r ".controller_group // .ControllerGroup // empty" /opt/ml/config/provisioning_parameters.json 2>/dev/null' \
2>/dev/null | tr -d '\r\n' || true)
if [[ -n "$PROV_GROUP" ]]; then
MATCHED=$(jq -r --arg g "$PROV_GROUP" \
'[.ClusterNodeSummaries[]? | select(.InstanceGroupName == $g)] | length' <<< "$NODES_JSON")
if [[ "$MATCHED" -gt 0 ]]; then
CONTROLLER_GROUP="$PROV_GROUP"
CONTROLLER_DISCOVERY_METHOD="provisioning_parameters.json on $PROBE_ID_V"
fi
fi
fi
fi
# (4) Out of options — refuse to guess. Tell the operator how to unblock.
if [[ -z "$CONTROLLER_GROUP" ]]; then
bad "cannot identify the Slurm controller instance group"
if [[ "$HAS_SLURM_CONFIG" == "true" ]]; then
info "no InstanceGroup has SlurmConfig.NodeType=Controller in DescribeCluster output"
info "this is unexpected for a managed-Slurm cluster — verify the cluster was"
info "created with the SlurmConfig parameter, or pass --controller-group <NAME>."
else
info "self-managed Slurm cluster — provisioning_parameters.json was not readable"
info "from a probe node, and no --controller-group flag was provided."
info ""
info "Resolve by either:"
info " 1. inspecting the head node manually:"
info " aws ssm start-session --target $PROBE_TARGET --region $REGION"
info " cat /opt/ml/config/provisioning_parameters.json | jq ."
info " 2. re-running with the controller group's name:"
info " --controller-group <INSTANCE_GROUP_NAME>"
info ""
info "Available instance groups in this cluster:"
jq -r '.ClusterNodeSummaries[] | " - " + .InstanceGroupName + " (" + .InstanceId + ")"' \
<<< "$NODES_JSON" | sort -u
fi
exit 1
fi
CONTROLLER_GROUP=$(validate_group_name "$CONTROLLER_GROUP")
# Pick the first node from the controller group.
CONTROLLER_ID=$(jq -r --arg g "$CONTROLLER_GROUP" \
'.ClusterNodeSummaries[]? | select(.InstanceGroupName == $g) | .InstanceId' <<< "$NODES_JSON" | head -1)
[[ -n "$CONTROLLER_ID" ]] || { bad "controller group $CONTROLLER_GROUP has no nodes"; exit 1; }
CONTROLLER_ID=$(validate_instance_id "$CONTROLLER_ID")
ok "controller node: $CONTROLLER_ID (group=$CONTROLLER_GROUP, source=$CONTROLLER_DISCOVERY_METHOD)"
SSM_HEAD="sagemaker-cluster:${CLUSTER_ID}_${CONTROLLER_GROUP}-${CONTROLLER_ID}"
# --- Collect Slurm state from head node ---------------------------------------
section "2. Slurm cluster state (from head node)"
SSM_PROBE=$(ssm_run "$SSM_HEAD" 'echo SSM_OK' || true)
if ! grep -q '^SSM_OK$' <<< "$SSM_PROBE"; then
bad "cannot reach head node via SSM — every downstream check would be unreliable"
if ssm_transport_failed "$SSM_PROBE"; then
info " transport error detected (TargetNotConnected, AccessDenied, or EOF race)"
fi
info " reproduce manually with the same target and region:"
info " aws ssm start-session --target $SSM_HEAD --region $REGION"
info " if that fails, route to the hyperpod-ssm skill before retrying."
exit 1
fi
ok "SSM transport to head node working"
SINFO_OUT=$(ssm_run "$SSM_HEAD" 'sinfo -h -o "%N|%T|%E" 2>&1 | head -200' || true)
if [[ $(printf '%s\n' "$SINFO_OUT" | wc -l) -ge 200 ]]; then
warn "sinfo output reached the 200-line cap — node-state results may be truncated on this large cluster"
fi
if grep -qi 'command not found' <<< "$SINFO_OUT"; then
bad "sinfo not installed on head node — Slurm lifecycle script may not have run"
info "verify on the node: systemctl status slurmctld; ls /opt/slurm*/etc /etc/slurm 2>/dev/null"
exit 1
fi
if [[ -z "$SINFO_OUT" ]]; then
warn "sinfo returned no rows — empty cluster, or controller not yet responding"
fi
# Parse sinfo lines. Node names from sinfo are server-controlled; validate before they
# can be embedded into any later command. Values that fail validation are dropped, not
# trusted; we report the count of skipped entries so the operator notices.
DOWN_NODES=()
REBOOT_NODES=()
FAIL_NODES=()
BAD_REASON_NODES=()
SKIPPED_INVALID=0
while IFS='|' read -r node state reason; do
[[ -z "$node" ]] && continue
if ! [[ "$node" =~ ^[a-zA-Z0-9._-]{1,253}$ ]]; then
SKIPPED_INVALID=$((SKIPPED_INVALID+1))
continue
fi
# Reasons can contain spaces and punctuation; allow them but strip ANSI/control chars.
reason="$(_sanitize "$reason")"
if grep -qi 'fail' <<< "$state"; then
if [[ "$reason" =~ ^Action:(Reboot|Replace)$ ]]; then
FAIL_NODES+=("$node|$reason")
elif grep -qiE 'action[ :_-]*re(boot|place)|reboot|replace' <<< "$reason"; then
BAD_REASON_NODES+=("$node|$reason")
fi
fi
if grep -qiE 'down|drain' <<< "$state"; then
if grep -qi 'unexpectedly rebooted' <<< "$reason"; then
REBOOT_NODES+=("$node")
else
DOWN_NODES+=("$node|$reason")
fi
fi
done <<< "$SINFO_OUT"
[[ "$SKIPPED_INVALID" -gt 0 ]] && warn "$SKIPPED_INVALID sinfo row(s) had invalid node names and were ignored"
if [[ ${#DOWN_NODES[@]} -eq 0 && ${#REBOOT_NODES[@]} -eq 0 && ${#FAIL_NODES[@]} -eq 0 && ${#BAD_REASON_NODES[@]} -eq 0 ]]; then
ok "all nodes in healthy Slurm states"
else
[[ ${#DOWN_NODES[@]} -gt 0 ]] && bad "${#DOWN_NODES[@]} node(s) DOWN/DRAIN (Section A)"
[[ ${#REBOOT_NODES[@]} -gt 0 ]] && bad "${#REBOOT_NODES[@]} node(s) with 'unexpectedly rebooted' (Section B)"
[[ ${#FAIL_NODES[@]} -gt 0 ]] && warn "${#FAIL_NODES[@]} node(s) in fail state with valid Action:* reason (HyperPod recovery in progress)"
[[ ${#BAD_REASON_NODES[@]} -gt 0 ]] && bad "${#BAD_REASON_NODES[@]} node(s) in fail state with non-matching reason (Section D)"
fi
# --- Section D: Action:* reason-string validation -----------------------------
if [[ ${#BAD_REASON_NODES[@]} -gt 0 ]]; then
section "D. Reason-string mismatch — HyperPod auto-recovery will NOT trigger"
for entry in "${BAD_REASON_NODES[@]}"; do
n="${entry%%|*}"; r="${entry#*|}"
bad "$n: reason='$r'"
done
info "the reason field must match exactly: Action:Reboot or Action:Replace"
info "(case-sensitive, no spaces, no trailing punctuation)"
hint "for re-issue procedure, see:"
info " https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-hyperpod-resiliency-slurm-replace-faulty-instance.html"
info " references/slurm-details.md#action-reason-string-validation"
ISSUES+=("bad-action-reason")
NEXT_STEPS+=("see AWS replace-faulty-instance docs (link above)")
fi
# --- Detect in-progress HyperPod replacements (informational) -----------------
if [[ ${#FAIL_NODES[@]} -gt 0 ]]; then
section " HyperPod recovery in progress (do not interfere)"
for entry in "${FAIL_NODES[@]}"; do
n="${entry%%|*}"; r="${entry#*|}"
info "$n ($r)"
done
info "AWS docs: do NOT change node state or restart slurmctld until this completes."
info "If a replacement seems stuck > 30 min, see:"
info " https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-hyperpod-resiliency-slurm-replace-faulty-instance.html"
fi
# --- Check controller health --------------------------------------------------
section "3. slurmctld health"
PING_OUT=$(ssm_run "$SSM_HEAD" 'scontrol ping 2>&1' || true)
PING_FIRST_LINE=$(head -1 <<< "$PING_OUT" | tr -d '\r')
if grep -qi 'UP' <<< "$PING_OUT"; then
ok "slurmctld responding: $(tr '\n' ' ' <<< "$PING_OUT")"
elif [[ -z "$PING_OUT" ]] || ssm_transport_failed "$PING_OUT"; then
warn "could not get a response from scontrol ping — cannot determine controller health"
info "this is most likely an SSM transport problem, not a hung controller"
info "do NOT restart slurmctld based on this finding alone"
elif grep -qi 'DOWN' <<< "$PING_OUT"; then
bad "slurmctld reports DOWN: $PING_FIRST_LINE"
ISSUES+=("controller-hung")
NEXT_STEPS+=("controller restart — see references/slurm-details.md#-c-controller-state--diagnostic-context")
else
bad "slurmctld responded with an unrecognized status: $PING_FIRST_LINE"
ISSUES+=("controller-hung")
NEXT_STEPS+=("inspect logs first; controller restart only if logs confirm a hang")
fi
# --- Section C-1: slurmdbd connectivity (controller-state restart trigger) ---
section "C (slurmdbd): accounting daemon connectivity"
DBD_OUT=$(ssm_run "$SSM_HEAD" 'sacctmgr -i show stats 2>&1 | head -20' || true)
if grep -qiE 'unable to contact|connection refused|cannot connect|no slurmdbd' <<< "$DBD_OUT"; then
bad "slurmctld cannot reach slurmdbd"
info "$(head -3 <<< "$DBD_OUT")"
hint "diagnostic and recovery procedure:"
info " https://slurm.schedmd.com/accounting.html"
info " references/slurm-details.md#slurmdbd-connectivity"
ISSUES+=("slurmdbd-disconnected")
NEXT_STEPS+=("restore slurmdbd connectivity (see AWS / Slurm docs linked above)")
elif grep -qiE 'rollup|rpc' <<< "$DBD_OUT"; then
ok "slurmdbd reachable"
else
warn "could not determine slurmdbd state from sacctmgr output"
info "if accounting is configured, run on the head node: sacctmgr show stats"
fi
# --- Section C-2: pending slurm.conf reconfiguration (controller-state restart trigger) ---
# HyperPod's slurm.conf lives at /opt/slurm-<version>/etc/slurm.conf rather than the
# upstream /etc/slurm/slurm.conf, so the remote script asks scontrol where the live
# config is. The output is a `<conf-mtime>|<ctld-start>|<conf-path>` line that we
# match strictly with a regex before parsing.
section "C (config): slurm.conf freshness"
read -r -d '' F_REMOTE <<'REMOTE_F' || true
set -e
# nosemgrep: bash.lang.correctness.unquoted-expansion.unquoted-variable-expansion-in-command
_CONF="$(scontrol show config 2>/dev/null | awk -F= '/^SLURM_CONF/ {gsub(/ /,"",$2); print $2; exit}')"
CONF_MTIME=0
if [ -n "$_CONF" ] && [ -r "$_CONF" ]; then
CONF_MTIME=$(stat -c %Y "$_CONF" 2>/dev/null || echo 0)
fi
CTLD_TS=$(systemctl show slurmctld -p ActiveEnterTimestamp --value 2>/dev/null || true)
CTLD_START=0
if [ -n "$CTLD_TS" ]; then
CTLD_START=$(date -d "$CTLD_TS" +%s 2>/dev/null || echo 0)
fi
printf 'F_RESULT|%s|%s|%s\n' "${CONF_MTIME}" "${CTLD_START}" "${_CONF}"
REMOTE_F
F_LINE=$(ssm_run "$SSM_HEAD" "$F_REMOTE" 2>/dev/null | grep -E '^F_RESULT\|[0-9]+\|[0-9]+\|' | head -1 || true)
if [[ "$F_LINE" =~ ^F_RESULT\|([0-9]+)\|([0-9]+)\|(.*)$ ]]; then
CONF_MTIME="${BASH_REMATCH[1]}"
CTLD_START="${BASH_REMATCH[2]}"
CONF_PATH="${BASH_REMATCH[3]}"
# CONF_PATH must be a real-looking absolute path before we put it into operator-
# facing recommendations. Reject anything that has shell-active characters.
if ! [[ "$CONF_PATH" =~ ^/[A-Za-z0-9._/-]+$ ]]; then
warn "slurm.conf path returned by remote did not validate; skipping freshness check"
elif [[ "$CONF_MTIME" -gt "$CTLD_START" && "$CTLD_START" -gt 0 ]]; then
DELTA=$((CONF_MTIME - CTLD_START))
warn "$CONF_PATH modified ${DELTA}s after slurmctld last started — config may be stale in memory"
hint "for the reload-vs-restart decision and procedure, see:"
info " https://slurm.schedmd.com/scontrol.html"
info " https://slurm.schedmd.com/slurm.conf.html"
info " references/slurm-details.md#scontrol-reconfigure-vs-restart"
ISSUES+=("stale-conf")
NEXT_STEPS+=("review reload procedure in linked docs")
else
ok "slurm.conf older than slurmctld start time — no pending reconfigure"
fi
else
warn "could not determine slurm.conf vs slurmctld timestamps"
fi
# --- Check for stuck jobs -----------------------------------------------------
section "4. Job queue health"
SQUEUE_OUT=$(ssm_run "$SSM_HEAD" 'squeue -h -o "%i|%T|%r" 2>&1 | head -200' || true)
if [[ $(printf '%s\n' "$SQUEUE_OUT" | wc -l) -ge 200 ]]; then
warn "squeue output reached the 200-line cap — stuck-job counts below may underreport on this large cluster"
fi
STUCK_PENDING=0
STUCK_COMPLETING=0
while IFS='|' read -r jobid state reason; do
[[ -z "$jobid" ]] && continue
[[ "$state" == "PENDING" && "$reason" == "Resources" ]] && STUCK_PENDING=$((STUCK_PENDING+1))
[[ "$state" == "COMPLETING" ]] && STUCK_COMPLETING=$((STUCK_COMPLETING+1))
done <<< "$SQUEUE_OUT"
if [[ $STUCK_PENDING -gt 0 ]]; then
warn "$STUCK_PENDING job(s) PENDING with Reason=Resources"
if [[ ${#DOWN_NODES[@]} -eq 0 ]]; then
ISSUES+=("stuck-pending-with-idle-nodes")
NEXT_STEPS+=("controller restart — Section C")
fi
fi
if [[ $STUCK_COMPLETING -gt 0 ]]; then
bad "$STUCK_COMPLETING job(s) stuck in COMPLETING"
ISSUES+=("stuck-completing")
NEXT_STEPS+=("controller restart — Section C")
fi
[[ $STUCK_PENDING -eq 0 && $STUCK_COMPLETING -eq 0 ]] && ok "no stuck jobs"
# --- Per-node inspection (read-only) ------------------------------------------
inspect_node() {
local slurm_node="$1"
# Defense-in-depth: validate again at the boundary even though all upstream paths
# validate. Cheap, and catches future refactors that miss a callsite.
slurm_node=$(validate_node_name "$slurm_node")
local instance_id group ssm_target
# PrivateDnsName looks like `ip-10-1-2-3.us-west-2.compute.internal`. The strict
# `<name>.` match handles the default `ip-x-x-x-x` form and rejects the false
# positive where node `ip-10-1-2-3` would otherwise also match
# `ip-10-1-2-30.<region>.compute.internal`.
instance_id=$(jq -r --arg dns "$slurm_node" '
.ClusterNodeSummaries[]?
| select((.PrivateDnsName // "") | startswith($dns + "."))
| .InstanceId' <<< "$NODES_JSON" | head -1)
if [[ -z "$instance_id" ]]; then
if [[ ! "$slurm_node" =~ ^ip-[0-9]+-[0-9]+-[0-9]+-[0-9]+$ ]]; then
warn "$slurm_node: not in the default ip-X-X-X-X form — Slurm-node-name → instance-ID auto-mapping needs DNS lookup or scontrol show node, neither cheap from here. Pass --target-instance-id <i-xxx> if you have it, or look up via 'scontrol show node $slurm_node | grep NodeAddr' on the controller."
else
warn "$slurm_node: cannot map to instance ID (PrivateDnsName mismatch — verify node is in this cluster)"
fi
return
fi
instance_id=$(validate_instance_id "$instance_id")
group=$(jq -r --arg id "$instance_id" \
'.ClusterNodeSummaries[] | select(.InstanceId==$id) | .InstanceGroupName // ""' <<< "$NODES_JSON")
group=$(validate_group_name "$group")
ssm_target="sagemaker-cluster:${CLUSTER_ID}_${group}-${instance_id}"
local slurmd_status disk mem rpc_check
slurmd_status=$(ssm_run "$ssm_target" 'systemctl is-active slurmd 2>&1' | tr -d '\r\n' || true)
disk=$(ssm_run "$ssm_target" 'df -h / | awk "NR==2 {print \$5}"' | tr -d '\r\n' || true)
mem=$(ssm_run "$ssm_target" 'free -h | awk "/Mem:/ {print \$3\"/\"\$2}"' | tr -d '\r\n' || true)
# Slurm-RPC reachability: srun -w "$NODE" hostname. The remote script reads $NODE
# from the environment, so the slurm node name is never string-interpolated into
# the remote shell — it lives in env-var space the whole way.
rpc_check=$(ssm_run "$SSM_HEAD" 'timeout 10 srun --immediate=5 -w "$NODE" hostname 2>&1 | tail -1' \
"NODE=$slurm_node" | tr -d '\r\n' || true)
info "$slurm_node ($instance_id): slurmd=$slurmd_status disk=$disk mem=$mem"
info " srun RPC: ${rpc_check:-<no output>}"
local disk_num="${disk%\%}"
if [[ "$disk_num" =~ ^[0-9]+$ && "$disk_num" -ge 95 ]]; then
bad " $slurm_node: root volume ${disk} — clean up before any restart"
info " HyperPod storage layout: https://github.com/aws/sagemaker-hyperpod-cluster-setup/blob/troubleshooting-doc-20250917/troubleshoot/index.md"
ISSUES+=("disk-full-$slurm_node")
NEXT_STEPS+=("clean disk on $slurm_node before recovery")
fi
if [[ "$slurmd_status" != "active" ]]; then
bad " $slurm_node: slurmd is '$slurmd_status'"
info " for recovery procedure, see:"
info " https://github.com/aws/sagemaker-hyperpod-cluster-setup/blob/troubleshooting-doc-20250917/troubleshoot/index.md"
fi
if [[ -n "$rpc_check" ]] && grep -qiE 'auth|munge|invalid' <<< "$rpc_check"; then
bad " $slurm_node: srun reports auth/munge error — slurmd-controller trust broken"
info " for munge troubleshooting, see Slurm authentication docs:"
info " https://slurm.schedmd.com/authentication.html"
fi
}
if [[ -n "$TARGET_NODE" ]]; then
section "5. Inspecting node: $TARGET_NODE"
inspect_node "$TARGET_NODE"
elif [[ ${#DOWN_NODES[@]} -gt 0 || ${#REBOOT_NODES[@]} -gt 0 ]]; then
section "5. Inspecting affected nodes"
for entry in "${DOWN_NODES[@]-}"; do
[[ -z "$entry" ]] && continue
inspect_node "${entry%%|*}"
done
for n in "${REBOOT_NODES[@]-}"; do
[[ -z "$n" ]] && continue
inspect_node "$n"
done
fi
# --- Section E: HyperPod auto-resume support + recent missed-resume detection ---
section "E. Auto-resume support"
AR_HELP=$(ssm_run "$SSM_HEAD" 'srun --help 2>&1 | grep -i auto-resume | head -3' || true)
if [[ -n "$AR_HELP" ]]; then
ok "srun --auto-resume is available on this cluster"
else
warn "srun --auto-resume not found in srun --help output"
info "this AMI / Slurm build may predate HyperPod auto-resume support"
info "see: references/slurm-details.md#hyperpod-auto-resume"
ISSUES+=("auto-resume-unsupported")
NEXT_STEPS+=("upgrade the cluster AMI / Slurm package to enable --auto-resume")
fi
read -r -d '' G_FAILS <<'REMOTE_G' || true
sacct -X -n --starttime=now-6hours \
-o JobID,State,ExitCode,NodeList \
--state=NODE_FAIL,FAILED 2>/dev/null \
| awk 'NF>=4 && $4!~/None/ {print $1"|"$2"|"$4}' | head -50
REMOTE_G
RECENT_FAILS=$(ssm_run "$SSM_HEAD" "$G_FAILS" 2>/dev/null || true)
MISSED_AR=()
NOW_EPOCH=$(date +%s)
while IFS='|' read -r jobid state nodelist; do
[[ -z "$jobid" ]] && continue
# Only single-node failures — multi-node lists need a real range expander.
[[ "$nodelist" == *,* || "$nodelist" == *\[* ]] && continue
# Validate before passing to remote.
if ! [[ "$nodelist" =~ ^[a-zA-Z0-9._-]{1,253}$ ]]; then
continue
fi
# A successful HyperPod replace clears the node's Reason field once the new instance
# registers, so grepping for "Action:Replace" is unreliable. Detect a recent replace
# by comparing scontrol show node's BootTime to wall-clock: a fresh BootTime within
# the last 6h that's later than the failed-job's End time strongly suggests the node
# was replaced (or rebooted) after the job died.
BOOT_LINE=$(ssm_run "$SSM_HEAD" 'scontrol show node "$NODE" 2>/dev/null | tr " " "\n" | grep "^BootTime="' \
"NODE=$nodelist" | head -1 | tr -d '\r\n' || true)
BOOT_STR="${BOOT_LINE#BootTime=}"
[[ -z "$BOOT_STR" || "$BOOT_STR" == "Unknown" ]] && continue
BOOT_EPOCH=$(date -d "$BOOT_STR" +%s 2>/dev/null || echo 0)
[[ "$BOOT_EPOCH" =~ ^[0-9]+$ && "$BOOT_EPOCH" -gt 0 ]] || continue
AGE=$((NOW_EPOCH - BOOT_EPOCH))
if [[ $AGE -ge 0 && $AGE -le 21600 ]]; then # 6h window
MISSED_AR+=("$jobid|$state|$nodelist|$BOOT_STR")
fi
done <<< "$RECENT_FAILS"
if [[ ${#MISSED_AR[@]} -gt 0 ]]; then
bad "${#MISSED_AR[@]} recent job(s) failed on a node that was rebooted/replaced shortly after — possible missed auto-resume:"
for entry in "${MISSED_AR[@]}"; do
IFS='|' read -r jobid state nodelist boot <<< "$entry"
info " job $jobid ($state) on $nodelist (node BootTime=$boot)"
done
info "(heuristic: node BootTime is within the last 6h, suggesting a replace or reboot)"
hint "verify the launch command used srun --auto-resume=1 (NOT just sbatch):"
info " sacct -j <JOBID> -o JobID,JobName,Submit,Start,End,State,ExitCode,NodeList -X"
info " scontrol show job <JOBID> # only if still in the controller's recent history"
info "see: references/slurm-details.md#hyperpod-auto-resume"
ISSUES+=("missed-auto-resume")
NEXT_STEPS+=("verify --auto-resume=1 is on the srun line, not just sbatch")
elif [[ -n "$RECENT_FAILS" ]]; then
ok "recent failed jobs do not match the missed-auto-resume pattern"
else
ok "no recent NODE_FAIL / FAILED jobs in the last 6h"
fi
# --- Findings → documentation links ------------------------------------------
# This skill is diagnostic-only. It never prints a remediation command. For each
# finding, point the user at the authoritative doc and let them act.
section "Where to read next"
if [[ ${#REBOOT_NODES[@]} -gt 0 ]]; then
hint "Section B — nodes flagged 'unexpectedly rebooted':"
for n in "${REBOOT_NODES[@]}"; do
info " $n"
done
info " HyperPod Slurm troubleshooting:"
info " https://github.com/aws/sagemaker-hyperpod-cluster-setup/blob/troubleshooting-doc-20250917/troubleshoot/index.md"
info " diagnostic context: references/slurm-details.md#-b-unexpected-reboot--diagnostic-context"
fi
if [[ ${#DOWN_NODES[@]} -gt 0 ]]; then
hint "Section A — nodes DOWN/DRAIN:"
for entry in "${DOWN_NODES[@]}"; do
n="${entry%%|*}"; r="${entry#*|}"
info " $n (reason: $r)"
done
info " HyperPod Slurm troubleshooting:"
info " https://github.com/aws/sagemaker-hyperpod-cluster-setup/blob/troubleshooting-doc-20250917/troubleshoot/index.md"
info " if the node flaps after a manual recovery → route to hyperpod-node-debugger"
fi
CTRL_RESTART_REASON=""
ISSUES_STR=" ${ISSUES[*]-} "
[[ "$ISSUES_STR" == *" controller-hung "* ]] && CTRL_RESTART_REASON="scontrol ping failed"
[[ "$ISSUES_STR" == *" stuck-completing "* ]] && CTRL_RESTART_REASON="${CTRL_RESTART_REASON:+$CTRL_RESTART_REASON, }jobs stuck COMPLETING"
[[ "$ISSUES_STR" == *" stuck-pending-with-idle-nodes "* ]] && CTRL_RESTART_REASON="${CTRL_RESTART_REASON:+$CTRL_RESTART_REASON, }jobs PENDING with idle nodes"
if [[ -n "$CTRL_RESTART_REASON" ]]; then
hint "Section C — controller-state issue ($CTRL_RESTART_REASON):"
info " Slurm slurmctld(8) — for what is preserved across a controller restart:"
info " https://slurm.schedmd.com/slurmctld.html"
info " HyperPod Slurm troubleshooting:"
info " https://github.com/aws/sagemaker-hyperpod-cluster-setup/blob/troubleshooting-doc-20250917/troubleshoot/index.md"
if [[ ${#FAIL_NODES[@]} -gt 0 ]]; then
warn "HyperPod recovery is in progress on:"
for entry in "${FAIL_NODES[@]}"; do
n="${entry%%|*}"
info " $n"
done
info "AWS docs warn against changing node state or restarting slurmctld during a"
info "replacement; wait for it to complete, then re-run this script."
fi
info " diagnostic context: references/slurm-details.md#-c-controller-state--diagnostic-context"
fi
if [[ "$ISSUES_STR" == *" missed-auto-resume "* ]]; then
hint "Section E — recent job failed on a node that was later replaced:"
info " the most common cause is --auto-resume on sbatch instead of srun."
info " Use SageMaker HyperPod auto-resume:"
info " https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-hyperpod-resiliency-slurm-auto-resume.html"
info " diagnostic context: references/slurm-details.md#hyperpod-auto-resume"
fi
# --- Summary ------------------------------------------------------------------
section "Summary"
printf ' Issues detected: %d\n' "${#ISSUES[@]-0}"
if [[ ${#ISSUES[@]-0} -eq 0 ]]; then
ok "cluster Slurm state is healthy"
else
echo ""
echo " Findings:"
for i in "${ISSUES[@]}"; do
info "- $i"
done
fi
if [[ ${#NEXT_STEPS[@]-0} -gt 0 ]]; then
echo ""
echo " Where to read next:"
for s in "${NEXT_STEPS[@]}"; do
info "- $s"
done
fi
Related skills
FAQ
Will this skill run fixes for me?
No. It is read-only and diagnostic-only; for any remediation it links to official AWS or Slurm docs that you authorize and execute.
What if my cluster runs EKS?
If describe-cluster shows Orchestrator.Eks, it stops and routes you to hyperpod-node-debugger or hyperpod-nccl.