Making CUDA Checkpointing 15x Faster on a B300: A Worklog A worklog benchmarking NVIDIA's cuda-checkpoint on a B300 GPU found that tuning thread and buffer counts, NUMA placement and host-to-PCIe copy matching can make CUDA checkpointing and restore up to 15x faster, with NUMA alone accounting for up to 2x and some copies running 3x faster on one platform than another. The author identifies memory allocation and the number of copy buffers and threads as the two application-specific bottlenecks in cuda-checkpoint, and reports a possible 2x speedup from a version run under gVisor. The worklog concludes that performance is hard to predict without benchmarks and traces, and recommends parallelizing operations. Making CUDA Checkpointing 15x Faster on a B300: A Worklog GPU Checkpointing GPU checkpointing lets an application save its GPU state and later pick up where it left off. This gives us more flexibility in how we run application on top of GPUs. An inference service can release them during quiet periods and resume when traffic returns Modal: Serverless GPU scaling https://modal.com/blog/truly-serverless-gpus Doubleword: Faster SGLang cold starts https://blog.doubleword.ai/fast-sglang-starts Cedana: GPU checkpointing and migration with CRIU https://cedana.com/blog/criu-gpu-checkpointing/ . We wanted to see how far we could push GPU checkpointing and restore out of curiousity and to learn a couple things along the way. We began with the cuda-checkpoint implementation on a B300 and spend some time understanding how it works and to understand where the bottlenecks lay. Then we’ll try different ideas to make it faster and see what did and didn’t work. gVisor cuda-checkpoint is often paired with another program such as CRIU CRIU: GPU checkpointing https://www.criu.org/GPU Checkpointing or gVisor gVisor: GPU checkpoint/restore https://gvisor.dev/docs/user guide/checkpoint restore/ gpu-checkpointrestore Modal: Memory snapshots with gVisor https://modal.com/blog/mem-snapshots Modal: GPU memory snapshots https://modal.com/blog/gpu-mem-snapshots to fully serialize the gpu resource and the application itself to disk. Often, one will hear about sandboxing or snapshotting with these types of applications. We observe a possible 2x speedup by running this version of cuda-checkpoint. Takeaways/TLDR - Tuning is difficult. There’s many types of hardware and system configurations. - Hardware makes a difference for application performance where sometimes copies can be 3x faster on one platform. - The configuration of the number of threads and buffers difficult as host copy speeds must match up to the PCIE copy speeds. - It’s hard to predict performance without tools. Always have some benchmarks and traces to understand what is going on. - NUMA affects performance up to 2x the performance due to memory being allocated on a different socket - The two application specific bottlenecks of cuda-checkpoint are memory allocation and the number of buffers and threads used for copying data to and from the GPU. - Find opporunities in parallelize operations How to use cuda-checkpoint Checkpointing and restoring a CUDA process each take two steps: To checkpoint, we first lock the process’s CUDA activity, preventing new work and waiting for existing work to finish. We then checkpoint its GPU state into host memory and release its GPU resourcesAlthough releasing the GPU resources is not necessary for checkpointing, it allows other resource-hungry applications to run on the GPU In between, another GPU application can utilized the free’d up resources. To bring it back, we restore the GPU resources and copy the saved data back to GPU memory. Finally, we unlock the process’s CUDA activity so it can submit work again. NVIDIA’s cuda-checkpoint tool exposes these four operations through a simple command-line interfaceThis cli communicates using a custom protocol to a cuda process over a pipe. It is a thin wrapper, but impacts latency as shown later: ps aux | grep your gpu application cuda-checkpoint --action lock --pid $PID cuda-checkpoint --action checkpoint --pid $PID cuda-checkpoint --action restore --pid $PID cuda-checkpoint --action unlock --pid $PID And this is how it interacts in action: The API is very similar: pid t pid = getpid ; CUcheckpointLockArgs lock args{0}; cuCheckpointProcessLock pid, &lock args ; CUcheckpointCheckpointArgs ckpt args{0}; cuCheckpointProcessCheckpoint pid, &ckpt args ; CUcheckpointRestoreArgs rest args{0}; cuCheckpointProcessRestore pid, &rest args ; CUcheckpointUnlockArgs unlock args{0}; cuCheckpointProcessUnlock pid, &unlock args ; CUprocessState st; cuCheckpointProcessGetState pid, &st ; if st == CU PROCESS STATE CHECKPOINTED { // ... } This tool only stores the checkpoint image to CPU memory. Existing work Fergus Finn at Doubleword made a ~9 GB checkpoint-and-restore benchmark on an RTX 4090 nearly 4× faster, cutting the time from 4.5 seconds to 1.2 seconds. The improvements came from avoiding repeated CLI initialization, enabling huge pages, preallocating the host buffer, and deferring its cleanup Reverse-engineering NVIDIA’s cuda-checkpoint for faster cold starts https://blog.doubleword.ai/what-happens-when-you-checkpoint-a-cuda-process . We’ll explore these techniques in more detail, tracing the driver’s internals to explain why they help and which bottlenecks they address. We’ll also apply some of them ourselves and see that the optimizations with CLI, huge pages and preallocating host buffers are useful. What’s happening in cuda-checkpoint? We’ll dig into libcudaNVIDIA’s user-space CUDA driver to see what’s happening underneath the hood during each step. We want to observe where the time goes through tracesEach perfetto trace shows a vLLM instance running on two B300s running GLM5-3 flash ~347GB in total . Each GPU instance holds around half the memory ~172GB and we are checkpointing both gpus, each referred to as a rank. Setup | Setup | Component | Specification | |---|---|---| | Host | Number of nodes | 1 | | | GPU | 8 × NVIDIA B300 SXM6 AC | | | PCIe | Gen6 x16; one link per GPU | | | NUMA | GPUs 0–3 on node 0; GPUs 4–7 on node 1 | | | CPU | 2 × Intel Xeon Platinum 8559C; 48 cores per socket | | | Memory | 4 TB; 1 TiB container limit; no swap | | | Driver | 580.95.05 | | Workload | Model | GLM-5.3-flash | | | Runtime | vLLM; tensor parallelism 2 | | | GPUs | 0 and 1 for every run | | Checkpoint | Per rank | 172.74 GB through the capture ring | | | Driver image per rank | 173.85 GB including about 1.1 GB on the native driver path | | Restore | Per rank | 173.8 GB | Note Every optimization arm ran as an untraced / traced, although it didn’t make much of a difference with tracing on. We run GLM-5.3-flash with vLLM 0.30.0 on GPUs 0 and 1 of one B300 host. Tensor parallelism is 2, so each GPU has a worker process, called a rank. We use eager execution, an 8,192-token context limit, and an 8 GiB KV cache per GPU. CUDA VISIBLE DEVICES=0,1 python -m vllm.entrypoints.openai.api server \ --model /path/to/GLM-5.3-flash \ --tensor-parallel-size 2 \ --enforce-eager \ --max-model-len 8192 \ --kv-cache-memory-bytes 8589934592 Generation settings go in each request: {"temperature": 0, "seed": 1234, "max tokens": 128} We run some requests to see if vLLM is running. The benchmark controller waits until vLLM reports no running or queued requests. The controller starts cuda-checkpoint --action lock --pid