Benchmark · Nebius · Kubernetes

Boosting dedicated inference on Nebius with warmctl

GLM-5.3-Flash on 8×H100 serves in under a minute, read straight off a shared filesystem, on a Kubernetes node that has never seen the model.

GLM-5.3-Flash · FP8 · TP8 on 8×H100 · new replica → first tokensame scale
vLLM cold start load weights, compile, capture graphs, tune, warm up 650 s warmctl, file share cold page cache, nothing on the node 58.4 s warmctl, page cache snapshot already in memory 37.3 s
58.4 smedian cold start from the share
650 svLLM cold start, same GPUs
20 GiB/sshared filesystem, 3 nodes
15 / 15restores matched bitwise

01 · THE PROBLEMA replica you pay for before it serves

How long does it take to bring a model up for dedicated inference? For a small model, a minute. For a frontier MoE like GLM-5.3-Flash, it is ten to twenty.

Running inference yourself is not hard because of the API server. It is hard because of the cold start. A new vLLM replica of GLM-5.3-Flash on 8×H100 has to load 320 GiB of FP8 weights, compile its kernels, capture CUDA graphs, autotune hundreds of GEMM shapes and warm up. That is 650 seconds before the first token, and it does not count pulling the weights to a fresh node.

Every one of those minutes is a full 8×H100 node you pay for and cannot serve from. Autoscaling reacts ten minutes late, so you over-provision for peaks you could have followed. A rollout or a lost node costs the same ten minutes again, with users waiting.

If you want dedicated inference on Nebius that comes up in under a minute, that is what warmctl is for. Here is how it works, and the numbers from a real cluster.

02 · WHAT IS WARMCTLWarm up once. Restore everywhere.

warmctl is an open-source project, Apache-2.0, on GitHub. It starts the engine once, warms it over every shape it will serve, and then takes a snapshot of the whole warm process: its memory, the GPU memory and CUDA state of every rank, the NCCL communicators and the memory ranks share, and everything the warm-up produced, from compiled kernels to tuned GEMMs. The weights travel next to it, already processed, one file set per rank.

The snapshot ships as an ordinary OCI artifact, like a container image: a manifest and content-addressed blobs that any registry can store, so you push it once and pull it on any host with the same GPUs and driver branch. There, warmctl up maps the processes back with CRIU, re-attaches the GPUs with cuda-checkpoint, reloads the weights to the same addresses and checks a canary token before it opens the port. Nothing is recompiled, recaptured or re-tuned.

what a GLM-5.3-Flash snapshot holds404 GiB · 8 ranks
vLLM process treeCRIU · 82 GiB
CUDA contexts, GPU memorycuda-checkpoint
NCCL, shared GPU memory8 ranks
kernels, CUDA graphs, tuned GEMMs2.2 GiB
processed weights, per rank320 GiB
glm53-flash:8xh100-80gb-sxm-580.173.02OCI
manifest + 6,326 blobsregistry.warmctl.io
The warm engine on the left is what a cold start spends ten minutes building. warmctl freezes it into content-addressed blobs that any OCI registry, mirror or file share can hold.

03 · LET'S BENCHMARKThree 8×H100 nodes on Nebius

To test it the way you would run it, I created a Nebius Managed Kubernetes cluster with one node group of three gpu-h100-sxm nodes, 8×H100 each, and a shared filesystem mounted on every node. It is a few clicks in the console, and with preemptible instances a test like this is cheap to run.

the test clusterNebius Managed Kubernetes
Kubernetes · node group · 3 × gpu-h100-sxmnode a8×H100node b8×H100node c8×H100shared filesystem5 TB · virtiofs · mounted on every node
KubernetesNebius Managed Kubernetes 1.36.3, eu-north1
Nodes3 × 8gpu-128vcpu-1600gb: 8×H100 80GB SXM, 128 vCPU, 1.6 TB RAM
Node imageUbuntu 24.04, kernel 6.11, containerd 2.3, NVIDIA driver 580.173.02
Shared filesystem5 TB Nebius shared filesystem, mounted on every node at /mnt/filesystem-q1
ModelGLM-5.3-Flash, FP8 with MTP, TP8, 204,800-token context
Snapshotglm53-flash:8xh100-80gb-sxm-580.173.02, 404 GiB
Runtimeregistry.warmctl.io/warmctl/runtime:latest: warmctl + vLLM, 10.3 GB

You do not have to build the snapshot yourself: warmctl publishes ready-made ones for popular open models in its public catalog. The benchmark has three stages. The first two are setup that you do once, the third is what every new replica pays.

Stage 1 · Put the snapshot on the shared filesystem

Once per cluster, a Kubernetes Job pulls the snapshot from registry.warmctl.io and copies it to the share. The pull reads the OCI manifest, then fetches the blobs over 64 parallel connections and checks every digest; large files arrive in 64 MiB pieces and are assembled on disk. It lands on the node's local disk first, because many small writes are slow on any network filesystem, and a wide copy then moves it to the share in 32 streams.

stage 1 · once per clusterJob glm-mirror
registry.warmctl.ioOCI · 400 GiB
pull · 723 s · 590 MB/s
node-local diskone node
copy · 97 s · 4.1 GiB/s
shared filesystemevery node mounts it
$ warmctl pull glm53-flash:8xh100-80gb-sxm-580.173.02
  pulled 399.76 GiB: 7,126 files in 6,326 blobs, 723 s
$ cp -r … /mnt/filesystem-q1/warmctl/glm53-flash   # 32 streams, 97 s

Fourteen minutes, once. From here on, every node in the cluster reads the same copy, and no node needs local disk for snapshots.

Stage 2 · Install warmctl on the nodes

Once per node, a DaemonSet pulls the warmctl runtime image, which carries warmctl, cuInterpose and the exact vLLM build the snapshot was made with, and sets the one kernel switch CRIU needs, kernel.io_uring_disabled=2. This is node bootstrap, not a replica's critical path, so the timed runs below never wait for an image.

stage 2 · once per nodeDaemonSet runtime-prepull
warmctl/runtimevLLM inside · 10.3 GB
containerd pull · 3–7 min per node
3 × 8×H100ready to restore

Stage 3 · Cold starts from the share

Every new replica is a pod: the stock one from the warmctl Kubernetes guide, pointed at the directory on the share instead of a registry name.

containers:
  - name: warmctl
    image: registry.warmctl.io/warmctl/runtime:latest
    args: [up, /share/warmctl/glm53-flash]
    securityContext: {privileged: true}
    resources: {limits: {nvidia.com/gpu: 8}}
    readinessProbe: {httpGet: {path: /health, port: 8080}, periodSeconds: 1}
    volumeMounts: [{name: share, mountPath: /share, readOnly: true}]

Cold means cold: before every run, the node's page cache was dropped, so every byte of the 404 GiB came off the shared filesystem, and the node had no copy of the snapshot of its own. I ran it nine times one node at a time, then twice on all three nodes at once.

three replicas from one share · replayed at 10× speedround 2, measured
node a · 8×H1000.0 s
reads the shareGLM-5.3-Flash
node b · 8×H1000.0 s
reads the shareGLM-5.3-Flash
node c · 8×H1000.0 s
reads the shareGLM-5.3-Flash
59.2 smedian, one at a time
58.3 smedian, three at once
55.8–62.9 sall 15 runs
11×faster than a vLLM cold start
15 cold starts · page cache dropped before every runwarmctl up → serving
start, CRIUGPU statewake, weightscanary
0 s 10 s 20 s 30 s 40 s 50 s 60 s 70 s one at a time node a · run 1 62.9 s node a · run 2 61.1 s node a · run 3 59.5 s node b · run 1 61.7 s node b · run 2 58.0 s node b · run 3 58.3 s node c · run 1 59.2 s node c · run 2 56.9 s node c · run 3 56.4 s three at once round 1 · node a 58.8 s round 1 · node b 58.1 s round 1 · node c 59.9 s round 2 · node a 58.4 s round 2 · node b 58.3 s round 2 · node c 55.8 s
Every run passed the canary bitwise: the restored engine produced the same first token, with the same log-probability, as the engine that was snapshotted.
NodeRun 1Run 2Run 3Read rate
node a62.9 s61.1 s59.5 s6.9–7.3 GiB/s
node b61.7 s58.0 s58.3 s7.0–7.5 GiB/s
node c59.2 s56.9 s56.4 s7.3–7.7 GiB/s

Starting all three replicas at the same moment did not slow any of them down. Each node still read the share at 7.2–7.8 GiB/s, so the filesystem served about 22 GiB/s in all:

Roundnode anode bnode cShare, 3 nodes
round 158.8 s58.1 s59.9 s22.0 GiB/s
round 258.4 s58.3 s55.8 s22.6 GiB/s

04 · WHY IT IS FASTRead ahead, all the way down

A restore needs two things from storage: the CRIU images, 82 GiB of process memory, and the weights, 320 GiB in 8 rank sets. warmctl starts 32 readers before it starts CRIU. They read the CRIU images first, so CRIU finds them in page cache, then the weights rank by rank, while the GPUs are being restored. By the time a rank reloads its weights, most of them are already in memory.

one cold start from the share, second by secondnode b · run 2
0 s 10 s 20 s 30 s 40 s 50 s 60 s warmctl up serving 58.0 s CRIU 15.7 s GPU state 11.1 s wake, weights 25.9 s install 3.4 s read ahead from the share 402 GiB · 32 readers · 7.5 GiB/s CRIU images 82 GiB · 13.4 s weights 320 GiB, rank by rank · done at 53.8 s

That only works if the storage keeps up, and the Nebius shared filesystem does. I ran fio on all three nodes at once, 32 threads each, one minute per test:

fio on the shared filesystem · three nodes at once · per secondGiB/s
node anode bnode call three
0 5 10 15 20 25 sequential write · 4 MiB 21.3 GiB/s, 3 nodes sequential read · 4 MiB 20.1 GiB/s, 3 nodes GiB/s
Testnode anode bnode cThree nodes
Sequential write, 4 MiB7.05 GiB/s6.91 GiB/s7.06 GiB/s21.0 GiB/s
Sequential read, 4 MiB6.61 GiB/s6.76 GiB/s7.07 GiB/s20.4 GiB/s
Random read, 4 KiB53.6k IOPS53.6k IOPS53.7k IOPS161k IOPS

Each node gets about 7 GiB/s both ways, and three nodes together get 21 GiB/s with no sign of a ceiling. warmctl reads at 7.0–7.8 GiB/s during a restore: it takes nearly everything a node can pull from the share, which is why one replica or three come up in the same minute.

05 · AND THEN IT SERVESStock speed, from the first request

A fast start is worth little if the engine is slower after it. So with the three replicas up, I ran vllm bench serve against each of them: random prompts, fixed output lengths, from one request at a time to hundreds.

1,024 tokens in, 512 out · output throughput by concurrencytok/s
0 500 1,000 1,500 2,000 2,500 197 1 concurrent 788 16 concurrent 2,234 64 concurrent 2,262 128 concurrent 2,248 256 concurrent output tok/s
1,024 tokens in, 2,048 out · output throughputtok/s
1 concurrent 347 tok/s · ITL 7.9 ms 32 concurrent 2,744 tok/s · ITL 23.2 ms 128 concurrent 4,170 tok/s · ITL 30.1 ms
one request · time to first token by prompt lengthseconds
1k tokens in 0.18 s · ITL 7.9 ms 8k tokens in 0.28 s · ITL 7.9 ms 32k tokens in 0.93 s · ITL 8.0 ms 64k tokens in 1.28 s · ITL 8.0 ms 96k tokens in 3.49 s · ITL 8.1 ms
Decoding stays at 8 ms per token from 1k to 96k tokens of context; only the prefill grows.
Tokens in → outConcurrentOutput tok/sTotal tok/sTTFT p50ITL p50
1k → 5121197591184 ms7.9 ms
1k → 512167882,364355 ms17.0 ms
1k → 512642,2346,701420 ms30.0 ms
1k → 20481347520184 ms7.9 ms
1k → 2048322,7444,116351 ms23.2 ms
1k → 20481284,1706,25528,234 ms30.1 ms
4k → 1024642,43612,180626 ms30.7 ms
8k → 51211843,133279 ms7.9 ms
32k → 512115510,045928 ms8.0 ms
64k → 512113217,0701,278 ms8.0 ms
96k → 51218115,6363,490 ms8.1 ms
32k → 512833421,7331,094 ms14.0 ms

A single request decodes at 7.9 ms per token, about 197 tokens/s with MTP. With 128 concurrent long generations the node produces 4,170 output tokens/s, and with 32k-token prompts it processes 21,733 tokens/s. These match what the same profile does as a freshly started vLLM on 8×H100: the restored engine is the warm engine, CUDA graphs and tuned kernels included.

06 · TAKEAWAYSWhat this changes