Engineering · RunPod · vLLM cold start

Solving vLLM cold starts on RunPod: why CRIU doesn’t work there

Cold start is a tax on every new vLLM replica, and on RunPod it is steep: a 180B model takes eleven minutes or more before it answers. warmctl skips that wait by snapshotting and restoring an engine that is already warm. The catch: CRIU, the standard tool for process snapshots, cannot run in a RunPod container. Here is how we made it work anyway.

vLLM, cold124 s
vLLM, compile caches on the volume83 s
warmctl, snapshot on the volume21 s
warmctl, the pod’s own copy11 s

Qwen3-8B on a new RunPod pod with one L40S. The model, compile caches and snapshot live on a network volume in the same data center. Time until the server answers.

~21 sa new pod serving Qwen3-8B, snapshot on a network volume
4×faster than vLLM with warm compile caches
11 sonce the pod keeps a local copy
0extra privileges: Docker’s defaults are enough

00 · THE PROBLEMWhere the time goes during a cold start

Every time you scale out on RunPod, a new pod boots vLLM in a fresh container with the model on a network volume. Before it can serve its first token, it goes through three steps:

Keeping compiled kernels on the volume helps, cutting Qwen3-8B’s start from 124 s to 83 s. But every new replica still pays for weight loading, graph capture and warmup — and on Qwen3.8-Flash-Next (180B on four GPUs) a warm-cache start still takes 668 s (751 s cold), 586 s of which is just reading weights off the volume.

What it means. Cold start is a tax paid on every new replica: while a pod boots, traffic waits or you pay for idle GPUs kept warm. Compile caches shave the tax; they don’t remove it.

01 · WARMCTLBoot once, restore everywhere

warmctl is an open-source tool (Apache-2.0) that warms an inference engine once and ships the result as a snapshot. A snapshot here is the whole running engine at the moment it became ready: the memory of its processes, their open files and their GPU memory. warmctl packs it as an OCI image, the format container registries already store, so you push it to any registry and every new replica restores it instead of booting.

On a virtual machine it stands on three pieces:

On VMs this restores Qwen3-8B in about 9 seconds (KServe) and a 27B model in 11 (RTX PRO 6000).

Bottom line. On a machine you control, the hard part, the operating system’s view of the engine, is CRIU’s job, and CRIU does it well.

02 · THE WALLRunPod is different: a GPU container is not a virtual machine

one criu dump, two clouds · VM vs RunPod podcomparing both
Virtual machineNebius · AWS · GCP
your kernel
vLLM · 3 processes
API server
engine core
GPU worker
kernel callCAP_SYS_ADMIN · clone3 · /proc/sys
✓
criu dump · kernel allows
checkpoint images3 of 3 saved
RunPod podcontainer on a shared host
restricted
vLLM · 3 processes
API server
engine core
GPU worker
container limitsno caps · seccomp · ro /proc/sys
✕
criu dump · refused (EPERM)
checkpoint imagesrefused
left: you own the kernel · right: container limits block CRIUVM 3/3 · Pod 0/3 (EPERM)

Most GPU clouds rent you a virtual machine. You get your own Linux kernel and root on it, so CRIU can do whatever it needs. Container-first clouds such as RunPod rent you a container on a shared host instead. Your processes run on the provider’s kernel, inside limits the provider sets, and three of those limits stop CRIU:

We checked a RunPod pod: exactly Docker’s defaults, and there is no way to ask for more.

What it means. Kernel-level checkpointing needs kernel-level privileges, and a container-first cloud will not hand them out. On RunPod, warmctl’s main backend cannot even start, so the fix has to live in user space.

03 · MULTIPLE BACKENDSMultiple backend support in warmctl: CRIU and DMTCP

Once kernel-level checkpointing is off the table in a container, how else can you bring a multi-GPU vLLM replica up in seconds? Before adding a second backend, we evaluated the alternatives (see our full alternatives comparison):

CRIU alternativeVerdict on RunPod
Other CRIU tools (Dynamo Snapshot)Same wall: still needs CRIU in a privileged node agent.
Fast weight loaders + compile cachesStill 83 s on 8B and 668 s on 180B: Python init, CUDA graphs and warmup remain.
vLLM initialized snapshots / PreloadSingle-GPU and single-host only, or keeps a daemon holding VRAM.
User-space checkpoint (DMTCP)Chosen as warmctl’s second backend. Processes save themselves via LD_PRELOAD — 0 kernel privileges, multi-GPU, portable.

Rather than compromise on cold-start time, we built multiple process-backend support into warmctl: CRIU and DMTCP behind a single internal process-tree interface (tree):

A bake records which backend it used in the snapshot’s manifest (process.backend) and OCI tag (-dmtcp), and warmctl up automatically picks the snapshot backend the current host can restore.

Bottom line. Supporting both CRIU and DMTCP under one roof means warmctl uses the fastest kernel path on VMs and seamlessly falls back to user-space checkpointing in locked-down GPU containers — while GPU checkpointing, weight streaming, and OCI distribution stay identical.

04 · UNDER THE HOODWhat’s the difference: CRIU and DMTCP, side by side

in a RunPod pod · CRIU (kernel) vs DMTCP (library)1 · GPU state → RAM
CRIU · via the kernelinspects from outside
kernel syscalls
vLLM · 3 unmodified processes
API server
no shim
engine core
no shim
GPU worker
no shim
GPU statecuda-checkpoint → RAM
kernel wallptrace · unshare · clone3 · /proc/sys
✕
blocked by container seccomp & caps
checkpoint imagesrefused · EPERM
DMTCP · via LD_PRELOADruns inside each process
library shim
vLLM · launched with preloaded library
API server
libdmtcp.so
engine core
libdmtcp.so
GPU worker
libdmtcp.so
GPU statecuda-checkpoint → RAM
user-space shimhooks libc (socket, mmap, clone) + fdkeep
✓
bypasses kernel limits · 0 privileges
checkpoint images3 of 3 written
left: CRIU hits EPERM · right: preloaded libdmtcp.so writes from insideCRIU 0/3 · DMTCP 3/3

Both tools freeze the same three vLLM processes, but from opposite sides of the operating system boundary:

If DMTCP needs no privileges, why not drop CRIU and use DMTCP everywhere? Because CRIU reads the ground truth from the kernel regardless of how a resource was created, whereas DMTCP only sees what passes through its libc wrappers. In a modern inference stack, plenty bypasses libc: the CUDA driver’s internal eventfds and pipes, Rust libraries (cuInterpose) issuing raw syscalls, io_uring, UCX’s mmap hooks, Python’s fcntl64, and ZMQ sockets that must drain in lockstep. Each of those silently dropped state, hung a dump, or crashed a restart until we patched DMTCP itself (11 commits in TensorRails/dmtcp) and wrote a companion plugin (fdkeep) to rebuild what bypassed libc. CRIU works out of the box on arbitrary process trees with zero runtime overhead; DMTCP is the surgical path for locked-down containers where the kernel door is shut.

What it means. The snapshot format, OCI registry, and warmctl up stay the same. CRIU remains the universal default on VMs, and DMTCP takes over inside containers.

05 · ON RUNPODHow to run it on RunPod

  1. Create a network volume in the data center where your pods will run, and start one pod from the warmctl runtime image with the volume mounted at /nv. warmctl uses RunPod’s default /workspace itself.
  2. Bake the snapshot on that pod, then copy it to the volume, manifest.json last, so a pod starting meanwhile never sees half a snapshot:
    warmctl build --in-place --profile qwen3-8b-l40s-tp1 \
      --model-dir /nv/models/qwen3-8b --output /root/snap/qwen3-8b --compress
  3. Start every new replica from it: any pod with the same image, GPU model and driver, with this as its start command.
    warmctl up /nv/snap/qwen3-8b --mode in-place    # OpenAI-compatible API on :8080

06 · RESULTSWhat a new replica costs now — and how it serves

Running those three steps on a new RunPod pod cuts Qwen3-8B’s cold start to a fifth of warm-cache vLLM:

Qwen3-8B, a new podNetwork volume, EU-NL-1 (L40S)Global Volume (A40)
vLLM, cold124 s162 s
vLLM, compile caches on the volume83 s125 s
warmctl, snapshot on the volume20–23 s49 s
warmctl, the pod’s own copy11 s11 s

Smaller models gain even more: Qwen3-0.6B starts in 8 seconds from a network volume and 4 from the pod’s disk, against 34 for vLLM with warm caches; split over two GPUs (TP=2), it serves in 5 to 6 seconds.

Does a restored engine lose throughput or accuracy?

Because DMTCP keeps its LD_PRELOAD wrappers in the process after restart, we benchmarked a restored Qwen3-8B engine head-to-head against stock vLLM and a CRIU-restored engine on the same RTX PRO 6000 GPU (vllm bench serve, 1,024 input / 512 output tokens, greedy):

Engine (Qwen3-8B)c=1 tok/sc=1 ITL p50c=64 tok/sc=64 TPOT p50
Stock vLLM87 tok/s11.40 ms2,503 tok/s23.75 ms
warmctl · CRIU restore87 tok/s11.39 ms2,498 tok/s23.83 ms
warmctl · DMTCP restore87 tok/s11.41 ms2,495 tok/s23.46 ms

Throughput and inter-token latency match within noise (0.3%), and every restore verifies chunk fingerprints on the GPU copy stream plus a canary generation check before /health returns 200.

What it means. A restored replica serves at full stock speed — and if you look at the startup table above (11 s from local disk vs 21 s from a Network Volume and 49 s from a Global Volume), the remaining cold-start time is no longer vLLM or DMTCP. It is the storage.

07 · STORAGE BOTTLENECKAnother RunPod problem: slow remote storage

Once DMTCP and cuda-checkpoint restore the processes and GPU state in a few seconds, we hit a second wall on RunPod: remote storage bandwidth. Throughput may vary by GPU tier and region, but across the A40, L40S and RTX PRO 6000 pods we tested, a single reader got only 0.11–0.28 GiB/s, and even with 32 parallel readers a regional RunPod Network Volume (MooseFS over FUSE) topped out at ~0.8–1.0 GiB/s (and a Global Volume at 0.5 GiB/s).

Unlike clouds such as Nebius, Verda, or CoreWeave, where shared network filesystems over RDMA or virtiofs deliver 7 to 15+ GiB/s per node, a RunPod pod spends almost its entire startup time pulling gigabytes off the volume. Squeezing ~1 GiB/s out of RunPod’s FUSE mount forced us to add two things: 32-stream parallel staging (restoring while copying, so dmtcp_restart and cuda-checkpoint run while weights stream in) and a dedicated --compress mode to shrink how many bytes cross the wire:

How Qwen3-8B is restored from a RunPod Network VolumeSnapshot size on volumeTime to serving
Plain copy off FUSE, then restore23.11 GiB (raw)35.9 s
32-reader stage: restore while copying (uncompressed)23.11 GiB (raw)28–30 s
32-reader stage + --compress + weights reloaded as they land13.35 GiB (compressed)20.2–22.9 s
Subsequent starts from the pod’s local staged copy (uncompressed)23.11 GiB (local disk)11.1 s

You can either bake a compressed snapshot directly, compress an existing snapshot, and then restore it on any replica pod:

Option A · BakeBake compressed straight to the volumezstd frames + BF16 byte planes
23.1 → 13.4 GiB
$ warmctl build --in-place --profile qwen3-8b-l40s-tp1 \
  --model-dir /nv/models/qwen3-8b --output /nv/snap/qwen3-8b --compress
  published /nv/snap/qwen3-8b: 37 files, 23.11 GiB, 13.35 GiB stored
Option B · ConvertOr compress an already-built snapshotkeeps the raw source pushable to OCI
images 2.84× · weights 1.46×
$ warmctl compress /root/snap/qwen3-8b /nv/snap/qwen3-8b
  compressed 37 files into /nv/snap/qwen3-8b (13.35 GiB stored)
Step 2 · ReplicaRestore on any replica pod32-reader stage + on-the-fly frame decode
~21 s to /health 200
$ warmctl up /nv/snap/qwen3-8b --mode in-place
  staged in /var/lib/warmctl/staged/qwen3-8b · serving started 21.4s

Linux process images compress well — 2.84× with independent 32 MiB zstd frames (7.28 → 2.56 GB on Qwen3-8B). Model weights are near-random and barely compress as raw bytes, but splitting BF16 weights into two byte planes (the high sign+exponent byte compresses to 0.37×, while the mantissa stays raw) yields 1.46× on the weights too.

What matters: --compress will hurt you on fast storage. Decompression is heavily CPU-bound — 8 worker threads on a pod decode weights at about 3.1–3.4 GiB/s (and single-stream zstd inside mtcp_restart ran at 0.67 GB/s, slowing a local restore from 10.3 s to 15.4 s). If you have fast local NVMe or a high-speed shared network filesystem like on Nebius (where we measured 7.0–7.8 GiB/s per node and 22 GiB/s across three nodes), Verda, or CoreWeave, storage is far faster than the CPU can decompress, so --compress will only slow your startup down:

Storage tierRead per pod/nodeUse --compress?
RunPod Global Volume (S3 FUSE)0.11–0.50 GiB/sYes — network is 6–10× slower than CPU decode.
RunPod Network Volume (MooseFS)0.8–1.0 GiB/sYes (BF16) — cuts Qwen3-8B start from 28–30 s to 20–23 s.
Local NVMe / pod’s staged copy3–6+ GiB/sNo — CPU decode is slower than disk (10.3 s raw vs 15.4 s piped zstd).
Fast shared FS (Nebius, Verda, CoreWeave)7–15+ GiB/sNo — network storage is 2–4× faster than CPU decompression.

What it means. Slow remote storage is a separate bottleneck on RunPod, but --compress offsets it at build time. Just don’t turn it on where your storage is faster than your CPUs.

08 · MULTI-GPU SCALETesting large multi-GPU models

Single-GPU 8B models are the easy case; production deployments split 30B to 180B+ models across multiple GPUs. Beyond dense TP=2 and TP=4 models (Qwen3-32B, Gemma 4 31B), the hardest case we tested on RunPod was Qwen3.8-Flash-Next: a 180B mixture-of-experts model in FP8 on four RTX PRO 6000 cards, running our full catalog recipe — tensor and expert parallel across all four GPUs, FP8 KV cache, multi-token prediction, and the native 262K context.

Stock vLLM needs 751 seconds to start it on a new pod, 586 of them reading 180 GB of weights from the network volume. A replica that takes over twelve minutes to appear cannot absorb a spike: by the time it serves, the spike is over.

Figure 3Qwen3.8-Flash-Next on a new 4×RTX PRO 6000 pod
vLLM, cold751 s
vLLM, compile caches on the volume668 s
warmctl, snapshot on the volume215 s
warmctl, the pod’s own copy21 s

A 180B mixture-of-experts model in FP8 over four GPUs. Model, caches and snapshot on a network volume in the same data center. Time until the server answers.

What broke, and what we fixed

Qwen3.8-Flash-Next, 4×RTX PRO 6000Seconds to serving
vLLM, cold751 s
vLLM, compile caches on the volume668 s
warmctl, snapshot on the volume215 s
warmctl, compressed snapshot on the volume283 s
warmctl, the pod’s own copy21 s

What it means. A replica of a 180B model on RunPod now serves in under four minutes from the network volume instead of eleven to twelve, and in 21 seconds when its pod restarts with a copy on disk, where vLLM would take the full eleven minutes again. Faster storage next to the GPUs would shrink the first number; nothing in warmctl stands in the way.

09 · STATUSWhere it stands

10 · TRY ITSet it up on RunPod

The step-by-step guide, with every pod setting and the exact start commands, is the tutorial Serve and scale vLLM on RunPod; the design and every limit are in docs/dmtcp.md. warmctl is on GitHub, and the dmtcp backend ships in v2.0.0-beta.16. If you run inference on RunPod and want help with your models, write to us.

11 · FAQQuestions we get

Why is vLLM’s cold start slow on RunPod?
Each new pod loads the weights from a network volume, compiles the model with torch.compile, captures CUDA graphs and warms up: 83–124 s for Qwen3-8B, 11–12.5 minutes for a 180B model on four GPUs. Compile caches save part of it; the weights are read again on every pod.
Can I run CRIU on RunPod?
No. Pods get Docker’s default capabilities and seccomp profile and a read-only /proc/sys; CRIU needs CAP_SYS_ADMIN or CAP_CHECKPOINT_RESTORE, unshare and clone3.
What does warmctl use instead?
DMTCP, which checkpoints processes from inside, in user space, together with NVIDIA’s cuda-checkpoint for GPU memory. warmctl picks it automatically when CRIU cannot run.
Which storage should hold the snapshot?
A network volume in the same data center as your pods. It reads at 0.8–1.0 GiB/s per pod, about twice a Global Volume, and that sets the start time.
Does it work on RunPod Serverless?
Not tested yet; this post covers RunPod Pods.

Sources