Tutorial · benchmark · RTX PRO 6000

Start Qwen3.8-27B in 11 seconds on an RTX PRO 6000

A cold vLLM start of Qwen3.8-27B takes 215 seconds: load, compile, capture CUDA graphs. Do it once, ship the warm engine as an OCI image, and every start after is an 11-second restore. Step by step, with the benchmark.

vLLM cold start215 s
warmctl up11.0 s

Qwen3.8-27B on one RTX PRO 6000, from start to the first served request. The cold start loads, compiles and captures; the warm start restores the result.

the whole tutorial in one pictureHugging Face → warm start
once per model and GPU
every replica
Hugging FaceQwen/Qwen3.8-27B-FP8, FP8 weights29 GB$ hf download
warmctl buildload, compile, capture graphs, warm up, snapshot337 s$ warmctl build
OCI imagemanifest and content-addressed blobs38 GiB$ warmctl push
warmctl upCRIU and cuda-checkpoint restore, then serve11.0 s$ warmctl up

00 · THE IDEAWarm up once, start warm everywhere

A vLLM engine does a lot of work before it answers its first request: it loads the weights, compiles the model, autotunes kernels and captures CUDA graphs for every batch size it will serve. Every new replica, restart and scale-out does all of it again. warmctl does that work once, takes a snapshot of the warm engine and ships it as an ordinary OCI image. Starting it later is a restore, not a cold start.

This guide walks the whole path on one NVIDIA RTX PRO 6000 Blackwell with 96 GB of memory and Qwen3.8-27B, one of the most downloaded models on Hugging Face this month (7.7 million downloads), in its FP8 release: download it, describe the GPU and the engine in two small YAML files, build the warm image, run it, benchmark it, push it to a registry and start it from there.

You need
GPUNVIDIA RTX PRO 6000 (96 GB), driver 580 or newer
HostLinux x86-64, Docker with the NVIDIA container toolkit, root
Diskabout 100 GB free: the model, the snapshot and the vLLM image
Timeabout 30 minutes, most of it pulling the 26 GB vLLM image and the one cold start

01 · INSTALLInstall warmctl

$ curl -fsSL https://warmctl.io/install.sh | sudo sh
warmctl-install: downloading warmctl-v2.0.0-beta.9-linux-amd64.tar.gz
warmctl-install: checksum ok
warmctl-install: installed warmctl v2.0.0-beta.9 in /opt/warmctl
$ sudo sysctl -w kernel.io_uring_disabled=2   # CRIU cannot dump io_uring

The installer picks the newest release, checks its SHA-256 and puts warmctl and NVIDIA’s cuInterpose in /opt/warmctl; it took 2.1 s. Snapshots need io_uring off, because CRIU cannot dump it.

02 · MODELDownload the model from Hugging Face

$ python3 -m venv ~/hf && ~/hf/bin/pip install -U huggingface_hub hf_xet
$ ~/hf/bin/hf download Qwen/Qwen3.8-27B-FP8 --revision 017b9c7af6b5689d5dd426a76e0bc077eb5ca20a \
    --local-dir /models/Qwen3.8-27B-FP8

29 GB in about 100 s. Pin the revision: a snapshot is the warm state of one exact checkpoint, the profile records which, and the engine inside the container runs offline.

03 · HARDWARECheck the host and the GPU

A snapshot restores only on the hardware it was built on: the same GPU model and count, the same driver version, a CPU with the same features. So warmctl keeps a table of the GPUs it supports and matches yours by its nvidia-smi name. warmctl hardware prints the table and marks this host:

$ warmctl hardware
ID                 NVIDIA-SMI NAME                               MEMORY      ARCH    NVLINK  TOPOLOGY  THIS HOST
h100-80gb-sxm      NVIDIA H100 80GB HBM3                         81559 MiB   sm_90   yes     nvswitch
h200-141gb         NVIDIA H200                                   143771 MiB  sm_90   yes     nvswitch
l40s-48gb          NVIDIA L40S                                   46068 MiB   sm_89   no      pcie
b200-180gb         NVIDIA B200                                   183359 MiB  sm_100  yes     nvswitch
rtx-pro-6000-96gb  NVIDIA RTX PRO 6000 Blackwell Server Edition  97887 MiB   sm_120  no      pcie      1 GPU

doctor checks the same row along with everything else a build needs: Docker, the driver, io_uring, the NVIDIA container runtime and the kernel.

$ sudo warmctl doctor
ok   docker           29.8.2 (API 1.56)
ok   gpus             1 x NVIDIA RTX PRO 6000 Blackwell Server Edition, driver 580.173.02
ok   hardware         rtx-pro-6000-96gb (1 x NVIDIA RTX PRO 6000 Blackwell Server Edition)
ok   io_uring         kernel.io_uring_disabled=2 (snapshots need 2)
ok   nvidia-runtime   nvidia-ctk on PATH: true
ok   kernel           6.11.0-1016-nvidia

The profile in the next step names the row, rtx-pro-6000-96gb. If your GPU is not in the table, doctor says warn hardware with the GPU’s name and build would stop. Describe it in a hardware.yaml next to your profile, one row per GPU model, with the same fields as the RTX PRO 6000 row warmctl ships:

# ~/warmctl/hardware.yaml
schema: warmctl.hardware/v1
rows:
  - id: rtx-pro-6000-96gb
    nvidia_smi_name: "NVIDIA RTX PRO 6000 Blackwell Server Edition"
    memory_mib: 97887         # name and memory: nvidia-smi --query-gpu=name,memory.total
    arch: sm_120              # Blackwell, compute capability 12.0
    nvlink: false
    topology_class: pcie      # nvswitch for NVLink systems
    device_config_name: NVIDIA_RTX_PRO_6000_Blackwell_Server_Edition
    driver_min: "580"
$ warmctl hardware --dir ~/warmctl            # your row joins the table
$ sudo warmctl doctor --profile-dir ~/warmctl  # and the host check finds it

build reads the same directory through --profile-dir, as step 05 shows.

04 · PROFILEDescribe the engine in one YAML file

A profile says which model to serve, on which hardware, with which vLLM settings, and what to warm up: whatever the warmup runs ends up compiled in the snapshot. Anything left out comes from warmctl’s defaults. This one is set up for serving, not for a demo. warmctl ships it as qwen38-27b-rtx6000, so --profile qwen38-27b-rtx6000 works without the file; copy it into ~/warmctl when you want to change something, and a file there wins over the shipped one:

# ~/warmctl/qwen38-27b-rtx6000.yaml
schema: warmctl.profile/v1
id: qwen38-27b-rtx6000
model:
  id: Qwen/Qwen3.8-27B-FP8
  revision: 017b9c7af6b5689d5dd426a76e0bc077eb5ca20a   # pin it: a snapshot is this exact checkpoint
  served_names: [qwen38-27b]
parallel: {tp: 1}
hardware: [rtx-pro-6000-96gb]
vllm:                                   # any `vllm serve` flag
  max-model-len: 131072                 # 128K context
  max-num-seqs: 64                      # requests decoded at once
  max-num-batched-tokens: 8192          # prefill chunk
  kv-cache-memory-bytes: 51539607552    # 48 GiB of KV cache, room left for activations
  enable-chunked-prefill: true
  limit-mm-per-prompt: {image: 0, video: 0}   # text only: skip the vision encoder's buffers
  mamba-cache-mode: align               # prefix caching for the hybrid linear-attention layers
graphs:                                 # CUDA graphs captured before the snapshot
  mode: FULL_AND_PIECEWISE
  decode_batches: [1, 2, 4, 8, 16, 24, 32, 48, 64]
warmup:                                 # run once, so their kernels are compiled
  decode_prompt_tokens: 32
  prefill_tokens: [512, 2048, 8192]
resources: {memory: 128GiB}

The numbers follow from the card. The FP8 weights take about 29 GiB of the 96, 48 GiB go to the KV cache and the rest stays free for activations and CUDA graphs. Qwen3.8-27B mixes full-attention and linear-attention layers, and only 16 of its 64 layers keep a per-token cache; the other 48 are linear attention with a fixed-size state per sequence. So vLLM fits 755,547 tokens into the 48 GiB, 5.76 full 128K contexts at once.

$ warmctl profile show ~/warmctl/qwen38-27b-rtx6000.yaml   # merged over the defaults
$ warmctl profile argv ~/warmctl/qwen38-27b-rtx6000.yaml   # the exact command it will run
vllm serve /model --compilation-config {"cudagraph_capture_sizes":[1,2,4,8,16,24,32,48,64],"cudagraph_mode":"FULL_AND_PIECEWISE","mode":3,…} --enable-chunked-prefill --enable-prefix-caching --enable-sleep-mode --kv-cache-memory-bytes 51539607552 --limit-mm-per-prompt {"image":0,"video":0} --mamba-cache-mode=align --max-model-len 131072 --max-num-batched-tokens 8192 --max-num-seqs 64 --served-model-name qwen38-27b --worker-extension-cls=warmctl_vllm.worker.Extension …

05 · BUILDBuild the warm image

$ sudo warmctl build --profile-dir ~/warmctl --profile qwen38-27b-rtx6000 \
    --model-dir /models/Qwen3.8-27B-FP8 --gpus 0 --output /snaps/qwen38-27b
[    0.0s] profile qwen38-27b-rtx6000: 1 ranks, 9 graph sizes, 19 warmup cases
[    1.1s] donor warmctl-qwen38-27b started; loading the model
[  216.1s] engine ready             # the cold start every replica pays without warmctl
[  216.2s] 1 workers: sm_120, vLLM 0.30.1rc1.dev616+g1a001d584.d20261003, torch 2.15.0.dev20260928+cu134
[  216.9s] warmup 1/19 (512 tokens x 1: 0.75s)
[  219.3s] warmup 3/19 (8192 tokens x 1: 1.48s)
[  228.3s] warmup 12/19 (32 tokens x 64: 1.43s)
[  233.3s] warmup 19/19 (48 tokens x 1: 0.72s)
[  234.8s] canary recorded: 512-token prompt, miss 0.746s, hit 0.747s
[  300.0s] weights exported: 27.72 GiB in 1 ranks, slowest rank 65.1s
[  302.0s] CUDA checkpointed: 3 owners in 1.9s
[  308.2s] CRIU dump of 4 processes in 6.2s (criu 4.2.1)
[  336.9s] published /snaps/qwen38-27b: 340 files, 38.15 GiB (weights 27.72 GiB) in 5.5s
[  336.9s] snapshot completed: qwen38-27b: profile qwen38-27b-rtx6000, 1 ranks on rtx-pro-6000-96gb, 38.15 GiB

build starts vLLM in a container exactly as the profile says and pays the cold start once. Then it runs the warmup grid, records a canary (a prompt, its first token and log-probability), exports the weights, checkpoints the GPU with cuda-checkpoint and dumps the processes with CRIU:

what the snapshot holds, and where it goes on restoreto scale · from the build and up logs
on disk38.15 GiB
process + GPU state · 9.9 GiB9.9weights · 27.72 GiB27.72 GiB
CRIU 4.4 s4.4 sCUDAweights 3.0 s3.0 scanary
restore11.0 s to serving
whatsizeon restoretime
compiled kernels, CUDA graphs, autotune results, /dev/shm0.53 GiBunpacked0.7 s
engine processes, with GPU memory and CUDA state inside9.9 GiBCRIU 4.4 s, CUDA 0.9 s5.3 s
weights, as the engine laid them out27.72 GiBreloaded, fingerprints checked3.0 s
profile, hardware, canary: manifest.json< 1 MBcanary prompt compared1.5 s
a fresh container from the same imagenonestarted0.5 s

Not in it: the 48 GiB KV cache. It is empty when the snapshot is taken and is allocated again on restore, so the snapshot is smaller than what the engine holds on the GPU.

06 · RUNStart it warm

$ sudo warmctl up /snaps/qwen38-27b --gpus 0 --listen 127.0.0.1:8000
[    0.0s] snapshot qwen38-27b: profile qwen38-27b-rtx6000, 1 ranks, 38.15 GiB
[    5.6s] trees installed in 0.7s, CRIU restore in 4.3s
[    6.5s] CUDA restored: 3 owners (1 in parallel) in 1.0s
[    9.5s] weights reloaded and fingerprints match: 27.72 GiB, slowest rank 2.9s
[   11.0s] canary matches (policy warn; miss 0.747s, hit 0.744s)
[   11.0s] serving qwen38-27b-rtx6000 on http://127.0.0.1:8000

up checks the host against the snapshot, starts a fresh container, restores the processes and the GPU state at the same addresses, reloads the weights and compares the canary before it opens the port. Nothing is loaded, compiled or captured again. That is the warm path: the snapshot was still in the page cache from the build. With the cache dropped, every byte comes off the disk and the same restore took 106.6 s, because this VM reads its volume at 0.44 GiB/s and 38 GiB take that long to read. The disk sets that number, not the engine, and it is still half of the cold start.

$ curl -s localhost:8000/v1/chat/completions -H 'Content-Type: application/json' -d '{
    "model": "qwen38-27b", "messages": [{"role": "user", "content": "What is the capital of France? One word."}],
    "chat_template_kwargs": {"enable_thinking": false}}' | jq -r '.choices[0].message.content'
Paris

07 · BENCHMARKHow fast it serves

vllm bench serve against the restored engine, from a sidecar container on the same host: random prompts, --ignore-eos so every request writes all its tokens, about six minutes in all. First 1,024-token prompts with 512-token answers, from 1 to 64 requests at once:

output tokens per second, 1K prompt, 512-token answervllm bench serve · 1×RTX PRO 6000
1 request45each 46 tok/s · first token 139 ms
8 requests324each 42 tok/s · first token 253 ms
32 requests916each 32 tok/s · first token 1,625 ms
64 requests1,372each 24 tok/s · first token 1,534 ms

One request decodes at 46 tokens per second. Each token reads nearly all 27.7 GiB of weights, and the card’s 1.8 TB/s of memory bandwidth caps a single stream near 60. Batching shares that read: 64 requests make 1,372 tokens per second, 30 times one, and each still gets 24. Time to first token rises at 32 and 64 because new prompts queue behind 8,192-token prefill chunks; lower max-num-batched-tokens if first-token latency matters more than throughput.

Then one request with a long prompt:

PromptFirst tokenPer tokenTokens/s
1K139 ms21.7 ms46
8K761 ms22.0 ms45
32K3,062 ms23.0 ms43

Prefill runs at about 10,700 tokens per second, so a 32K-token prompt is answered in 3.1 s, and decoding barely slows down with context: three of every four layers keep a fixed-size state, so the attention the long prompt adds is a quarter of what a dense model of this size would pay.

08 · SHIPPush it to a registry, start it anywhere

$ sudo warmctl push /snaps/qwen38-27b ghcr.io/you/qwen38-27b
# on any other host with an RTX PRO 6000 and a 580 driver:
$ sudo warmctl up ghcr.io/you/qwen38-27b

The snapshot is one OCI artifact: manifest.json as its config and every file as content-addressed layers, so any registry stores it, GHCR, Harbor, ECR or your own. push tags it by platform and lists it under latest, so one repository can hold an image per GPU and up takes the one that fits the host. We pushed to a registry on the same host (89 s for 38 GiB) and ran up against an empty store: pulled and serving in 102 s. The download itself took 10 s; the rest of the pull was this VM’s disk writing 38 GiB, and the restore after it took 11 s, as before.

09 · NEXTWhere to go from here