
Skill
hyperpod-incident-rca
perform root-cause analysis for HyperPod incidents
Description
Root-cause analysis for a SageMaker HyperPod incident, after triage has decided to PROCEED. Runs at the INCIDENT_RCA stage. Reads describe-cluster, list-cluster-nodes, list-cluster-events, and HMA CloudWatch streams; reconstructs a timeline; classifies as Suppress / Monitor / Escalate / Resolved against time budgets and recurrence statistics from the HyperPod mental model. Produces a human-readable verdict report with recommended operator actions. The complementary INCIDENT_TRIAGE skill `hyperpod-incident-triage` decides LINKED / SKIPPED / PROCEED before this skill runs.
SKILL.md
HyperPod incident RCA skill
This skill runs at the INCIDENT_RCA stage, after DevOps Agent's
triage stage decided to PROCEED with a full investigation. By the
time this skill loads, the task already has a primary investigation
slot and the agent has full AWS API access.
The complementary triage skill hyperpod-incident-triage runs at
the INCIDENT_TRIAGE stage BEFORE this skill, and decides whether an
incoming event should be LINKED / SKIPPED / PROCEED using concise
declarative correlation rules (keep distinct fault types on the same
instance group separate; skip concurrent periodic audits). Separately,
for periodic audits, the audit Lambda gates volume — it inspects
Kubernetes state itself and only invokes an investigation when a real
issue is present (plus a daily heartbeat), so a "periodic audit"
investigation reaching this skill already corresponds to something
worth looking at.
When this skill DOES run, it does triage-like classification and
root-cause analysis in one pass because they need the same evidence:
snapshot data
(describe-cluster, describe-cluster-node) alone cannot distinguish
"HyperPod is auto-recovering" from "HyperPod has given up" — only the
cross-source timeline can.
Operating policy. Read-only. Never call a state-changing API. Present every action that mutates anything (operator-driven replacement, SSM session, label change) as a Recommended action (operator runs this) block. The customer's operator decides whether to execute it.
Required reading. Open references/hyperpod-mental-model.md in parallel with this skill. The mental-model doc is the ground truth for what each HyperPod signal means; this skill is the decision procedure that consumes those meanings.
Why one skill, not two
A single failed instance can vanish from list-cluster-nodes between
retry attempts, and HyperPod may auto-retry from Failed status —
neither is a terminal escalation signal on its own. Distinguishing
"still retrying within budget" from "stuck" requires the full
list-cluster-events chain, the HMA CloudWatch stream, and the cluster
status all aligned on a wall-clock timeline. Building a separate
"triage" skill that decides without that data would mean re-deciding
incorrectly on every event.
Relationship to hyperpod-incident-triage and the audit Lambda
Two things run before this RCA skill and mean it does NOT need to re-implement duplicate/stale-audit suppression itself:
hyperpod-incident-triagemakes the LINK / SKIP / PROCEED decision with concise declarative rules (same fault on the same component links; different fault types on the same instance group stay separate; concurrent periodic audits are skipped).- The periodic-audit Lambda gates volume: for scheduled audits it inspects Kubernetes state (CrashLoopBackOff / NotReady) and POSTs the webhook only when a real issue exists — so a periodic-audit investigation reaching this skill already corresponds to a real finding, not an idle poll. HyperPod control-plane faults arrive via the event-driven webhook bridge, not the audit.
RCA still does the full Phase 1 gather (including Pod/Node inspection and Phase 3d threshold checks) because the reasoning stage needs the full evidence. Earlier RCA "stale-evidence" rules (3 / 3b) were removed and are not reintroduced — that concern is handled upstream now.
Workflow
Trigger modes
The skill is loaded from two trigger sources, and Phase 1 / Phase 3 behavior differs slightly between them:
| Mode | Trigger | Scope |
|---|---|---|
| Incident mode | Webhook fire from the bridge Lambda (an EventBridge HyperPod event passed the noise filter). The investigation context carries clusterName and usually an instanceId. | Focused on the specific incident referenced in the trigger. |
| Audit mode | A synthesized webhook POST from the periodic-audit Lambda. Unlike a raw scheduled trigger, this payload does carry context: the description names the cluster (clusterName) and, when the Lambda-side checks fired, lists detectedIssues and a k8sChecks config line. | Confirm the detected issues, then scan the named cluster for any in-flight or recently-resolved fault chains and re-classify each. Catches: (1) Monitor incidents that have now succeeded (emit Resolved), (2) Monitor incidents stuck past their re-check budget (escalate), (3) recurring patterns that didn't trigger an EventBridge event recently but persist statistically. |
In both modes the same Phase 1 / Phase 2 / Phase 3 / Phase 4 logic applies. The differences are noted inline below.
Phase 1 — Gather (run in parallel)
For the incident referenced in the trigger event (cluster name + optional instance id), collect these in parallel. Do not stop on a single signal — gather all of them before classifying.
Audit mode: the trigger payload does carry the cluster name
in its description (the periodic-audit Lambda synthesizes it), so use
that. Only if the name is genuinely absent, fall back to the cluster(s)
reachable from this Agent Space's AWS account association (typically
just one HyperPod cluster — discover it via sagemaker list-clusters).
Run the full Phase 1 gather for the cluster. Then in Phase 3, classify
per open fault chain found in the cluster-events window (plus any
detectedIssues the payload already carries), not per single trigger.
- Cluster state:
aws sagemaker describe-cluster --cluster-name <name>— currentClusterStatus,NodeRecovery,Orchestrator(Eks vs. Slurm),InstanceGroups[].CurrentCount/TargetCount/Status. - Node inventory:
aws sagemaker list-cluster-nodes --cluster-name <name>plusdescribe-cluster-nodefor any instance id named in the trigger event. An instance id mentioned in the event that is NOT inlist-cluster-nodesis a signal, not an error — the node may have been removed mid-retry. - Cluster events chain:
aws sagemaker list-cluster-events --cluster-name <name>, paginated to ≥500 entries or until events reach 7 days back, whichever comes first. This is the canonical record of replacement attempts including failed ones and survives nodes disappearing from the node list. The wider 7-day window also feeds the recurring- pattern classification rules in Phase 3 — don't shorten the lookback even when the trigger event is recent.- Available on EKS clusters and on Slurm clusters with Continuous
Provisioning enabled. If the API returns
ValidationException/equivalent on a Slurm cluster without CP, note "event timeline unavailable; confidence degraded" in the final report and proceed with the remaining sources.
- Available on EKS clusters and on Slurm clusters with Continuous
Provisioning enabled. If the API returns
- HMA CloudWatch stream: log group
/aws/sagemaker/Clusters/<NAME>/<CLUSTER_ID>, streams matchingSagemakerHealthMonitoringAgent/*(filter to the affected instance group / instance if known). Filter forHealthMonitoringAgentDetectionEvententries and any Xid / DCGM / EFA / OOM messages within the same window as Phase 1 step 3. - Lifecycle script stream (only if a replace attempt is in the
timeline): same log group, streams matching
LifecycleConfig/*for the affected instance group / instance id. Look for non-zero exit, timeout, S3 / IAM errors. - EKS node state (EKS only):
kubectl get node <name> -o yamlfor the affected node (if still present). Surfacesagemaker.amazonaws.com/node-health-status,fault-types,fault-reasons,fault-detailslabels/annotations and any taints. - Slurm node state (Slurm only, if SSM not required): use what's
reachable from the control plane —
describe-cluster-nodegives most of what we need; deepscontrol/sinforequires SSM and is out of scope here. - Kubernetes state — MANDATORY in audit mode on EKS clusters.
You MUST execute both of the following before entering Phase 2,
even if the trigger payload has no
k8sChecksblock, even if earlier gather steps already suggest a plausible verdict, and even iflist-cluster-eventsreturned a rich history. Live Pod/Node state is the source of truth for "what is broken right now" — the wider event history is context, not a substitute.kubectl get pods -A -o json— full cluster Pod state.kubectl get nodes -o json— full Node state includingstatus.conditions[]and theirlastTransitionTime.
Skip this step only if: (a) the cluster's orchestrator is Slurm (not EKS), OR (b) the trigger is incident-mode (webhook-triggered for a specific fault; Pod/Node scan belongs in periodic audit).
The trigger'sk8sChecksblock supplies thresholds and namespace filters used by Phase 3d — not a gate on whether kubectl runs. Fields:{ "enabled": true, "crashLoopMinRestarts": 5, "crashLoopRecencyMinutes": 15, "notReadyNodePercentThreshold": 10, "notReadyDurationMinutes": 15, "ignoreNamespaces": ["kube-public", "kube-node-lease"], "systemNamespaces": ["kube-system", "aws-hyperpod", "amazon-cloudwatch"] }
Where to find this block at runtime. The DevOps Agent platform preserves the top-leveldescriptionstring of the incoming task verbatim, but drops nested sub-objects from the webhook payload. The audit Lambda therefore inlines thek8sChecksblock into the task description text on a line that begins:k8sChecks configuration (parse as JSON, then apply per Phase 1 step 8 + Phase 3d): { ... }
Locate that line in the task description, extract the JSON object on the next line, and use its fields for Phase 3d thresholds and namespaces. Do NOT default any field unless the whole block is absent — the block's values override the built-in defaults regardless of whether they equal the defaults.
If the block is absent from the description entirely, use defaults:crashLoopMinRestarts=5,crashLoopRecencyMinutes=15,notReadyNodePercentThreshold=10,notReadyDurationMinutes=15,ignoreNamespaces=["kube-public","kube-node-lease"],systemNamespaces=["kube-system","aws-hyperpod","amazon-cloudwatch"].
If the block hasenabled: false, still run the kubectl commands (Phase 1's job is discovery), but Phase 3d will not fire — see note there.
Phase 1 gather sanity gate — verify BEFORE entering Phase 2
Before starting Phase 2 (timeline reconstruction), confirm that Phase 1 executed the required steps:
- Steps 1, 2, 3 (describe-cluster, list-cluster-nodes, list-cluster-events): required for both incident and audit modes.
- Step 8 (kubectl get pods, kubectl get nodes): required in audit mode on EKS. Missing here is a hard error — do not proceed to Phase 2. Instead: run step 8 now, then re-enter this gate.
Do not rationalize skipping step 8 with reasoning like "the
list-cluster-events window looks quiet so I don't need to check
pods" or "the LCS storm from earlier is more interesting." Those
are outputs of Phase 3 reasoning, not inputs to Phase 1. Pod state
must be gathered as evidence before Phase 3 rules can weigh
"live k8s problem" against "historical event pattern."
Phase 2 — Reconstruct the timeline
Build a single ordered timeline keyed by UTC timestamp across all sources, restricted to the affected scope (cluster, instance group, or instance). Mark each entry with its source. The shape should be:
T+0:00 [HMA] HealthMonitoringAgentDetectionEvent — Xid 79 on GPU 3
T+0:32 [ClusterEvent] Action:Replace marked on i-aaa (NodeRecovery=Automatic)
T+0:33 [Node] list-cluster-nodes: i-aaa removed
T+0:34 [ClusterEvent] Replacement started for instance group worker1
T+18:21 [ClusterEvent] Replacement failed: EFA health checks did not run successfully
T+18:25 [Node] list-cluster-nodes: still missing
T+20:10 [ClusterEvent] Replacement started for instance group worker1 ← second attempt
...
This is the artifact the classification phase reasons over. Include it in the final report regardless of verdict — operators need it to double-check the agent's call.
Phase 2b — Build the fault-content signature for each event
Every fault event (Error/Warn cluster event or HMA detection within the 4h window for signature-set computation, and within the 7d window for recurrence statistics) gets a signature string, formed by concatenating the event's Description with any FailureMessage the bridge Lambda enriched it with, prefixed by the InstanceGroup name.
Signature format:
<ig>:<full-description-and-failure-message-content>
Sources of the content, in order of preference:
- The bridge Lambda's enriched description (top-level
descriptionfield in the incoming trigger payload). This already includesDescription: ...+FailureMessage: ...+InstanceMetadata: ...fields the bridge assembled by callingDescribeClusterEvent. - For events retrieved via
list-cluster-eventsduring Phase 1 pagination (not through the bridge), calldescribe-cluster-event --event-id <eid>yourself for each Error/Warn EventId. ConcatenateDescription+EventDetails.EventMetadata.Instance.FailureMessage(and any other populatedInstance.*fields) the same way the bridge does. - For HMA CloudWatch stream events: use the full
HealthMonitoringAgentDetectionEventtext. - Cluster State Change events: use
ClusterStatus + Description.
Why full concatenated content, not hard-coded categories.
Classifying events into a fixed enum of categories (gpu-xid,
lifecycle-script-failed, capacity-insufficient, etc.) via regex
rules merges distinct fault types — for example, "capacity for
ml.g5.8xlarge", "capacity for ml.p5.48xlarge", "EFA health check
failed", and "generic provisioning failure" all collapse to the same
instance-creation-failed:generic key, because the top-level
Description field only says "Failed to provision EC2 Instance in Cluster ...". The actual root cause lives in FailureMessage (a
different string per fault type), so include it. Concatenated raw
content is more robust than enum-based categories.
InstanceGroup extraction:
- Cluster Event:
detail.EventDetails.InstanceGroupName - Node Health / Cluster State Change:
detail.InstanceGroupName - Cluster-level fault: empty (
"") — no IG prefix.
Phase 2c — Compute recurrence statistics
Over the 7-day list-cluster-events window from Phase 1 (and HMA
CloudWatch stream over the same window), compute and record (used by
Phase 3 rules 6–8):
replacements_7d_total— count ofReplaceactions / replacement- started cluster events across the whole cluster in the last 7 days.replacements_7d_by_group[<ig>]— same, partitioned by InstanceGroup.replacements_24h_total— same metric, 24h window.signature_count_7d[<signature>]— count of distinct fault events with the same signature string in the last 7 days. Uses the Phase 2b signature format<ig>:<content>. Exclusion: events whose Description contains"lost orchestration-ready status"MUST be excluded from signature counting. These are scale-in-progress noise (see mental-model § "Scale-in-progress emits spurious Warn events"), not fault signals. Do NOT count them toward rule 6 thresholds.
Include these counts in the verdict description's "What HyperPod is doing right now" paragraph when they're ≥2 — even if the verdict itself doesn't change, the operator should see the count.
Phase 2d — Compute the signature set
The signature set is used for verdict-title generation. Encoding it in the title lets the DevOps Agent platform's title-based dedup absorb back-to-back audits of the same fault set (identical set → identical title), while a new fault set produces a new title and a fresh investigation.
current_signature_set— sorted, deduplicated set of signature strings (per Phase 2b) for every distinct fault event in the 4-hour window. Examples:{"worker4:Description: Failed to provision EC2 Instance in Cluster my-cluster and InstanceGroup worker4. FailureMessage: We currently do not have sufficient capacity to launch new ml.g5.8xlarge instances. Please try again."}{"worker2:Description: Instance i-XXXX is unhealthy. HyperPod Health Monitoring Agent (HMA) has detected fault type NvidiaGPUUnhealthy on this node and is unhealthy. Repair action: Replace."}
Render in the verdict title as a sorted comma-separated list. Full strings; do NOT truncate. If total title length exceeds platform limits (rare but possible for very long FailureMessages), trim signatures from the middle with...markers to preserve the beginning (where the discriminating text lives).current_most_recent_event_at— the latestEventTimeof any Error/Warn cluster event or HMA detection within the window.
Phase 3 — Classify
Ordering — MANDATORY.
- First, run Phase 3d (Kubernetes-state checks — CrashLoopBackOff, NotReady). Emit any Escalate verdicts from Phase 3d immediately.
- Then, run the fault-chain rules below in this Phase 3 table.
Reason: Phase 3d looks at live current state, which is the most actionable signal for a periodic audit. The main rule table below looks at the 4h/24h/7d event history, which is context. When both would fire, the operator needs the live state surfaced regardless of whether historical patterns are ALSO present. Do not skip Phase 3d because "the event history already provides an interesting verdict" — that produced the 2026-07-09 miss where a live CrashLoopBackOff was overshadowed by a recurring-LCS-pattern verdict from a already-resolved 07-08 storm.
Emitting both a Phase 3d verdict and a Phase 3 verdict on the same audit is expected and correct. Each becomes its own symptom record; the operator gets both signals via email.
Phase 3d — Kubernetes-state checks (audit mode, EKS only)
Run this BEFORE the fault-chain classification rules below
(see the "Ordering — MANDATORY" note at the top of Phase 3). Phase
3d uses the kubectl get pods / kubectl get nodes output from
Phase 1 step 8, which is mandatory in audit-mode-on-EKS regardless
of the k8sChecks payload block. Emit each verdict as an
independent symptom record.
Threshold + namespace configuration. Use the k8sChecks block
parsed from the task description line per Phase 1 step 8 — the
DevOps Agent platform preserves the top-level description verbatim
but flattens nested payload sub-objects, so data.metadata.k8sChecks
is NOT reliably present; the description-inline copy is the source of
truth. If no block is in the description, use the defaults documented
in Phase 1 step 8. If the block is present with enabled: false, DO
NOT emit Phase 3d verdicts (customer has opted out of the k8s-state
Escalate) — but Phase 1's kubectl gather still ran because it's part
of general state discovery, and its data may still surface in Phase 4
report context.
Pod check — CrashLoopBackOff loop. For each Pod in the
kubectl get pods -A -o json output:
- Namespace classification. Look up
pod.metadata.namespace:- If it is in
k8sChecks.ignoreNamespaces→ skip this pod entirely. - If it is in
k8sChecks.systemNamespaces→ workload class issystem-workload. - Otherwise → workload class is
customer-workload.
This is a plain set-membership lookup, in this exact order. No pattern matching, no wildcards, no precedence rules. The Lambda has already validated thatignoreNamespacesandsystemNamespacesdo not overlap, so a pod's namespace matches at most one list. - If it is in
- CrashLoopBackOff detection. For each container in
pod.status.containerStatuses[], decide whether it is in a persistent crash loop. Kubernetes does not expose when the loop started in a snapshot —containerStatuses[].state.waitingcarries onlyreason/message(no timestamp), andlastState.terminatedholds only the most recent crashed run — so use crash evidence, not a duration:- Loop present when
state.waiting.reason == "CrashLoopBackOff"OR alastState.terminatedrecord exists (persistent evidence, stable across the backoff cycle). - Enough restarts:
restartCount >= k8sChecks.crashLoopMinRestarts. - Still active: the last crash
(
lastState.terminated.finishedAt) is withink8sChecks.crashLoopRecencyMinutesminutes — or the container is currently inCrashLoopBackOff(treat a missingfinishedAtas active only in that case).
- Loop present when
- Verdict. If all three hold → emit an
Escalateverdict with:- Title:
Triage verdict: Escalate — CrashLoopBackOff loop active :: (<namespace>/<pod>:<container>) - Workload class tag in the description (
system-workloadorcustomer-workload) so downstream email routing can decide who to page. - Include: pod name, namespace, container name,
restartCount, whether it is currently in backoff, and the most recentlastState.terminated.reason/.exitCodeif available.
- Title:
Node check — NotReady percentage. From
kubectl get nodes -o json:
- Total node count = length of
items[]. - For each node, examine
status.conditions[]fortype: Ready. Ifstatus: "False"or"Unknown"ANDnow() - lastTransitionTimeexceedsk8sChecks.notReadyDurationMinutesminutes → count as NotReady. - If
(NotReady count / total count) * 100meets or exceedsk8sChecks.notReadyNodePercentThreshold→ emit anEscalateverdict with:- Title:
Triage verdict: Escalate — NotReady nodes exceeded threshold :: (<n>/<total> nodes NotReady) - Include the affected node names, their NotReady durations, and
any
taints[]that would explain the state.
- Title:
Interaction with the fault-chain classification. If a HyperPod fault chain in the main rule table already covers the same node (e.g. rule 11 says the retry chain is stuck for a specific instance), the Phase 3d NotReady check may re-flag the same node. That's fine — the two verdicts have different scopes and different recommended actions. Emit both; the operator gets richer context.
Interaction with Suppress — periodic audit, no open incidents.
Rule 1 fires when no HyperPod fault chains are open. Phase 3d can
still fire independently — a CrashLoopBackOff pod is an incident
even when HyperPod's own event stream is quiet. If Phase 3d
produces any verdict, do NOT emit the rule 1 Suppress; Phase 3d has
found something to report.
Fault-chain classification rules
Apply the rules below in order. Stop at the first match.
Audit mode: classify each open fault chain found in the
cluster-events window independently. Emit one verdict per chain
(plus one Suppress — periodic audit, no open incidents if no
chains are open AND Phase 3d also emitted nothing). A fault chain
is "open" if it had a fault event within the last 4 hours and has
not yet emitted a successful Running transition + 30 min
clean-window.
Historical-only chains are not open. If the only fault events
in the 4-hour window belong to a chain that has already completed
(e.g., all replacements succeeded and all affected nodes are back
in Running for ≥30 min), the chain is closed. Do not emit an
Escalate verdict on the basis of signature_count_7d alone
against a closed chain unless the recurrence rule 6/7/8 fires from
statistics that INCLUDE at least one event within the last 4 hours.
A 7-day-old already-recovered pattern is context, not an actionable
incident.
The recurring-pattern rules (6–8) are checked before the
single-incident rules (9–13) — a node that's auto-recovering for the
3rd time this week should be flagged as a pattern, not silently
classified as Monitor — first attempt.
| # | Signal pattern | Verdict |
|---|---|---|
| 1 | Audit mode AND no fault events in the 4-hour window AND no Monitor fault chain still open | Suppress — periodic audit, no open incidents (skip Phase 4 entirely or emit a minimal record; nothing actionable) |
| 2 | A previously-Monitor fault chain now shows the affected instance(s) back in Running AND no new HMA detection for that instance in the last 30 min AND cluster status is InService | Resolved — auto-recovery succeeded (operator gets a closure email; include the original detection time and total elapsed) |
| 3 | Trigger detail-type is Cluster Event with EventLevel=Info and the timeline shows no node-health activity | Suppress |
| 4 | Cluster status is Failed or RollingBack | Escalate (cluster-level) |
| 5 | NodeRecovery=None on the cluster AND a node has been marked Action:* / UnschedulablePending* AND no replacement has started within 5 minutes | Escalate — auto-recovery is off; operator must trigger replacement |
| 6 | signature_count_7d[<signature>] ≥ 3 for ANY signature — same fault content (Description + FailureMessage) has driven ≥3 replacements on the same InstanceGroup in the last 7 days (auto-recovery may be succeeding each time). Because signatures are the full concatenated content, this fires cleanly for repeated same-cause faults (e.g. 3× capacity errors for the same instance type) but does NOT over-merge distinct causes. | Escalate — recurring fault pattern: HyperPod is repairing the symptom, but the underlying cause hasn't gone away. Include the signature (first ~200 chars) and the timestamps of all prior occurrences. Recommend operator actions appropriate to the fault content (vendor-exclusion for hardware Xid faults; LCS bug for lifecycle-script failures; capacity request or IG-move for capacity failures; VPC/SG review for EFA health-check failures). Infer the appropriate action class from the FailureMessage content, not from a hard-coded category enum. |
| 7 | replacements_24h_total ≥ 5 — five or more replacements anywhere in the cluster within 24 hours | Escalate — fleet-wide instability: the rate of node churn is abnormal regardless of individual root causes. |
| 8 | replacements_7d_by_group[<ig>] ≥ 5 — five or more replacements on the same InstanceGroup in 7 days | Escalate — instance-group instability: the affected IG (which may be a specific SKU or topology placement) is failing more often than the rest of the cluster. |
| 9 | Exactly one replacement attempt in flight, started within the last 30 minutes, no prior failure in the chain | Monitor — first attempt (next re-check in 30 min via scheduled audit) |
| 10 | Multiple replacement attempts in the chain, total elapsed since the first failure ≤ 90 minutes, the most recent attempt is Running or Started (not yet failed) | Monitor — elevated (retry in progress, watch closely) |
| 11 | Multiple replacement attempts, total elapsed > 90 minutes, no successful Running transition, AND no new attempt started within the last 30 minutes | Escalate — retry chain is stuck |
| 12 | Node was in Failed state AND no new replacement attempt has started within the last 30 minutes AND total time in failing chain > 60 minutes | Escalate — HyperPod has given up |
| 13 | Instance id from the trigger event is missing from list-cluster-nodes AND list-cluster-events shows no new attempt for the last 30 minutes AND the most recent attempt failed | Escalate — instance vanished, no retry |
| 14 | HMA detection event present but no corresponding Action:* / replacement event in the timeline within 10 minutes | Escalate — HMA fired without escalating; investigate why (mismatch in node-recovery config, signal didn't classify) |
| 15 | None of the above match | Monitor — uncategorized (include the full timeline; flag for review) |
Duplicate / stale-audit suppression is handled upstream, not by this skill: the
hyperpod-incident-triageskill LINKs/SKIPs duplicate incident events and concurrent audits, and the periodic-audit Lambda only invokes an investigation when a real issue is present. The former RCA "stale-evidence" rules (3 / 3b) were removed and are not reintroduced.
Rule 2 (Resolved) closes the loop on prior Monitor verdicts.
A previous Monitor verdict promised a re-check; this rule provides
that re-check via the scheduled audit. The verdict description
should explicitly say "Incident detected at
Rule 1 (Suppress — periodic audit, no open incidents) is the no-op
case. When the scheduled audit fires on a healthy cluster, emit a
single minimal record acknowledging the audit ran. The email notifier
filters Suppress verdicts so no email is sent.
Recurring-pattern verdicts (6–8) are Escalate even when the
individual incident is auto-recovering correctly. The reasoning is
in your operational goals: HyperPod's resiliency repairs the symptom,
but a 3× recurrence of a categorized fault on the same IG is an
underlying cause (vendor / capacity pool for gpu-xid, code bug for
lifecycle-script-failed, pool exhaustion for capacity-insufficient,
etc.) that auto-recovery can't fix. The verdict explanation should
explicitly say "this incident is auto-recovering, but the pattern
across the last
Time budgets are not hardcoded constants — they encode the mental-model doc's "How long things take" section. A single replace takes 20–30 min; two attempts plus a slack gap = ~90 min. Don't change these without updating the mental-model doc first.
Phase 4 — Report (using DevOps Agent's native schema)
DevOps Agent's investigation output is structured: the terminal tool
is write_final_investigation_report, and the agent emits symptom
and finding records along the way that get serialized into the
final report. Author the verdict and timeline into that schema
directly — do NOT invent a separate four-section markdown format
(it would be at the agent's mercy during serialization and may not
survive into the final report).
The agent's schema supports these record types:
| Record type | Used for |
|---|---|
symptom | Observable state the operator would notice (a node went unhealthy, an investigation was triggered, the cluster is in Failed state). First-class; survives serialization with title + description verbatim. |
finding with finding_type: "root_cause" | An identified hardware/software root cause that cascades to one or more symptoms. |
finding with finding_type: "cause" | An intermediate cause linking root_cause to symptoms. |
finding with finding_type: "hypothesis" | An unverified explanation. |
investigation_gaps[] | What the agent could not verify and would need the operator to confirm. |
Emit exactly these records, in this order:
- First
symptom— titledTriage verdict: <verdict-name> :: <signature-set>where<verdict-name>is one ofSuppress,Monitor — first attempt,Monitor — elevated,Monitor — uncategorized,Resolved — auto-recovery succeeded, orEscalate — <reason>. The<signature-set>suffix is required for any verdict that names specific fault evidence (all Escalate / Monitor / Resolved variants) — it's a stable, sorted-and-deduplicated representation of thecurrent_signature_setcomputed in Phase 2d, formatted as a comma-separated list of the<ig>:<content>signature strings (per Phase 2b) that drove the verdict. Verdict titles use the full concatenated fault-content signature, NOT a hard-coded category name. Examples (line-wrapped here for readability; the actual titles are single-line):Triage verdict: Escalate — recurring fault pattern :: (worker4:Description: Failed to provision EC2 Instance ... FailureMessage: We currently do not have sufficient capacity to launch new ml.g5.8xlarge instances. Please try again.)Triage verdict: Monitor — first attempt :: (worker2:Description: Instance i-XXXX is unhealthy. HyperPod Health Monitoring Agent (HMA) has detected fault type NvidiaGPUUnhealthy on this node ...)Triage verdict: Resolved — auto-recovery succeeded :: (worker2:Description: Instance i-XXXX is unhealthy ...)Triage verdict: Escalate — fleet-wide instability :: (worker2:..., worker3:..., worker4:...)Triage verdict: Suppress — periodic audit, no open incidents(no suffix when there's no signature set)
Purpose: the DevOps Agent platform's automatic task-dedup uses title equality within a ~30 minute window. Encoding the signature set in the title means a NEW signature (a new Xid type on the same IG, or the same Xid spreading to a new IG) produces a different title and the platform does NOT dedup it — the new investigation runs. Conversely, identical signature sets produce identical titles and platform dedup quietly absorbs the duplicate audit.
The description is the prose justification in the shape:Verdict: <name> Summary: <A single plain-English paragraph (2–4 sentences) that flows naturally and weaves together THREE things, in this order: (1) WHAT HAPPENED — the specific observed problem: fault type, affected instance group / node / instance type, and how many times / how long (never just the generic verdict category); (2) THE LIKELY CAUSE — the most probable root cause inferred from the FailureMessage / event content and cluster context (not a hardcoded category); (3) THE RECOMMENDED ACTION — concrete operator next step(s) appropriate to the cause. Write it as prose, NOT as labeled fields or bullets. Lead the paragraph with "what happened" so the first sentence works as a standalone headline. Example: "Repeated capacity errors are failing to provision ml.p5.48xlarge for instance group 'faulttest' on cluster my-slurm-cluster — 5 attempts in the last 40 minutes. The likely cause is that on-demand capacity for ml.p5.48xlarge is unavailable in this Availability Zone and the instance group is not backed by a training plan or reserved capacity, so every Continuous-Provisioning retry hits the same wall. Recommended: launch in an AZ/Region where you hold reserved capacity for this instance type or associate the group with a training plan; otherwise reduce the target count to stop the retry loop and request a capacity increase."> What HyperPod is doing right now: <one paragraph plain English summary referencing the timeline> Timeline (UTC): <the timeline reconstructed in Phase 2, one event per line, source-tagged> Most recent event at: <UTC ISO 8601 timestamp of the most recent fault event in the 4-hour window. Recommended for all verdicts that contain a non-empty signature set so operators reading the report can correlate with their own timelines. Example: 2026-06-30T01:36:49Z> Next re-check: <only for Monitor verdicts: UTC timestamp 30 min from now if "first attempt", 15 min from now if "elevated"> Recommended actions (operator runs these): <only for Escalate verdicts: explicit operator-runnable remediation steps — see the per-verdict guidance below and the SSM GPU-UUID step for recurring-pattern verdicts. Omit this heading for Suppress / Monitor / Resolved verdicts.>
TheSummary:paragraph is REQUIRED for every non-Suppress verdict. Write it as ONE natural-language paragraph (2–4 sentences) that covers, in order, what happened → the likely cause → the recommended action. Do NOT use labeled fields, bullets, or JSON — just prose. Lead with "what happened" so the first sentence stands alone as a headline (the email notifier uses the paragraph as the body summary and its first sentence as the subject). Name the specific problem (instance type, instance group, counts, AZ) — never restate the generic verdict category. Do NOT put[direct]/[proxy]/[unverified]confidence tags in this paragraph; those belong on the detailed evidence claims later in the description. Suppress verdicts may omit it (no email).related_resourceson this symptom:["HyperPod cluster <name>"]plus any affected instance IDs. - Additional
symptomrecords — one per observable failure condition (e.g. "Instance i-xxxx marked unhealthy with NvidiaGPUUnhealthy"). These are the per-resource symptoms the agent would naturally produce; keep them as separate records so they cross-link withcascades_tofrom the findings. findingrecords as appropriate (root cause, intermediate cause, hypothesis). Cross-link viacascades_to: [<symptom-id>].investigation_gaps[]— anything Phase 1 couldn't reach. Examples:list-cluster-eventsunavailable on a Slurm cluster without Continuous Provisioning; HMA CloudWatch log group not yet populated; no SSM access from the agent (always include this one with a note that operator can confirm via the suggested SSM command).
Emit aninvestigation_gapwhenDescribeClusterEventreturned no FailureMessage for any Error/Warn event (i.e. the API returnedEventDetails.EventDetails.EventMetadata.Instanceas null or with onlyNodeLogicalId). Title:FailureMessage missing from N event(s). Description: list the affectedEventIds + timestamps. This surfaces the known HyperPod bug where FailureMessage is populated in the API response for capacity errors but omitted for other fault categories — operators reviewing the report should escalate to the HyperPod team with those EventIds as evidence.write_final_investigation_report— listing the verdict symptom FIRST insymptoms[], then the per-resource symptoms. Findings listed under theirfinding_typearrays (root_cause[],cause[],hypothesis[]).investigation_gaps[]populated from step 4.
CRITICAL: the FIRST symptom is the verdict symptom
Downstream automation (email notifier, dashboards, dedup) keys off the
FIRST symptom's title matching Triage verdict: <name> :: <signature>.
If you emit a descriptively-titled symptom first (e.g.
"worker1 lifecycle script execution failures across multiple nodes on my-cluster")
the verdict is invisible to the pipeline and no email is sent.
This has been observed to fail in RCA runs. The first
symptom record you emit MUST have title beginning with
Triage verdict:. Descriptive titles are for the second and later
symptom records, which capture per-resource observations.
Few-shot examples of the first symptom record
Copy the shape and titles exactly. Substitute your investigation's concrete data.
Example A — Escalate for a recurring capacity fault:
title: "Triage verdict: Escalate — recurring fault pattern :: (worker4:Description: Failed to provision EC2 Instance in Cluster prod-01 and InstanceGroup worker4. FailureMessage: We currently do not have sufficient capacity to launch new ml.g5.8xlarge instances. Please try again.)"
description: |
Verdict: Escalate — recurring fault pattern
Summary:
Repeated capacity errors are failing to provision ml.g5.8xlarge for instance group "worker4" — 4 failed replacements in the last 24 hours. The likely cause is that on-demand capacity for ml.g5.8xlarge is unavailable in us-west-2 for this cluster and worker4 is not backed by a training plan or reserved capacity, so each Continuous-Provisioning retry hits the same capacity wall. Recommended: launch worker4 in an AZ/Region where you hold reserved capacity or a training plan for ml.g5.8xlarge, or switch to a SKU with availability (e.g. ml.g6.8xlarge); alternatively lower worker4's target count to stop the retry loop while you request a capacity increase.
What HyperPod is doing right now:
HyperPod is auto-retrying, but the same insufficient-capacity error has driven 4 replacements on worker4 in the last 24 hours. Continuous Provisioning is looping without progress; each new attempt hits the same on-demand capacity wall for ml.g5.8xlarge in us-west-2. Operator action is required — the pattern will not self-resolve.
Timeline (UTC):
2026-07-08T15:12:03Z aws.sagemaker Cluster Event Error Failed to provision EC2 Instance in worker4 (attempt 1)
2026-07-08T15:31:47Z aws.sagemaker Cluster Event Error Failed to provision EC2 Instance in worker4 (attempt 2)
2026-07-08T15:53:22Z aws.sagemaker Cluster Event Error Failed to provision EC2 Instance in worker4 (attempt 3)
2026-07-08T16:14:59Z aws.sagemaker Cluster Event Error Failed to provision EC2 Instance in worker4 (attempt 4)
Most recent event at:
2026-07-08T16:14:59Z
Example B — Escalate for coordinated lifecycle-script failures across multiple instances:
title: "Triage verdict: Escalate — coordinated lifecycle-script failure :: (worker1:Description: Lifecycle scripts did not run successfully. Ensure the scripts exist in provided S3 path, are accessible, and run without errors.)"
description: |
Verdict: Escalate — coordinated lifecycle-script failure
Summary:
Every new worker1 instance on cluster my-cluster is failing bootstrap with the same lifecycle-script (LCS) execution error — 6 instances failed across two waves at 2026-07-08T19:21Z and 19:32Z. The likely cause is a bug or missing/inaccessible file in the on_create script in the instance group's S3 path, so each freshly provisioned node hits the identical failure. Recommended: inspect the LCS log stream for one of the affected instances to find the failing command, then fix on_create.sh (or on_create_main.sh) in the cluster's S3 bucket; the retry loop clears on the next attempt once the script succeeds.
What HyperPod is doing right now:
HyperPod is retrying, but every new worker1 instance is failing bootstrap with the same LCS execution error. 3 instances (i-0aaaa1111bbbb2222, i-0cccc3333dddd4444, i-0eeee5555ffff6666) failed simultaneously at 2026-07-08T19:21Z, then 3 more at 2026-07-08T19:32Z with the same error. Continuous Provisioning + Automatic NodeRecovery will keep respawning these logical nodes at ~10-minute intervals until an operator fixes the LCS.
Timeline (UTC):
2026-07-08T19:20:01Z aws.sagemaker Cluster Event Info Instance lifecycle script execution for i-0aaaa1... has Started
2026-07-08T19:20:05Z aws.sagemaker Cluster Event Info Instance lifecycle script execution for i-0cccc3... has Started
2026-07-08T19:20:07Z aws.sagemaker Cluster Event Info Instance lifecycle script execution for i-0eeee5... has Started
2026-07-08T19:21:02Z aws.sagemaker Cluster Event Error Lifecycle scripts did not run successfully (i-0aaaa1...)
2026-07-08T19:21:05Z aws.sagemaker Cluster Event Error Lifecycle scripts did not run successfully (i-0cccc3...)
2026-07-08T19:21:08Z aws.sagemaker Cluster Event Error Lifecycle scripts did not run successfully (i-0eeee5...)
2026-07-08T19:31:53Z aws.sagemaker Cluster Event Error Lifecycle scripts did not run successfully (i-07777a...)
2026-07-08T19:32:04Z aws.sagemaker Cluster Event Error Lifecycle scripts did not run successfully (i-08888b...)
2026-07-08T19:32:13Z aws.sagemaker Cluster Event Error Lifecycle scripts did not run successfully (i-09999c...)
Most recent event at:
2026-07-08T19:32:13Z
Recommended actions (operator runs these):
Inspect the LCS log stream /aws/sagemaker/Clusters/my-cluster/<cluster-id>/LifecycleConfig/worker1/<instance-id> for any of the affected instances to identify the failing command. Fix on_create.sh (or on_create_main.sh) in s3://<your-lcs-bucket>. Once fixed the retry loop will clear on its next attempt.
related_resources: ["HyperPod cluster my-cluster", "i-0aaaa1111bbbb2222", "i-0cccc3333dddd4444", "i-0eeee5555ffff6666"]
Example C — Monitor (first attempt in flight):
title: "Triage verdict: Monitor — first attempt :: (worker2:Description: Instance i-0abcdef1234567890 is unhealthy. HyperPod Health Monitoring Agent (HMA) has detected fault type NvidiaGPUUnhealthy on this node and is unhealthy. Repair action: Replace.)"
description: |
Verdict: Monitor — first attempt
Summary:
HMA flagged instance i-0abcdef1234567890 in instance group "worker2" as unhealthy (NvidiaGPUUnhealthy) and requested a Replace, and HyperPod has already started the first replacement attempt. The likely cause is a transient or isolated GPU fault on that single node, which HyperPod's automatic recovery is equipped to handle on its own. No operator action is needed right now — the replacement is expected to reach InService within 20-30 min; the next scheduled audit will re-check and emit a Resolved closure if it succeeds.
What HyperPod is doing right now:
HMA has flagged i-0abcdef1234567890 as unhealthy (NvidiaGPUUnhealthy) and requested Replace. HyperPod has started the replacement and this is the first attempt in the chain. Expected wall-clock: 20-30 min for the new instance to reach InService.
Timeline (UTC):
2026-07-08T18:45:12Z aws.sagemaker Cluster Node Health Event Instance i-0abcdef1234567890 unhealthy: NvidiaGPUUnhealthy
2026-07-08T18:45:15Z aws.sagemaker Cluster Event Info Instance deletion is starting as part of instance replacement
Most recent event at:
2026-07-08T18:45:15Z
Next re-check:
2026-07-08T19:15:15Z
Example D — Suppress (audit found nothing):
title: "Triage verdict: Suppress — periodic audit, no open incidents"
description: |
Verdict: Suppress — periodic audit, no open incidents
What HyperPod is doing right now:
Scheduled audit at 2026-07-08T19:00Z; scanned the last 4 hours of cluster events. No fault events, no open Monitor chains, cluster status InService, all instance groups at target. Nothing to investigate.
Anti-example — do NOT do this
If your first symptom looks like this, downstream automation is broken:
title: "worker1 lifecycle script execution failures across multiple nodes on my-cluster" ← WRONG: missing "Triage verdict:" prefix
description: "HyperPod cluster my-cluster emitted coordinated lifecycle-script execution failures..."
The content is fine as a second symptom record. But the FIRST symptom must be the verdict.
For Suppress verdict: still emit the verdict symptom (so the
operator can audit what was suppressed and why), but skip per-resource
symptoms and findings — there's nothing to root-cause.
For Monitor verdicts: the verdict symptom's description tells
the human "no action needed; HyperPod is recovering; expected
completion by HH:MM UTC. You will be notified again only if the
situation changes." This is the key UX improvement — silence is bad;
"we're watching and here's why we're not alarming you" is good.
For Escalate verdicts: include explicit operator-runnable
remediation in the verdict symptom's description, under a
Recommended actions (operator runs these): heading. The agent
cannot execute these; the operator must.
Confidence annotations: for every material claim in the verdict
description, prefix with one of [direct] (observed via API/log
output the agent itself ran), [proxy] (inferred from HMA
classification or a correlated source), or [unverified] (would need
on-node SSM or AWS-internal data the agent can't reach). This
replaces the separate "Confidence" section.
[direct] requires direct observation, not inference. A claim
like "all three replacements landed on the same physical host" is
not [direct] even when three identical fault signatures are
observed — the customer surface (NodeId, InstanceId, ENI, K8s node
name) does not expose physical hardware identity. Such claims are
[unverified] at best. Mislabeling inference as [direct] is a
serious failure mode because operators trust the annotation to mean
"the agent saw this in the data."
Hypothesis discipline for recurring-pattern verdicts
(rules 6–8). When the verdict is one of Escalate — recurring fault pattern, Escalate — fleet-wide instability, or
Escalate — instance-group instability, the verdict description
MUST enumerate at least two competing hypotheses for the root
cause, each labeled [unverified] or [proxy], and each paired with
a discriminating operator action. Do not commit to a single root
cause without [direct] evidence. The required hypothesis classes
are:
- Software / workload — the workload running on the IG triggers the fault on whatever GPU it lands on (NCCL pattern, driver / CUDA version, application code path). Discriminator: change the workload, or move the IG to a different node and see if the fault follows.
- Infrastructure path — an EFA fabric path, leaf switch, or shared network resource surfaces as GPU-level errors on workloads that hit it. Discriminator: move the IG to a different subnet / AZ.
- Statistical hardware — a bad batch of the same SKU is over- represented in the capacity pool. Discriminator: open an AWS Support case with the fault signature requesting hardware exclusion, or wait + retry later from a different time/pool.
The verdict should NOT include "every replacement is landing on the same physical hardware" as a stated cause — that's the explanation operators are conditioned to expect from on-prem clusters, but on HyperPod the EC2 instance is owned by the service account, the underlying physical host is not exposed on the customer surface, and EC2 placement is non-deterministic per replacement. Read the "Recurring fault signature does NOT prove physical-host affinity" section in references/hyperpod-mental-model.md before authoring this part of the verdict.
The GPU UUID check is the only way to confirm or refute physical-host affinity, and it requires SSM (operator-only). The verdict's "Recommended actions" section MUST include this check as an explicit operator step whenever a recurring-pattern verdict is emitted (rules 6–8). The wording should be:
N. Verify or refute "same physical GPU" by capturing GPU UUIDs.
The agent cannot run this check (requires SSM, which is outside
the DevOps Agent permission guardrail). For each affected
instance ID, run:
aws ssm start-session \
--target sagemaker-cluster:<cluster-id>_<group>-<instance-id> \
--document-name AWS-StartNonInteractiveCommand \
--parameters '{"command":["nvidia-smi -L"]}'
Compare the UUID strings across the affected instances. If they
match, the same physical GPU is being recycled — that elevates
the "statistical hardware" hypothesis to [direct] evidence and
strengthens the case for an AWS Support exclusion request. If
they differ, "same physical hardware" is RULED OUT and the
investigation should pivot to the software/workload and
infrastructure-path hypotheses instead.
Replace <cluster-id>, <group>, <instance-id> with the actual
values from the affected instances in the timeline. List each
instance separately so the operator can run the commands in parallel.
Inputs the skill expects from the trigger
The webhook payload built by the bridge Lambda carries these fields. Per the
flattening note in Phase 1 step 8, read them from the task description at
runtime — the data.metadata.* / data.originalEvent.detail.* paths below
name the logical source:
data.metadata.clusterName— HyperPod cluster name (required)data.metadata.detailType—Cluster State Change/Cluster Node Health Event/Cluster Eventdata.originalEvent.detail.InstanceId— affected instance (for node health events)data.originalEvent.detail.EventDetails.InstanceGroupName/InstanceId(for cluster events)
If the cluster name is missing, abort with "skill cannot run without a HyperPod cluster name — check the webhook bridge's payload mapping."
What this skill does NOT do
- It does not SSH or run SSM on nodes. The DevOps Agent permission
guardrail blocks
ssm:StartSession/ssm:SendCommand. When on-node evidence is needed (Xid indmesg, DCGM counters, kubelet journal), the skill emits a recommendation that includes the exact SSM command for the operator to run, never executes it. - It does not replace nodes.
batch-replace-cluster-nodesis a state-changing call; the skill recommends it but never invokes it. - It does not modify Slurm or EKS state. No
scontrol update, nokubectl cordon, no label edits.
Tools used (in-guardrail)
sagemaker:DescribeCluster,ListClusterNodes,DescribeClusterNode,ListClusterEventslogs:FilterLogEvents,logs:DescribeLogStreamson/aws/sagemaker/Clusters/*eks:DescribeCluster(read-only) andkubectl get/describe(via theAWS::EKS::AccessEntrycreated by the CloudFormation stack)cloudwatch:GetMetricDataonClusterAgentandSagemakerHealthMonitoringAgentnamespaces — useful for time-series confirmation when individual log events are sparse
More skills from the awsome-distributed-ai repository
View all 3 skillsMore from AWS Labs
View publisheragentcore-investigation
investigate Bedrock AgentCore runtime sessions
mcp
Jul 12AWSDebuggingLogsObservabilityamazon aurora dsql
build applications with Aurora DSQL
mcp
Sep 2AuroraAWSDatabaseServerless +1aurora dsql
build applications with Aurora DSQL
mcp
Sep 2AWSDatabaseServerlessSQLaws dsql
build applications with Aurora DSQL
mcp
Sep 2AWSDatabaseMigrationServerless +1distributed postgres
build applications with Aurora DSQL
mcp
Sep 2AWSDatabasePostgreSQLServerless +1distributed sql
build applications with Aurora DSQL
mcp
Sep 2AWSDatabaseServerlessSQL