The Zombie Process Paradox: Debugging GPU Memory Retention After Process Death

Introduction

In the complex ecosystem of large-scale machine learning deployments, few debugging scenarios are as frustrating as the "hung server" problem. When an AI inference server like SGLang freezes during initialization on an 8-GPU machine, the natural instinct is to kill it and restart. But what happens when the processes refuse to die cleanly, and the GPU memory remains stubbornly allocated? This article examines a single message ([msg 3269]) from an opencode coding session where an AI assistant confronts exactly this situation — zombie processes clinging to existence while the assistant attempts to diagnose why GPU memory hasn't been released. The message captures a pivotal debugging moment, revealing the assistant's reasoning about Linux process states, CUDA memory management, and the assumptions that guide troubleshooting under pressure.

Context: The Hung Server

The narrative leading up to [msg 3269] is one of escalating frustration. The assistant had launched an SGLang inference server for the Kimi-K2.5 model using the flashinfer attention backend, a choice that would prove catastrophic. The server loaded model weights successfully — all 64 safetensor checkpoint shards completed — but then stalled during initialization. Hours passed. The server's port (8000) never opened for business. The scheduler worker processes (TP0 through TP7) were alive but trapped in a busy-wait loop, each thread writing a single byte to an event pipe approximately once per second, making no real progress.

The assistant's initial diagnosis pointed to a CUDA graph capture hang, likely triggered by the flashinfer MLA attention backend on the SM120 architecture. After exhausting lighter debugging options — checking dmesg for kernel errors, examining process file descriptors, tracing system calls — the assistant made the call to kill the server and restart with a known-working configuration (triton attention plus NCCL tuning variables). This was the pragmatic choice: cut losses and try a different approach rather than continue debugging a frozen initialization.

But the kill command didn't fully work. The parent Python process (PID 72169) was terminated, but its eight scheduler children became zombie processes — dead but not yet reaped by their parent. Worse, nvidia-smi continued to report approximately 76 GB of memory allocated on each of the eight GPUs, as if the model weights were still loaded.

The Subject Message: Reasoning Through Zombie Processes

Message [msg 3269] opens with the assistant's analysis of this predicament:

Zombie processes. Their parent (72169) was killed but they're still zombie. Need to kill the parent's parent or wait for init to reap them. But the real issue is GPU memory — the zombie processes shouldn't hold GPU memory. Let me check what's holding the GPU.

This is a concise but revealing piece of reasoning. The assistant immediately identifies the process state correctly: the scheduler workers are zombies (marked Zl in ps output from the previous message). A zombie process is one that has completed execution but still has an entry in the process table because its parent hasn't called wait() to read its exit status. Normally, the parent process reaps its children. But in this case, the parent (the Python launcher) was killed first, leaving the children orphaned. When a parent dies before its children, the children are reparented to init (PID 1), which should eventually reap them. However, the zombie state persists until init gets around to it, or until someone kills the zombie's parent's parent — a complicated operation when dealing with process trees.

The assistant's key insight is the phrase: "the zombie processes shouldn't hold GPU memory." This is a critical assumption rooted in the standard Linux memory model. A zombie process has already released all its resources — memory, file descriptors, locks — because it has exited. The only thing remaining is the process table entry (the struct task_struct and its exit status). The zombie's address space has been freed. Therefore, any GPU memory allocated by that process through CUDA should have been released when the process exited.

Yet nvidia-smi showed otherwise. This contradiction — zombie processes that can't possibly hold memory, but memory that appears allocated — is the puzzle the assistant must solve.

The Debugging Decision: Using fuser

The assistant's next move is to run fuser -v /dev/nvidia* to identify which processes are holding the NVIDIA device files open. This is a clever diagnostic choice. The fuser command identifies processes using specified files or sockets. By checking /dev/nvidia0 through /dev/nvidia7 (the GPU device files) and /dev/nvidia-uvm (the Unified Virtual Memory device), the assistant hopes to find a process — any process — that still has an open handle to the GPUs.

The command output reveals something unexpected:

                     USER        PID ACCESS COMMAND
/dev/nvidia-caps:    root     kernel mount /dev/nvidia-caps
/dev/nvidia-uvm:     root     kernel mount /dev/nvidia-uvm
/dev/nvidia-uvm-tools:
                     root     kernel mount /dev/nvidia-uvm-tools
/dev/nvidia0:        root     kernel mount /dev/nvidia0
/dev/nvidia1:        root     kernel mount /dev/nvidia1
/dev/nvidia2:        root     kernel mount /dev/nvidia2
/dev/nvidia3:        root     kernel mount /dev/nvidia3
/dev/nvidia4:    ...

The output (truncated to 20 lines) shows only "kernel mount" entries — no user-space processes with PIDs. Every NVIDIA device file shows the kernel as the owner, not any application process. This is both informative and puzzling.

What the fuser Output Actually Means

The fuser output reveals that the NVIDIA device files are held by kernel mounts, not by user processes. This is actually normal behavior: the NVIDIA driver creates these device files as part of its kernel module initialization. The "kernel mount" entries indicate that the devices are registered with the kernel's device filesystem (devtmpfs or similar), not that a kernel thread is actively using them.

However, the absence of user-space PIDs in the fuser output is significant. It means that no running process has an open file handle to any NVIDIA device. This should mean that no process is holding GPU memory — yet nvidia-smi reports 76 GB allocated per GPU.

This creates a new mystery: if no process holds the GPU devices open, why is the memory still allocated? The assistant's assumption that "zombie processes shouldn't hold GPU memory" appears to be correct — the zombies aren't holding anything. So what is?

Assumptions and Their Validity

The assistant makes several assumptions in this message, each worth examining:

Assumption 1: Zombie processes cannot hold GPU memory. This is generally correct under Linux. When a process exits (becoming a zombie), its memory mappings are torn down, including CUDA allocations. The CUDA driver should receive cleanup callbacks and release GPU memory. However, there are edge cases: if the CUDA driver itself has a bug, or if GPU memory is allocated via a different mechanism (e.g., persistent kernel module allocations, or memory mapped via /dev/nvidia-uvm that isn't tied to process lifetime), memory could persist. The assumption is reasonable but not absolute.

Assumption 2: The GPU memory shown by nvidia-smi is real. This is correct — nvidia-smi queries the NVIDIA driver directly for memory usage. The 76 GB per GPU figure represents actual allocated memory, not a stale cache.

Assumption 3: fuser would show any process holding GPU memory. This is where the assumption may be incomplete. GPU memory can be allocated through CUDA without the allocating process holding an open file descriptor to the device file at the moment of inspection. The CUDA runtime opens /dev/nvidia* during initialization and may close the file descriptors after setup, while the memory allocations persist through kernel-level state. Additionally, CUDA IPC handles or cudaMalloc managed memory might not be visible via fuser.

Assumption 4: The zombie processes are the cause. The assistant correctly identifies that zombies shouldn't hold memory, but the framing "the real issue is GPU memory" suggests the assistant is trying to determine whether the zombies are somehow responsible. The fuser output confirms they are not.

Input Knowledge Required

To fully understand this message, one needs knowledge of:

  1. Linux process states: Understanding what a zombie process is (terminated but not reaped), how orphan processes are reparented to init, and what resources a zombie retains (only the process table entry, not memory).
  2. CUDA memory management: How GPU memory is allocated and freed, the relationship between process lifetime and CUDA context lifetime, and how nvidia-smi reports memory usage.
  3. The NVIDIA device filesystem: /dev/nvidia0 through /dev/nvidia7 represent individual GPUs, /dev/nvidia-uvm is the Unified Virtual Memory device, and /dev/nvidia-caps provides capabilities information. These are created by the NVIDIA kernel driver.
  4. The fuser command: How it works (checking which processes have open file handles), its limitations (it only checks file descriptors, not kernel-level state), and how to interpret its output.
  5. The SGLang architecture: SGLang uses multiple worker processes (TP0-TP7) for tensor parallelism across 8 GPUs. Each worker initializes its own CUDA context and loads model weights into its assigned GPU's memory.

Output Knowledge Created

This message produces several valuable pieces of information:

  1. Confirmation that no user-space process holds GPU device files: The fuser output shows only kernel mounts, meaning the zombie processes and any other user processes have released their file handles to the NVIDIA devices.
  2. A narrowed problem space: Since no process holds the device files, the GPU memory retention must be caused by something else — perhaps the NVIDIA driver's kernel module hasn't fully cleaned up, or there's a lingering CUDA context that wasn't properly destroyed.
  3. A diagnostic dead end: The fuser approach didn't identify the culprit, forcing the assistant to consider alternative explanations and next steps.

The Thinking Process

The assistant's reasoning in this message follows a clear logical chain:

  1. Observation: The scheduler processes are zombies (Zl state).
  2. Inference: Zombies can't hold GPU memory (they've already released all resources).
  3. Contradiction: But nvidia-smi shows GPU memory still allocated.
  4. Hypothesis: Something else must be holding the GPU memory.
  5. Test: Use fuser to check what processes have open NVIDIA device handles.
  6. Result: Only kernel mounts — no user processes.
  7. Implication: The GPU memory retention is not due to a running process holding device files open. This is sound diagnostic reasoning. The assistant correctly identifies the zombie state, correctly reasons about what zombies can and cannot hold, and selects an appropriate tool to investigate the discrepancy. The fuser output, while not immediately solving the problem, provides useful negative information: the culprit is not a process with an open device handle.

What Happens Next

The message ends with the fuser output truncated. The assistant will need to pursue other diagnostic avenues: checking for CUDA IPC handles, examining the NVIDIA driver's memory tracking, or simply rebooting the GPUs via nvidia-smi --gpu-reset or a driver reload. The key insight from this message — that no user process is holding the GPU devices — points toward a kernel-level or driver-level issue rather than a simple process cleanup problem.

Broader Significance

This message illustrates a common pattern in systems debugging: the moment when a straightforward assumption (kill the process, free the resources) fails, forcing deeper investigation. The assistant's response is methodical: acknowledge the anomaly, reason about why the assumption should hold, and test alternative explanations. The fuser command is well-chosen for the hypothesis being tested. Even though it doesn't immediately resolve the problem, it eliminates one possible cause and narrows the search space.

The message also highlights the gap between standard Linux process semantics and GPU-accelerated computing. In traditional computing, killing a process reliably frees its memory. In GPU computing, the interaction between user-space CUDA runtime libraries, kernel-mode NVIDIA drivers, and hardware state can create situations where memory appears allocated after process death. Debugging these situations requires understanding both the Linux process model and the GPU driver architecture — a combination of knowledge that makes GPU systems debugging particularly challenging.

Conclusion

Message [msg 3269] captures a moment of diagnostic clarity in a complex debugging session. The assistant correctly identifies zombie processes, reasons about their inability to hold GPU memory, and deploys fuser to investigate the discrepancy. While the fuser output doesn't solve the problem, it provides crucial negative evidence that guides subsequent debugging. The message is a testament to the value of systematic reasoning in systems troubleshooting — questioning assumptions, testing hypotheses, and letting the evidence guide the investigation rather than jumping to conclusions. For anyone debugging GPU-accelerated systems, this pattern of thinking — process states, resource lifetimes, and the tools to inspect them — is an essential skill.