How-to guide

Run in Kubernetes

Apply a Deployment and a restored engine serves behind a Service, on a GPU node of the snapshot's platform. The pod runs the same command a host does: warmctl up NAME.

Prototype. The whole path passes end to end on a CPU host in pod-like containers; it has not yet restored on a GPU node of a real cluster. Expect rough edges, and tell us what you hit.

Before you start

1. The runtime image

Pods run the published warmctl runtime image, registry.warmctl.io/warmctl/runtime:latest: the snapshot's vLLM image with warmctl, its vLLM extension, NVIDIA cuInterpose and tini on top. Its marker file, /opt/warmctl/RUNTIME, tells up to restore in place, in the pod's own container, instead of asking Docker for one.

bash
# from the warmctl source tree
$ docker pull registry.warmctl.io/warmctl/runtime:latest

The image's base must be the snapshot's vLLM image; one runtime image serves every snapshot built on that image, and an in-place restore refuses a snapshot taken in another.

:latest follows the newest warmctl release; for a cluster that must not change under you, pin a version tag such as :v2.0.0-beta.5. To run your own copy, or a runtime image on another vLLM base, build it from the repository with make pod-image POD_IMAGE=<your registry>/warmctl/runtime:<tag> and push it there.

2. Prepare the nodes

A DaemonSet turns off io_uring, pulls the runtime image (some 20 GB, minutes, not seconds) and the snapshot ahead of time, and reads it once so the first restore hits the page cache:

node-prep.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata: {name: warmctl-node-prep}
spec:
  selector: {matchLabels: {app: warmctl-node-prep}}
  template:
    metadata: {labels: {app: warmctl-node-prep}}
    spec:
      nodeSelector: {nvidia.com/gpu.product: NVIDIA-H100-80GB-HBM3}
      initContainers:
        - name: io-uring
          image: registry.warmctl.io/warmctl/runtime:latest
          command: [/bin/sh, -c, "echo 2 > /proc/sys/kernel/io_uring_disabled"]
          securityContext: {privileged: true}
        - name: pull
          image: registry.warmctl.io/warmctl/runtime:latest
          args: [pull, glm53-flash:8xh100-80gb-sxm-580.173.02]
          volumeMounts: [{name: snapshots, mountPath: /var/lib/warmctl}]
        - name: page-cache
          image: registry.warmctl.io/warmctl/runtime:latest
          command: [/bin/sh, -c, "find /var/lib/warmctl/pulled -type f -exec cat {} + > /dev/null"]
          volumeMounts: [{name: snapshots, mountPath: /var/lib/warmctl}]
      containers:
        - name: hold
          image: registry.warmctl.io/warmctl/runtime:latest
          command: [/usr/local/bin/tini, --, /bin/sleep, infinity]
      volumes:
        - name: snapshots
          hostPath: {path: /var/lib/warmctl, type: DirectoryOrCreate}

Pick the platform tag that matches the nodes: warmctl catalog --all or the model's card in the catalog lists them.

3. Deploy

warmctl.yaml
apiVersion: apps/v1
kind: Deployment
metadata: {name: glm53-flash}
spec:
  replicas: 1
  selector: {matchLabels: {app: glm53-flash}}
  template:
    metadata: {labels: {app: glm53-flash}}
    spec:
      containers:
        - name: warmctl
          image: registry.warmctl.io/warmctl/runtime:latest
          args: [up, glm53-flash]
          securityContext: {privileged: true}
          resources:
            limits: {nvidia.com/gpu: 8}
          readinessProbe:
            httpGet: {path: /health, port: 8080}
            periodSeconds: 1
          volumeMounts: [{name: snapshots, mountPath: /var/lib/warmctl}]
      volumes:
        - name: snapshots
          hostPath: {path: /var/lib/warmctl, type: DirectoryOrCreate}
---
apiVersion: v1
kind: Service
metadata: {name: glm53-flash}
spec:
  selector: {app: glm53-flash}
  ports: [{port: 80, targetPort: 8080}]
bash
$ kubectl apply -f node-prep.yaml -f warmctl.yaml
$ kubectl logs -f deploy/glm53-flash

The gateway listens on :8080 from the start: /health answers 503 while the pod pulls and restores and 200 once the canary passed, so the Service routes only to ready pods. kubectl logs shows the restore and then the engine's own log.

The hostPath volume is only for speed: snapshots pulled there stay for the next pod on the node. Without it each pod downloads into its own layer. Keep it on local disk; a snapshot read from a network share restores several times slower.

4. Scale on queue depth

With Prometheus and KEDA in the cluster, scale on vLLM's waiting requests. A new replica costs a restore, not a cold start:

keda.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata: {name: glm53-flash}
spec:
  scaleTargetRef: {name: glm53-flash}
  minReplicaCount: 1
  maxReplicaCount: 4
  pollingInterval: 5
  cooldownPeriod: 600
  triggers:
    - type: prometheus
      metadata:
        serverAddress: http://prometheus-operated.monitoring.svc:9090
        query: sum(vllm:num_requests_waiting{model_name="glm53-flash"})
        threshold: "8"
        activationThreshold: "1"

Limits

Every detail of the in-place restore, its preflight and the open risks: docs/kubernetes.md in the source tree.