Personalize this lesson
Adapt explanations and teaching visuals to your background and preferred voice.
At 14:03:17 UTC, request r-7812 asks replica summarizer-7f9d6 for another 1.50 GiB. The allocation fails. Node gpu-a17 still answers health checks, peer replicas still serve traffic, and the alert says only CUDA out of memory.
The first receipt is more specific:
1node=gpu-a17 pod=summarizer-7f9d6 gpu=GPU-7f3
2capacity=40.00 GiB device_free=0.62 GiB
3pytorch_allocated=30.10 GiB pytorch_reserved=36.70 GiB
4inactive_split_blocks=6.10 GiB largest_inactive_split_block=0.88 GiB
5allocation_request=1.50 GiB
6nvml_device_used=39.38 GiB new_xids=[] new_uncorrectable_ecc=0There is no safe universal command after this receipt. A request retry, process restart, device quarantine, and node drain act at different scopes. Choose one only after evidence identifies the smallest broken scope.
Why is CUDA out of memory insufficient evidence for draining gpu-a17?
Answer
The error names a failed allocation in one process. It doesn't yet distinguish live application demand, allocator fragmentation, memory held outside PyTorch, a stale process, or a device fault. Draining the node would evict unrelated workloads before that scope is known.
Build an incident receipt before changing state
An incident receipt is a time-aligned record of request, process, device, node, and cluster evidence. Capture it while the failed process still exists. PyTorch can record allocator history and dump a snapshot for memory_viz; its snapshot includes allocator state and OOM events, but only for memory visible to the PyTorch allocator.[1]
For r-7812, preservation comes before restart:
- Remove the replica from application readiness so new work stops arriving.
- Record request ID, pod UID, container restart count, PID, node, GPU UUID, model revision, allocator backend and configuration, CUDA runtime, driver, and timestamps.
- Dump the PyTorch memory snapshot if the process still responds.
- Record device-wide memory, process accounting, ECC counters, clocks, power, temperature, Xid events, and device-plugin health.
- Copy logs and artifacts to incident storage with access controls.
- Only then choose containment and repair scope.
This order matters. A process exit destroys allocator history. A device reset can destroy volatile fault context. A node reboot can erase local logs. Preservation is an operational gate, not permission to collect another tenant's prompts, model inputs, or secrets.

The receipt separates several memory quantities:
| View | Exact value | What it can support | What it can't prove |
|---|---|---|---|
| PyTorch allocated | 30.10 GiB | Live tensor blocks known to allocator | Which tensor is semantically necessary |
| PyTorch reserved | 36.70 GiB | Segments allocator still holds | Contiguous availability for a 1.50 GiB request |
| Reserved minus allocated | 6.60 GiB | Allocator accounting gap to reconcile in snapshot | Bytes currently inactive and reusable |
| Inactive split blocks | 6.10 GiB | Free fragments trapped in partly active segments at OOM | One reusable block of sufficient size |
| Largest inactive split block | 0.88 GiB | Largest observed fragment in snapshot | Future allocation layout after workload changes |
| NVML device used | 39.38 GiB | Whole-device usage across CUDA users | Ownership without process or subsystem attribution |
| NVML used minus reserved | 2.68 GiB | Approximate use outside PyTorch reserve | Whether NCCL, another process, or driver state owns it |
reserved - allocated is an accounting gap, not a reusable-byte counter. Stronger evidence comes from the snapshot: 6.10 GiB is inactive across split blocks trapped in partly active segments, but no such block exceeds 0.88 GiB, so those fragments can't satisfy a 1.50 GiB request. Meanwhile, the 2.68 GiB device-wide delta is worth attribution, but it isn't large enough to explain the whole failure. Together, these numbers support a working classification of allocator fragmentation under high application demand, with a small outside-PyTorch component. That classification remains a hypothesis until segment and block history confirm why the layout formed.
Keep memory layers separate
GPU memory pressure has four common software layers. Combining them into one “leak” diagnosis produces the wrong repair.
| Layer | Strong evidence | Typical containment | Repair question |
|---|---|---|---|
| Application allocations | Allocated memory tracks batch, sequence, KV cache, workspace, or retained tensors | Reject, shed, or reroute bounded work | Which live state exceeded the admission envelope? |
| Allocator reservation | Reserved stays above allocated; snapshot shows inactive blocks and split segments | Remove replica, preserve snapshot, restart process if policy allows | Is layout fragmented, or is cached reserve expected? |
| Non-PyTorch allocation | NVML/device use materially exceeds PyTorch reserve | Attribute libraries and processes before tuning allocator | Does NCCL, CUDA graph state, custom code, or another framework own it? |
| Process leak | Device process list shows stale or duplicate owners after expected teardown | Stop admission to affected device; process owner cleans up within policy | Which lifecycle failed to release the process or context? |
PyTorch explicitly warns that its memory profiler can't see allocations made directly through CUDA APIs. NCCL is a documented example. torch.cuda.device_memory_used() provides a device-wide cross-check, and NVML exposes device and process telemetry for independent attribution.[1][2]
Don't treat reserved - allocated as wasted memory by definition. The caching allocator holds segments so later allocations avoid expensive device synchronization. Fragmentation becomes plausible when an OOM snapshot shows enough inactive split bytes but no fragment large enough for the failed request. PyTorch allocator configuration can change split behavior, yet tuning it before identifying the workload shape can hide an admission-control bug.[3]
This receipt assumes PyTorch's native caching allocator. With backend:cudaMallocAsync, some allocator statistics have backend-specific or undefined meaning, and native split-block tuning options don't apply. Record the backend and use its documented evidence instead of copying this inference unchanged.[3]
Record a finite allocator history
Enable history before the incident window when overhead and privacy policy permit:
1import torch
2
3torch.cuda.memory._record_memory_history(
4 enabled="all",
5 context="all",
6 stacks="python",
7 max_entries=100_000,
8)
9
10torch.cuda.memory._dump_snapshot("r-7812-memory.pickle")The initial _ marks these as private APIs, so pin and validate the PyTorch version used by your fleet. Open the pickle in the local memory_viz page. PyTorch says the browser processes the file locally without uploading it.[1]
History is a ring buffer, not an infinite flight recorder. Once max_entries fills, the oldest entries are replaced. Large histories can produce multi-gigabyte snapshots. Set a bounded window, record the start time and entry limit in the receipt, and trigger a dump near the failure. A snapshot with no OOM event may mean capture began too late or history wrapped, not that the alert was false.
A snapshot shows 6.10 GiB across inactive split blocks, but NVML reports 2.68 GiB more device use than PyTorch reserved. What should you conclude?
Answer
Two hypotheses remain. Allocator layout may block the 1.50 GiB request, while another CUDA user owns about 2.68 GiB. Inspect snapshot blocks and attribute device processes or libraries. Neither number alone proves a leak.
Classify beyond memory
An OOM can be the visible end of a different failure. A collective stalls, a peer disappears, requests accumulate, and healthy ranks hold buffers until an allocation fails. Build a parallel device and node view.
| Fault class | Evidence to correlate | Why symptom can mislead | Initial scope |
|---|---|---|---|
| ECC or device fault | New uncorrectable ECC, GPU UUID, kernel timestamp, device health | OOM or timeout can follow lost device access | Device or node quarantine pending memory-error guidance |
| Xid event | Exact code, GPU UUID, kernel timestamp, catalog action, nearby events | Same code family can reflect application, NVIDIA software, or hardware | Scope named by event-specific catalog and surrounding evidence |
| PCIe or NVLink fault | DCGM PCIe, NVLink, nvbandwidth, or NCCL test; link counters; peer topology | One slow or failed rank appears as a distributed timeout | Affected GPU topology, then node if isolation is unavailable |
| Clock limiting | Exact clock-event reasons, configured limits, temperature, power, clocks, workload phase | An expected power cap and a thermal fault can both reduce clocks | Replica first; platform path if unexpected under controlled load |
| Driver or runtime fault | Driver/runtime versions, module logs, NVML initialization, DCGM software test | Application sees initialization or unknown CUDA error | Stop admission; platform owner validates stack contract |
| CUDA process-state fault | Exact CUDA error, first failing operation, synchronization point | Asynchronous execution reports an earlier failure later | Process, unless device evidence expands scope |
NVML clock-event reasons name the active limiter, not its root cause. GPU idle and configured application clocks can be expected. Software power cap means clocks are respecting a configured power limit, while thermal slowdown points to temperature protection. Correlate reason, workload phase, configured limits, measured power, and temperature before calling it a fault.[2]
An Xid entry is not a root cause label. NVIDIA states that Xid events can arise from hardware, NVIDIA software, or an application. Recovery actions differ by event, and newer catalog guidance can attach a specific recovery action to the reported event.[4][5]
The GPU Operator device plugin listens for health events and can report a GPU unhealthy. Kubernetes then lowers allocatable device count, but pods already assigned to that device remain assigned and may fail or crash-loop.[6][7] Application readiness and workload containment still need an explicit controller or operator action. Device-plugin health alone is not retrospective eviction.
Respect asynchronous error reporting
CUDA work is asynchronous. An API call can enqueue work and return before the device executes it; an error may surface at a later synchronization boundary. Capture the first observed failure, stream, request, kernel, and correlation timestamp. CUDA_LAUNCH_BLOCKING=1 can help reproduce ordering in a controlled debug run, but it changes execution and should not become a fleet-wide production setting.[8]
Some CUDA runtime errors explicitly leave the process in an inconsistent state. NVIDIA documents illegal address, kernel-execution launch timeout, launch failure, and several hardware-stack errors as requiring process termination and relaunch. Retrying another request in that same process is unsafe. This rule doesn't apply to every CUDA error, so preserve the exact error enum and originating API operation rather than matching the word “CUDA.”[9]
Contain at smallest broken scope
Use a ladder. Move outward only when evidence says the smaller boundary can't protect users or hardware.
| Action | Appropriate evidence | What must be true first | Why a broader action is risky |
|---|---|---|---|
| Retry request elsewhere | Idempotent or deduplicated request; healthy alternate replica; failed process still trustworthy or excluded | Retry budget and side-effect contract permit replay | Blind retries amplify memory pressure and duplicate effects |
| Remove replica from readiness | One worker is unhealthy or near its memory envelope | Gateway and scheduler honor readiness quickly | Traffic can keep reaching a process that should be preserved |
| Restart process | Allocator fragmentation, process leak owned by replica, or fatal CUDA process-state error | Receipt and snapshot preserved; restart budget permits it | Restart erases volatile evidence and can loop on a bad device |
| Quarantine GPU | New uncorrectable ECC, isolation-requiring Xid action, fabric finding, or repeated device-scoped evidence | Device identity and cluster owner are known | Shared node may lose capacity; peer topology can be affected |
| Cordon or drain node | Node-wide driver, fabric, power, cooling, or repair requirement | Platform owner authorizes scope; workload disruption reviewed | Drain can evict unrelated tenants and destroy local ephemeral data |

For r-7812, the request is safe to retry on a different healthy replica only if its operation is idempotent or deduplicated. The failed replica leaves readiness. Its snapshot and receipt are copied. Because evidence points to allocator layout and the CUDA error is OOM rather than an error documented as process-corrupting, policy may allow graceful process teardown and relaunch. No ECC, Xid, link, stack, thermal, or power signal currently supports a node drain.
An illegal-address error appears after a request, but the next health check passes. May the worker serve another request?
Answer
No. CUDA documents illegal address as leaving the process in an inconsistent state. Remove the worker from readiness, preserve evidence, terminate and relaunch the process, then apply re-admission gates. A passing application probe doesn't repair CUDA process state.
Run diagnostics with an explicit question
DCGM diagnostics are active tests, not repair commands. NVIDIA describes four levels:
| DCGM level | Intended operational use | Representative additions | Scheduling rule |
|---|---|---|---|
| Level 1 | Fast readiness signal before work | Deployment and software checks | May fit an admission gate after local validation |
| Level 2 | Failure epilogue or brief health check | PCIe, memory, memory bandwidth, NVLink | Run outside user traffic when tests can contend |
| Level 3 | Admin post-mortem | Diagnostic stress, targeted power/stress, nvbandwidth, NCCL tests | Quarantined or idle hardware only |
| Level 4 | Extended post-mortem | Longer memory and pulse-style coverage | Maintenance window with platform ownership |
Exact tests depend on DCGM version, hardware, driver, and installed plugins. A named test may be unavailable. In plugin output, Pass applies only to checks that ran, Fail may be a device finding or a test-setup error, and Skip gives no health verdict. Treating Skip as Pass turns missing coverage into false readiness.[10][11]
Ask one question per run:
- “Can this GPU enumerate and pass deployment checks?” points toward Level 1.
- “Did memory or PCIe fail around this incident?” points toward selected Level 2 tests.
- “Can the quarantined tensor-parallel topology sustain fabric traffic?” points toward nvbandwidth or NCCL plugins at an admin level.
- “Does the device reproduce a memory or power failure under burn-in?” points toward selected stress tests on idle hardware.
Record command, DCGM version, selected entities, test names, parameters, wall time, plugin availability, raw result, and exit status. DCGM doesn't claim comprehensive diagnosis or automatic repair. A passing suite narrows evidence; it doesn't erase an Xid or guarantee future health.[10]
Make ownership part of automation
A multi-tenant GPU node has at least four owners:
| Boundary | Owner decision | Automation may do safely | Escalation trigger |
|---|---|---|---|
| Request | Serving team | Bound retries, deduplicate effects, route away from unhealthy replica | Side-effect ambiguity or exhausted retry budget |
| Process or pod | Workload owner | Toggle readiness, preserve app receipt, restart within disruption budget | Repeated crash or cross-process memory owner |
| GPU device | Platform GPU controller | Mark device unavailable, attach evidence, stop new allocation | New uncorrectable ECC, isolation-requiring Xid action, fabric finding, or repeated burn-in failure |
| Node and rack | Cluster or hardware owner | Cordon, drain, repair, reboot, or replace under policy | Shared driver, cooling, power, PCIe, or topology fault |
Error-aware automation consumes structured facts, not log substrings alone. A useful event includes GPU UUID, node UID, workload UID, process identity, exact CUDA or Xid code, source, first and last timestamps, recurrence count, and evidence links. Its transition should be idempotent: one device fault creates one quarantine record, not a storm of repeated drains.
Never let a workload controller reset a shared GPU or drain a node by inference. NVIDIA's own GPU Operator upgrade workflow separates cordon, workload termination, validation, and uncordon. Its documentation warns that full drain can evict unrelated pods and can permanently remove emptyDir data.[12] Production remediation needs equivalent scope checks and authority.
Preserved evidence also needs tenancy controls:
- Snapshot allocator metadata, not prompt bodies, unless incident policy explicitly requires content.
- Restrict process lists and command lines when they expose another tenant's paths or arguments.
- Attach retention, encryption, and access policy to receipts.
- Store stable IDs and hashes so responders can correlate without copying sensitive payloads.
Repair path for gpu-a17
The repair is intentionally narrow:
- Contain: Set
summarizer-7f9d6not ready. Stop scheduler admission to its process. - Preserve: Save snapshot, application logs, exact versions, NVML memory and process view, ECC counters, clocks, power, temperature, and kernel Xid window.
- Classify: Confirm
30.10 GiBallocated,36.70 GiBreserved,6.10 GiBinactive across split blocks,0.88 GiBlargest fragment, and39.38 GiBdevice-wide use. Attribute the2.68 GiBdelta. - Repair process: Finish or reject in-flight work according to the request contract. Relaunch the serving process under its normal supervisor.
- Prove baseline: Load the same model revision and configuration. Confirm expected idle memory and one process owner.
- Burn in: Replay a bounded canary workload that includes the 1.50 GiB allocation shape. Watch allocator headroom, device-wide delta, Xid/ECC, link status, clocks, temperature, power, and latency.
- Re-admit: Restore only a small traffic fraction. Expand after a fixed observation window with no gate failure.
If fragmentation returns under the same bounded workload, process restart was containment, not repair. Revisit admission envelopes, KV-cache sizing, batch and sequence caps, workspace lifetime, CUDA graph pools, and allocator configuration. If outside-PyTorch use grows, attribute NCCL, custom CUDA, peer processes, or context lifecycle before changing allocator settings.
Re-admission gates
Use explicit pass/fail gates rather than “looks stable”:
| Gate | Example evidence | Failure action |
|---|---|---|
| Identity | Expected GPU UUID, driver/runtime pair, model revision, one intended process owner | Hold admission and resolve inventory mismatch |
| Idle baseline | Device-wide and allocator memory inside known band | Investigate stale process, context, or changed model footprint |
| Functional probe | Deterministic canary returns expected shape and status | Keep replica out of readiness |
| Memory envelope | Peak allocated, reserved, largest reusable block, and outside delta remain within policy | Adjust workload or allocator plan before retry |
| Device health | No new disqualifying Xid/ECC; required DCGM checks ran and passed | Keep quarantined; follow event-specific owner runbook |
| Fabric health | Required peer path and selected fabric tests pass on affected topology | Keep topology unavailable |
| Thermal and power | Stable clocks, temperature, and power during bounded load | Escalate cooling, power, or hardware path |
| Soak | Fixed request count or duration completes without recurrence | Extend quarantine and compare receipts |
Burn-in must resemble the failed allocation and communication shape without carrying production traffic. “One inference succeeded” misses fragmentation that emerges after many allocation cycles and fabric faults that need multi-GPU load. Define duration, load, pass thresholds, and abort conditions before starting.
A Level 2 DCGM run reports PCIe Pass and nvbandwidth Skip. Can automation re-admit a topology whose incident involved NVLink timeouts?
Answer
No. PCIe Pass covers only the check that ran. Skip is no health verdict, and the missing fabric coverage is relevant to the incident. Keep the topology quarantined until the required test can run or an authorized owner chooses another evidence path.
Downloadable receipt analyzer
The fixture contains eight synthetic receipts spanning allocator fragmentation, application capacity, outside-PyTorch ownership, device fault, fabric fault, clock limiting, process-corrupting CUDA error, and driver/runtime failure.
The analyzer doesn't reset, kill, drain, or delete anything. It sorts evidence into a first hypothesis and a scoped next step. Its memory rows assume PyTorch's native allocator. new_xids and new_uncorrectable_ecc cover only the incident window. clock_event_reasons contains only limiters that were unexpected for the workload phase and correlated with the regression. cuda_error is normalized only after recording the exact enum and originating API operation; launch_timeout here means the process-invalidating kernel-execution error. dcgm_findings contains defect classes after an operator or version-pinned parser has inspected each Fail code and message; raw Fail and Skip results don't qualify. The fixture's 4.0 GiB outside-PyTorch threshold is illustrative, not universal. Production automation still needs workload-specific bands, platform policy, event-specific guidance, and ownership checks.
1import json
2from pathlib import Path
3
4FIXTURE = Path("assets/gpu_failure_receipts.json")
5PROCESS_INVALIDATING_ERRORS = {
6 "assert",
7 "hardware_stack_error",
8 "illegal_address",
9 "illegal_instruction",
10 "invalid_address_space",
11 "invalid_pc",
12 "launch_failure",
13 "launch_timeout",
14 "misaligned_address",
15 "mps_client_terminated",
16 "tensor_memory_leak",
17}
18STACK_ERRORS = {"insufficient_driver", "initialization_error", "system_driver_mismatch"}
19OUTSIDE_PYTORCH_ALERT_GIB = 4.0
20
21def classify(row: dict) -> tuple[str, str]:
22 if row["new_uncorrectable_ecc"]:
23 return "device-memory-fault", "quarantine device; follow memory-error guidance"
24 if {"pcie", "nvlink", "nvbandwidth"} & set(row["dcgm_findings"]):
25 return "fabric-fault", "quarantine affected topology; run scoped fabric diagnostics"
26 if row["new_xids"]:
27 return "xid-event", "map exact Xid to catalog action before choosing scope"
28 if row["cuda_error"] in STACK_ERRORS or "software" in row["dcgm_findings"]:
29 return "driver-runtime", "hold admission; platform owner checks driver/runtime contract"
30 if row["cuda_error"] in PROCESS_INVALIDATING_ERRORS:
31 return "process-corrupted", "stop retries in this process; preserve receipt and relaunch"
32 if row["clock_event_reasons"]:
33 return "clock-limited", "remove from readiness; compare reason, limit, power, and temperature"
34
35 outside_pytorch = max(
36 0.0, row["nvml_device_used_gib"] - row["pytorch_reserved_gib"]
37 )
38 if outside_pytorch >= OUTSIDE_PYTORCH_ALERT_GIB or row["stale_processes"]:
39 return "outside-pytorch", "attribute device-wide delta before changing allocator settings"
40 if (
41 row["oom_request_gib"] > 0
42 and row["inactive_split_block_total_gib"] >= row["oom_request_gib"]
43 and row["largest_inactive_split_block_gib"] < row["oom_request_gib"]
44 ):
45 return "allocator-fragmentation", "remove replica; inspect snapshot before process restart"
46 if row["oom_request_gib"] > 0:
47 return "application-capacity", "reduce live demand or admission; prove headroom before retry"
48 return "unclassified", "preserve receipt; collect evidence for the missing failure class"
49
50rows = json.loads(FIXTURE.read_text(encoding="utf-8"))
51for row in rows:
52 label, next_step = classify(row)
53 outside = max(0.0, row["nvml_device_used_gib"] - row["pytorch_reserved_gib"])
54 print(
55 f'{row["receipt_id"]}: {label:23} '
56 f'outside={outside:5.2f} GiB | {next_step}'
57 )1r-7812: allocator-fragmentation outside= 2.68 GiB | remove replica; inspect snapshot before process restart
2r-7813: application-capacity outside= 0.20 GiB | reduce live demand or admission; prove headroom before retry
3r-7814: outside-pytorch outside=16.40 GiB | attribute device-wide delta before changing allocator settings
4r-7815: device-memory-fault outside= 1.10 GiB | quarantine device; follow memory-error guidance
5r-7816: fabric-fault outside= 1.00 GiB | quarantine affected topology; run scoped fabric diagnostics
6r-7817: clock-limited outside= 1.00 GiB | remove from readiness; compare reason, limit, power, and temperature
7r-7818: process-corrupted outside= 0.70 GiB | stop retries in this process; preserve receipt and relaunch
8r-7819: driver-runtime outside= 0.00 GiB | hold admission; platform owner checks driver/runtime contractNotice the precedence. A new uncorrectable ECC event and decoded fabric finding outrank an OOM-derived memory guess. An Xid takes its scope from the event-specific catalog instead of being called a hardware fault by default. Process-invalidating CUDA errors outrank allocator arithmetic. Only then does the analyzer compare device-wide use, allocator reserve, inactive split blocks, and largest fragment. This is triage, not autonomous remediation.
Mastery rubric
You can operate this incident when you can:
- Reconcile allocated, reserved, inactive, largest-block, and device-wide memory without calling every delta a leak.
- Explain what a PyTorch snapshot omits and how finite history can erase the event you need.
- Preserve request, process, device, node, and cluster evidence before destructive state changes.
- Separate ECC/Xid, fabric, thermal/power, stack, and process-state evidence.
- Choose retry, process restart, device quarantine, or node drain from explicit scope and authority.
- Interpret DCGM level, selected plugin, Pass, Fail, and Skip without inflating coverage.
- Define readiness and burn-in gates tied to the original failure shape.
Score yourself on one unseen receipt:
| Level | Evidence | Decision quality |
|---|---|---|
| 0: symptom only | Repeats alert text | Chooses action from OOM or Xid keyword |
| 1: partial receipt | Captures one memory or device view | Names hypotheses but can't bound scope |
| 2: scoped response | Correlates process, device, node, and time | Contains smallest supported boundary and preserves evidence |
| 3: safe recovery | Adds owner, test coverage, and thresholds | Re-admits through failure-shaped burn-in with rollback criteria |
Pitfalls that widen blast radius
- Calling cached reserve a leak. Reserved memory is allocator policy; snapshot block history decides whether fragmentation is plausible.
- Reading NVML delta as proof of another process. Direct CUDA, NCCL, graphs, libraries, and peer processes all need attribution.
- Retrying in a corrupted CUDA process. Exact runtime error semantics decide whether relaunch is mandatory.
- Treating every Xid alike. Xid is a structured clue; catalog guidance and surrounding evidence decide scope.
- Treating Skip as Pass. Missing plugin coverage can't satisfy a readiness gate.
- Restarting before preservation. The restart may clear the only allocator or kernel evidence.
- Draining for convenience. Shared-node eviction and ephemeral-data loss require platform authority and review.
- Re-admitting after one probe. Short probes miss allocation-cycle fragmentation, topology load, and thermal soak failures.