Kubernetes and llm-d
On Kubernetes a sealed model runs as an ordinary engine Deployment. The container first
runs kllm unlock, which decrypts the tokenizer into a memory-backed /dev/shm and
prints its path, and, for a model with locked weights, unlocks the weights into a cache
directory. It then replaces itself with the engine, started on the unlocked weights with
that tokenizer. The same wrapping applies to llm-d, which schedules vLLM pods behind
its inference gateway. This page lists the prerequisites and describes the example
Deployment, its SGLang variant and the llm-d setup.
Warning
Neither the manifest nor the llm-d setup on this page has been run on a cluster. They
are taken from examples/kubernetes/. Review and test them before use.
Note
Planned — on the Roadmap, not available in 0.1.0.dev0. Dynamo and llm-d setups that run end to end and are kept in continuous integration, and container images published for each SGLang and vLLM release.
Prerequisites
An image. The manifest uses
kllm:vllm, built from the repository’sDockerfilewith--build-arg BASE=vllm/vllm-openai:latest(see Docker). No image is published; push the image to a registry that the cluster can pull from, and replaceimage:with its full reference.GPU scheduling. Nodes that advertise the
nvidia.com/gpuresource.The sealed model. A PersistentVolumeClaim named
modelsthat contains the sealed directory atQwen3-8B-sealed. It is mounted read-only at/models.The key. A Secret named
kllm-keywith two entries:model.key(the key file) andpassphrase(its passphrase).
Deploying the example
Create the Secret and apply the manifest:
$ kubectl create secret generic kllm-key --from-file=model.key \
--from-literal=passphrase=...
$ kubectl apply -f examples/kubernetes/kllm-vllm.yaml
The manifest defines a Deployment and a Service, both named qwen3-8b-sealed:
# A sealed model on Kubernetes with the kLLM vLLM image (see the Dockerfile).
# Development setup: the key file and its passphrase come from a Secret. In production the
# pod runs in a confidential VM and takes the key from the deployer's key broker
# (kLLM Enterprise).
#
# kubectl create secret generic kllm-key --from-file=model.key \
# --from-literal=passphrase=...
# kubectl apply -f examples/kubernetes/kllm-vllm.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: qwen3-8b-sealed
spec:
replicas: 1
selector:
matchLabels: {app: qwen3-8b-sealed}
template:
metadata:
labels: {app: qwen3-8b-sealed}
spec:
containers:
- name: engine
image: kllm:vllm
command: ["sh", "-c"]
# kllm unlock writes the secret tokenizer to /dev/shm (memory) and prints its path.
# For locked weights (the default of kllm seal) it also unlocks them into
# KLLM_UNLOCKED_DIR and reports "weights unlocked: PATH" on standard error; the
# engine must load that directory. A model sealed with --no-lock is used as it is.
args:
- >-
TOK=$(kllm unlock /models/Qwen3-8B-sealed --key /keys/model.key --engine vllm
2>/dev/shm/unlock.log)
&& MODEL=$(sed -n 's/^ *weights unlocked: //p' /dev/shm/unlock.log)
&& exec vllm serve "${MODEL:-/models/Qwen3-8B-sealed}" --tokenizer "$TOK"
--served-model-name Qwen3-8B --host 0.0.0.0 --port 8000
env:
- name: KLLM_PASSPHRASE
valueFrom: {secretKeyRef: {name: kllm-key, key: passphrase}}
- name: KLLM_UNLOCKED_DIR
value: /unlocked
ports: [{containerPort: 8000}]
resources:
limits: {nvidia.com/gpu: 1}
readinessProbe:
httpGet: {path: /health, port: 8000}
periodSeconds: 10
volumeMounts:
- {name: models, mountPath: /models, readOnly: true}
- {name: key, mountPath: /keys, readOnly: true}
- {name: shm, mountPath: /dev/shm}
- {name: unlocked, mountPath: /unlocked}
volumes:
- name: models
persistentVolumeClaim: {claimName: models}
- name: key
secret: {secretName: kllm-key, items: [{key: model.key, path: model.key}]}
- name: shm
emptyDir: {medium: Memory, sizeLimit: 8Gi}
# The unlocked weights, in the clear on the node; deleted with the pod, so every new
# pod unlocks them again. Size it for the model's weights.
- name: unlocked
emptyDir: {sizeLimit: 20Gi}
---
apiVersion: v1
kind: Service
metadata:
name: qwen3-8b-sealed
spec:
selector: {app: qwen3-8b-sealed}
ports: [{port: 8000, targetPort: 8000}]
Element |
Purpose |
|---|---|
|
|
|
Supplies the key file’s passphrase from the Secret, since the container has no terminal. |
|
The cache for the unlocked weights (Locked weights): an |
Volume |
Mounts only the |
Volume |
An |
|
Polls vLLM’s |
Service |
Exposes port 8000 inside the cluster. It adds no authentication. |
Important
The unlocked weights are on the node, in the clear, and so is the decrypted tokenizer; the warning below applies to both. Starting the engine on the sealed directory of a model with locked weights, the default since weight locking was added, starts it on weights that compute nothing useful.
The engine serves the OpenAI-compatible API on port 8000 under the model name
Qwen3-8B. To test it from a workstation:
$ kubectl port-forward service/qwen3-8b-sealed 8000:8000
$ curl http://127.0.0.1:8000/v1/models
Warning
This manifest is a development setup. The key file and its passphrase are stored in a
Kubernetes Secret, which cluster administrators and anyone with read access to Secrets
in the namespace can read, the decrypted tokenizer is in the node’s memory, and the
unlocked weights are on the node. None of them is protected from the node or the cluster
operator. In production the pod runs in a
confidential VM and takes the key from the deployer’s key broker, which requires kLLM
Enterprise; kllm unlock in 0.1.0.dev0 reads key files only and is refused in
production mode (KLLM_PRODUCTION other than empty or 0). See
Confidential VM deployment.
Running SGLang instead of vLLM
Use the kllm:sglang image and replace the container’s args with an SGLang launch.
kllm unlock defaults to --engine sglang:
image: kllm:sglang
command: ["sh", "-c"]
args:
- >-
TOK=$(kllm unlock /models/Qwen3-8B-sealed --key /keys/model.key 2>/dev/shm/unlock.log)
&& MODEL=$(sed -n 's/^ *weights unlocked: //p' /dev/shm/unlock.log)
&& exec python3 -m sglang.launch_server --model-path "${MODEL:-/models/Qwen3-8B-sealed}"
--tokenizer-path "$TOK" --served-model-name Qwen3-8B --host 0.0.0.0 --port 8000
--host 0.0.0.0 makes the server reachable on the pod’s address, which the Service and
the readiness probe need. Keep the env, volumeMounts and volumes sections of the
vLLM manifest, including the KLLM_UNLOCKED_DIR variable and the unlocked volume.
llm-d
llm-d schedules vLLM pods behind its inference gateway, with prefix-cache-aware and load-aware routing. A sealed model fits without changes to llm-d: in the model service’s container specification, use the kLLM image and wrap the vLLM command in the same way:
command: ["sh", "-c"]
args:
- TOK=$(kllm unlock /models/sealed --key /keys/model.key --engine vllm
2>/dev/shm/unlock.log) &&
MODEL=$(sed -n 's/^ *weights unlocked: //p' /dev/shm/unlock.log) &&
exec vllm serve "${MODEL:-/models/sealed}" --tokenizer "$TOK" --port 8000
Add the key Secret, the KLLM_PASSPHRASE variable and the memory-backed /dev/shm
volume from kllm-vllm.yaml, and, for a locked model, the KLLM_UNLOCKED_DIR variable
and a writable volume for the unlocked weights.
The choice of prefix-cache scorer determines which llm-d components hold the secret vocabulary:
Scorer |
Requirement |
|---|---|
Approximate prefix-cache scorer |
Hashes prompt text and needs no tokenizer. Only the vLLM pods hold the unlocked tokenizer. |
Precise prefix-cache scorer |
Tokenizes in the scheduler. Give the scheduler the unlocked tokenizer, and keep the scheduler in the trusted domain, because it then holds the secret vocabulary. |
Any component that holds the unlocked tokenizer can read requests and responses in clear text; place every such component in the trusted domain (Trusted domain) of the deployment profile in use (Deployment profiles).
See also
NVIDIA Dynamo for the
kllm unlockreference and the Dynamo launch.Docker for building the images.
Serving with vLLM for serving sealed models with vLLM.