Reading the Invisible Log: Debugging vLLM's CUDAGraph Compilation Stall via /proc
In the middle of an intensive profiling campaign targeting Kimi-K2.5 INT4 on eight NVIDIA RTX PRO 6000 Blackwell GPUs, the assistant encountered an unsettling plateau. GPU memory had climbed to 75.8 GiB per device and then stopped cold — holding steady for ten full minutes across ten consecutive one-minute polls. The model had clearly finished loading its weights, but something was preventing the server from becoming ready. This message ([msg 2439]) captures a moment of diagnostic pivot: the assistant formulates a hypothesis, then reaches into the Linux kernel's process filesystem to read the otherwise invisible stdout and stderr of a backgrounded process, confirming that the server is alive but trapped in the torch.compile/CUDAGraph compilation phase.
The Context: A Profiling Campaign Hits a Wall
The broader session was a deep-dive performance analysis of the Kimi-K2.5 INT4 model — a 1-trillion-parameter Mixture-of-Experts architecture deployed with tensor parallelism across 8 GPUs. Earlier benchmarks had revealed a surprising bottleneck: AllReduce communication consuming 51.5% of decode time (11.17ms per step), far exceeding the GEMM compute costs. To obtain definitive data, the assistant needed a torch.profiler capture, which required restarting vLLM with a --profiler-config flag.
In [msg 2428], the assistant launched a new vLLM instance manually:
nohup /root/ml-env/bin/python3 -m vllm.entrypoints.openai.api_server \
--model /shared/kimi-k2.5-int4 --tensor-parallel-size 8 \
--profiler-config "{\"profiler\": \"torch\", \"torch_profiler_dir\": \"/tmp/vllm_profile\"}" \
> /tmp/vllm_profiler_launch.log 2>&1 &
The output was redirected to a log file. But when checked in [msg 2431], that file was zero bytes — empty. The process appeared to be running (visible via ps aux), but the log was silent. Over the next ten minutes, the assistant polled GPU memory and worker counts, watching the memory climb from 4 MiB to 75.8 GiB and then stall. Eight worker processes (VLLM::Worker) had spawned. But the log file remained obstinately empty, and the server never became reachable.
The Hypothesis: CUDAGraph Compilation
The assistant's first move in [msg 2439] is to reason about what the stall means:
Memory is stuck at 75.8GB for 10 minutes — this could be the torch.compile / CUDAGraph compilation phase (happens after weight loading).
This is a critical piece of domain knowledge. In vLLM's V1 engine, model loading proceeds in distinct phases: first, the model weights are loaded and distributed across GPUs (which accounts for the climb to ~75.8 GiB of the final ~96.9 GiB footprint). Then, the engine enters a compilation phase where PyTorch's torch.compile and CUDAGraph capture optimize the model's execution graph. This phase is computationally intensive — it runs CUDA kernel autotuning, graph tracing, and optimization passes — and can take many minutes on large models. The fact that memory stopped climbing but didn't drop suggested the process was still alive and computing, not crashed.
But the assistant needed confirmation. The log file was empty, and the process was backgrounded with nohup. How could one see what the process was actually doing?
The Technique: Reading Process File Descriptors via /proc
The assistant deployed a clever Linux debugging technique:
ssh root@10.1.230.174 'ls -la /proc/$(pgrep -f "vllm.entrypoints" | head -1)/fd/1 2>/dev/null;
cat /proc/$(pgrep -f "vllm.entrypoints" | head -1)/fd/1 2>/dev/null | tail -30;
echo "---stderr---";
cat /proc/$(pgrep -f "vllm.entrypoints" | head -1)/fd/2 2>/dev/null | tail -30'
This command does three things. First, it resolves the PID of the vLLM entrypoint process using pgrep -f "vllm.entrypoints". Second, it checks the symbolic link at /proc/<pid>/fd/1 — file descriptor 1 is stdout — to confirm where output is actually going. Third, it reads directly from /proc/<pid>/fd/1 and /proc/<pid>/fd/2 (stderr) to capture whatever has been written, regardless of buffering or file system state.
The results were illuminating:
l-wx------ 1 root root 64 Feb 21 12:48 /proc/242751/fd/1 -> /tmp/vllm_profiler_launch.log
(Worker_TP2 pid=243561) <frozen importlib._bootstrap_external>:1297: FutureWarning:
The cuda.nvrtc module is deprecated and will be removed in a future release...
(Worker_TP3 pid=243562) <frozen importlib._bootstrap_external>:1297: FutureWarning:
The cuda.cudart module is deprecated and will be removed in a future release...
The stdout file descriptor confirmed that output was indeed going to the log file — but the file appeared empty because the C library's stdio buffering hadn't flushed the writes to disk. The stderr, however, was readable directly. It showed that worker processes TP2 and TP3 were alive and actively executing Python import code, triggering FutureWarnings about deprecated CUDA module paths (cuda.nvrtc and cuda.cudart moving to cuda.bindings.nvrtc).
What the Output Revealed
The stderr output told a clear story. The workers were running through initialization code — they hadn't crashed, they hadn't hung on a deadlock, they were simply still executing. The FutureWarnings about cuda.nvrtc and cuda.cudart deprecation are emitted during PyTorch's CUDA initialization path, which is triggered during the compilation phase when PyTorch compiles and optimizes CUDA kernels for the model's operations. The fact that these warnings appeared for workers TP2 and TP3 specifically (and presumably the other six workers were at similar stages) confirmed that all eight tensor-parallel workers were alive and progressing through the compilation pipeline.
This was reassuring but also frustrating: the compilation phase was simply taking a very long time. On an 8-GPU system running a 1T-parameter model with INT4 quantization and Marlin kernels, the torch.compile step has an enormous amount of code to process — every unique operation shape, every attention variant, every MoE routing pattern must be traced, compiled, and optimized. Ten minutes is long but not unreasonable for a model of this scale.
Assumptions and Their Validity
The assistant made several assumptions in this message, all of which were reasonable:
The stall is torch.compile/CUDAGraph compilation. This was correct. The memory plateau at 75.8 GiB (below the final 96.9 GiB) indicated that weight loading was complete but the engine hadn't moved to the serving phase. The stderr output showing CUDA-related FutureWarnings during import confirmed active computation. The only phase that fits between weight loading and serving readiness in vLLM's V1 engine is graph compilation and optimization.
The log file being empty means buffered output, not a crash. This was also correct. The /proc/<pid>/fd/1 symlink confirmed the file descriptor was open and pointing to the expected log file. An empty file with an active file descriptor means the writes are buffered in userspace (the C stdio buffer) and haven't been flushed to the kernel's page cache yet. This is normal behavior for long-running Python processes where output is line-buffered or fully buffered when redirected to a file.
The workers are alive and making progress. The stderr output confirmed this. The FutureWarnings are emitted during normal PyTorch initialization, not during error handling. If the process had deadlocked or crashed, the stderr would either be silent or contain traceback information.
Input Knowledge Required
To fully understand this message, one needs several pieces of background knowledge:
- vLLM's loading pipeline: The sequence of weight loading → worker spawning → graph compilation → server readiness. Understanding that
torch.compileand CUDAGraph capture happen after weights are distributed is essential to interpreting the 75.8 GiB plateau. - Linux /proc filesystem: The ability to read a process's file descriptors via
/proc/<pid>/fd/Nis a standard Linux debugging technique. File descriptor 1 is stdout, 2 is stderr. Reading them directly bypasses filesystem buffering issues. - C stdio buffering behavior: When stdout is redirected to a file (as with
> logfile 2>&1), the C library typically uses full buffering rather than line buffering. This means output may not appear in the file until the buffer fills (typically 4-8 KiB) or the process explicitly flushes. Reading via/proccan capture buffered content that hasn't been flushed to disk. - CUDA deprecation warnings: The
cuda.nvrtcandcuda.cudartmodule deprecation is a known transition in PyTorch 2.10+/CUDA 12.8 where the NVIDIA CUDA Python bindings were reorganized. These warnings are harmless but confirm that CUDA initialization code is executing. - The profiling campaign's goals: The assistant was trying to obtain
torch.profilertraces to quantify the AllReduce vs. GEMM breakdown. This required the--profiler-configflag, which in turn required a full server restart and reload of the 540 GB model.
Output Knowledge Created
This message produced several concrete pieces of knowledge:
- Confirmation of process health: The vLLM server was alive, not crashed or deadlocked. The eight worker processes were actively executing initialization code.
- Identification of the current phase: The server was in the torch.compile/CUDAGraph compilation phase, not stuck in weight loading or waiting for resources.
- Explanation for the empty log file: The log file wasn't empty because nothing was being written — it was empty because the C library hadn't flushed its buffer to disk. The actual output was visible via
/proc. - A reusable debugging technique: The
/proc/<pid>/fd/Napproach for reading buffered output of backgrounded processes is a general-purpose technique applicable to many server debugging scenarios.
The Thinking Process
The assistant's reasoning in this message follows a clear diagnostic pattern:
- Observe anomaly: Memory is stuck at 75.8 GiB for 10 minutes despite 8 workers being alive.
- Formulate hypothesis: This matches the known torch.compile/CUDAGraph phase.
- Design experiment: Read the process's actual stdout/stderr via
/procto confirm. - Execute and interpret: The stderr shows CUDA deprecation warnings from workers, confirming active initialization.
- Draw conclusion: The server is in compilation phase, not crashed. This is textbook debugging methodology: observe, hypothesize, test, interpret. The assistant didn't jump to conclusions about a crash or hang — it used system-level introspection to gather evidence before deciding on the next action.
The Broader Significance
This message is a small but revealing moment in a much larger engineering effort. The assistant was in the middle of a multi-hour profiling campaign, having already collected macro-benchmark throughput data, micro-benchmark GEMM latencies, and NCCL AllReduce measurements. The final piece — a torch.profiler trace — required restarting the server with special flags and enduring a 30+ minute model reload. The ten-minute stall at 75.8 GiB was a moment of uncertainty: was the server making progress, or had something gone wrong?
The ability to resolve that uncertainty with a simple /proc read is a testament to the power of Linux's process introspection capabilities. In production AI inference debugging, where servers run for hours and logs are often buffered, redirected, or rotated, the /proc filesystem is an indispensable tool for seeing what's really happening inside a running process. This message demonstrates that even in an era of sophisticated observability platforms, the humble /proc/<pid>/fd trick remains relevant and powerful.