--- title: Serve a multi-node model description: Run one copy of a model across several nodes by giving its engine a leader and workers, which OMENative creates, updates and repairs together as one Instance. since: v1.3 --- Run one copy of a model across several nodes, as a leader pod and worker pods that serve it together. [OMENative](../../concepts/omenative/overview.md) runs each leader and its workers as one [Instance](../../concepts/omenative/instances.md), which OME creates, updates, repairs and removes as a unit. By default, an update brings a new Instance up and serving before the old one drains. A `leader` and a `worker` on the engine are all it takes to run on OMENative. You'll serve Qwen3 0.6B, the model from [Serve your first model](../../getting-started/serve-your-first-model.md), on two pods with one GPU each, so you can try the steps on modest hardware. A larger model takes the same steps, with more GPUs in each pod or more workers, and often a longer [readiness deadline](#control-how-instances-update-and-recover).
- OME v1.3 or later, and `kubectl`, with the rights to create namespaces and cluster-scoped OME resources. - The ClusterBaseModel `qwen3-0-6b` from [Serve your first model](../../getting-started/serve-your-first-model.md), downloaded and `Ready`. - Two amd64 nodes with a free NVIDIA GPU each, exposed as `nvidia.com/gpu`, and the model `Ready` on both. `kubectl get nodes -l models.ome.io/clusterbasemodel.qwen3-0-6b=Ready` lists them. The leader and the worker each request 1 GPU, 10 CPUs and 30 GiB of memory. The scheduler can also place both pods on one node with two free GPUs. - Pod-to-pod network traffic between those nodes. - Recommended: the scheduler-plugins PodGroup CRD and a gang-aware scheduler, such as the alpha [OME scheduler](../../concepts/scheduling/ome-scheduler.md), which needs Kubernetes 1.35. With them, the leader and its worker are scheduled together or not at all. [Gang scheduling](../../concepts/serving/gang-scheduling.md) shows the setup. Without them, the example still runs when both GPUs are free.
## Step 1: Prepare a multi-node runtime Save this runtime as `srt-qwen3-0-6b-multi-node.yaml`. It's the `srt-qwen3-0-6b` runtime from Serve your first model, split into a `leader` and a `worker` in its `engineConfig`: ```yaml title="srt-qwen3-0-6b-multi-node.yaml" apiVersion: ome.io/v1beta1 kind: ClusterServingRuntime metadata: name: srt-qwen3-0-6b-multi-node spec: supportedModelFormats: - modelFramework: name: transformers version: "4.51.0" modelFormat: name: safetensors version: "1.0.0" modelArchitecture: Qwen3ForCausalLM autoSelect: false priority: 1 protocolVersions: - openAI modelSizeRange: min: 0.5B max: 1B engineConfig: tolerations: - key: nvidia.com/gpu operator: Exists effect: NoSchedule volumes: - name: dshm emptyDir: medium: Memory leader: runner: name: ome-container image: docker.io/lmsysorg/sglang:v0.5.5.post3-cu129-amd64 ports: - containerPort: 8080 name: http1 protocol: TCP command: - python3 - -m - sglang.launch_server - --host - "0.0.0.0" - --port - "8080" - --model-path - $(MODEL_PATH) - --served-model-name - Qwen/Qwen3-0.6B - --mem-frac - "0.9" - --tp-size - $(PARALLELISM_SIZE) - --dist-init-addr - $(OME_LEADER_ADDRESS):5000 - --nnodes - $(OME_INSTANCE_POD_COUNT) - --node-rank - $(OME_INSTANCE_POD_RANK) volumeMounts: - mountPath: /dev/shm name: dshm resources: requests: cpu: 10 memory: 30Gi nvidia.com/gpu: 1 limits: cpu: 10 memory: 30Gi nvidia.com/gpu: 1 startupProbe: httpGet: path: /health_generate port: 8080 initialDelaySeconds: 60 periodSeconds: 6 timeoutSeconds: 30 failureThreshold: 150 readinessProbe: httpGet: path: /health_generate port: 8080 periodSeconds: 60 timeoutSeconds: 200 failureThreshold: 3 livenessProbe: httpGet: path: /health port: 8080 periodSeconds: 60 timeoutSeconds: 60 failureThreshold: 5 worker: size: 1 runner: name: ome-container image: docker.io/lmsysorg/sglang:v0.5.5.post3-cu129-amd64 command: - python3 - -m - sglang.launch_server - --host - "0.0.0.0" - --port - "8080" - --model-path - $(MODEL_PATH) - --served-model-name - Qwen/Qwen3-0.6B - --mem-frac - "0.9" - --tp-size - $(PARALLELISM_SIZE) - --dist-init-addr - $(OME_LEADER_ADDRESS):5000 - --nnodes - $(OME_INSTANCE_POD_COUNT) - --node-rank - $(OME_INSTANCE_POD_RANK) volumeMounts: - mountPath: /dev/shm name: dshm resources: requests: cpu: 10 memory: 30Gi nvidia.com/gpu: 1 limits: cpu: 10 memory: 30Gi nvidia.com/gpu: 1 ``` `leader.runner` and `worker.runner` each define the pod's container, `ome-container`, and run the same SGLang command. OME sets variables that give each pod its place in the Instance: | Flag | Variable | Value | | --- | --- | --- | | `--tp-size` | `PARALLELISM_SIZE` | The pod's GPUs times the pods in the Instance: `2` here | | `--dist-init-addr` | `OME_LEADER_ADDRESS`, with port 5000 | The leader's DNS name, which resolves once the leader pod has an IP address | | `--nnodes` | `OME_INSTANCE_POD_COUNT` | `1` + `worker.size`: `2` here | | `--node-rank` | `OME_INSTANCE_POD_RANK` | `0` on the leader, `k + 1` on worker `k` | [Pod labels and environment](../../concepts/architecture/deployment-modes.md#pod-labels-and-environment) lists the other variables, and [Serving runtimes](../../concepts/runtimes/serving-runtimes.md#the-engine-decoder-and-router) explains when OME leaves `PARALLELISM_SIZE` unset. - `tolerations` and `volumes` on `engineConfig` apply to both pods. - The leader is the pod that serves requests, so only the leader declares the HTTP port and has probes. Keep serving probes off the workers: a failing readiness probe keeps the Instance from becoming ready, and a failing liveness probe can make OME recreate the whole Instance. - `worker.size: 1` gives each Instance one worker. - `supportedModelFormats` and `modelSizeRange` match those of `srt-qwen3-0-6b`. With `autoSelect: false`, OME uses this runtime only when an InferenceService names it. Create the runtime: ```bash kubectl apply -f srt-qwen3-0-6b-multi-node.yaml ``` ```output clusterservingruntime.ome.io/srt-qwen3-0-6b-multi-node created ``` !!! warning "LeaderWorkerSet is deprecated" The LeaderWorkerSet-backed `MultiNode` mode is deprecated. The catalog's multi-node runtimes, `srt-deepseek-rdma`, `srt-deepseek-rdma-pd` and `srt-kimi-k2-pd`, still pass its `LWS_*` variables, which OMENative pods don't get. To run one of them on OMENative, [make an OMENative copy](move-from-leaderworkerset.md#step-2-make-an-omenative-copy-of-the-runtime) first. ## Step 2: Create the InferenceService Create a namespace for the InferenceService: ```bash kubectl create namespace qwen3-multi-node ``` ```output namespace/qwen3-multi-node created ``` Save this InferenceService as `qwen3-multi-node-isvc.yaml`: ```yaml title="qwen3-multi-node-isvc.yaml" apiVersion: ome.io/v1beta1 kind: InferenceService metadata: name: qwen3-multi-node namespace: qwen3-multi-node spec: model: name: qwen3-0-6b runtime: name: srt-qwen3-0-6b-multi-node engine: minReplicas: 1 maxReplicas: 1 leader: {} worker: size: 1 ``` - `runtime.name` names the runtime from Step 1. - `leader: {}` and `worker` make the engine's Service send requests only to leaders, and `{}` keeps the runtime's leader as it is. Declare both: the webhook rejects an engine that declares only one. - `worker.size` is the number of workers in each Instance. It defaults to the runtime's `worker.size`, or 1. - `minReplicas` and `maxReplicas` count Instances, and each Instance needs the GPUs of a leader and all its workers. Here, that's one Instance of two pods. - To use a gang-aware scheduler, add its name as `schedulerName` under `engine`, as [Bring a gang-aware scheduler](../../concepts/serving/gang-scheduling.md#bring-a-gang-aware-scheduler) shows. Create the InferenceService: ```bash kubectl apply -f qwen3-multi-node-isvc.yaml ``` ```output inferenceservice.ome.io/qwen3-multi-node created ``` ## Step 3: Check the Instance Wait for the InferenceService to be ready. The first start pulls the SGLang image onto both nodes, which can take several minutes: ```bash kubectl wait --for=condition=Ready inferenceservice/qwen3-multi-node -n qwen3-multi-node --timeout=30m ``` ```output inferenceservice.ome.io/qwen3-multi-node condition met ``` OME created the [InferenceReplica](../../reference/api/ome.v1beta1.md#ome-io-v1beta1-InferenceReplica) `qwen3-multi-node-engine`, which runs the engine's Instances. Its counts are Instances, not pods: ```bash kubectl get inferencereplica qwen3-multi-node-engine -n qwen3-multi-node ``` ```output NAME COMPONENT DESIRED CURRENT READY AVAILABLE AGE qwen3-multi-node-engine engine 1 1 1 1 9m ``` List the Instance's pods with their ranks and nodes: ```bash kubectl get pods -n qwen3-multi-node -l ome.io/inferenceservice=qwen3-multi-node \ -o 'custom-columns=NAME:.metadata.name,RANK:.spec.containers[0].env[?(@.name=="OME_INSTANCE_POD_RANK")].value,NODE:.spec.nodeName,READY:.status.conditions[?(@.type=="Ready")].status' ``` ```output NAME RANK NODE READY qwen3-multi-node-engine-0-leader-0 0 gpu-node-1 True qwen3-multi-node-engine-0-worker-0 1 gpu-node-2 True ``` Your node names differ. If the PodGroup CRD is installed, check the Instance's PodGroup: ```bash kubectl get podgroup qwen3-multi-node-engine-0 -n qwen3-multi-node \ -o custom-columns=NAME:.metadata.name,MIN-MEMBER:.spec.minMember ``` ```output NAME MIN-MEMBER qwen3-multi-node-engine-0 2 ``` `MIN-MEMBER` is the Instance's two pods, so a gang-aware scheduler binds both or neither. ## Step 4: Send a request The engine's Service, `qwen3-multi-node-engine`, sends requests only to the leader. Forward a local port to it, and leave the command running: ```bash kubectl port-forward svc/qwen3-multi-node-engine 8080:8080 -n qwen3-multi-node ``` ```output Forwarding from 127.0.0.1:8080 -> 8080 Forwarding from [::1]:8080 -> 8080 ``` With ingress off, the default, don't send requests to the InferenceService's URL. It points to the Service `qwen3-multi-node`, which also selects the worker. To reach a multi-node engine from outside the cluster, see [Expose a multi-node engine](../networking/expose-without-ingress.md#expose-a-multi-node-engine). In a second terminal, send a chat completion request. `model` is the name that the runtime serves the model under, and `chat_template_kwargs` turns off Qwen3's thinking mode: ```bash curl -s http://localhost:8080/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model": "Qwen/Qwen3-0.6B", "messages": [{"role": "user", "content": "What is Kubernetes? Answer in one sentence."}], "max_tokens": 100, "chat_template_kwargs": {"enable_thinking": false}}' ``` The response is an OpenAI chat completion, with the model's answer in `choices[0].message.content`. ## Control how Instances update and recover The engine's `lifecycle` sets how OME updates and repairs its Instances. This version of `qwen3-multi-node-isvc.yaml` sets it explicitly and runs two Instances, which need four GPUs: ```yaml title="qwen3-multi-node-isvc.yaml" apiVersion: ome.io/v1beta1 kind: InferenceService metadata: name: qwen3-multi-node namespace: qwen3-multi-node spec: model: name: qwen3-0-6b runtime: name: srt-qwen3-0-6b-multi-node engine: minReplicas: 2 maxReplicas: 2 leader: {} worker: size: 1 lifecycle: restartPolicy: RecreateInstanceOnPodRestart readyPolicy: AllPodReady instanceReadyTimeout: 45m updateStrategy: type: SurgeThenDrain rollingUpdate: maxSurge: 1 maxUnavailable: 1 ``` | Field | Default | What it does | | --- | --- | --- | | `restartPolicy` | `RecreateInstanceOnPodRestart` | When a pod of a ready Instance fails, disappears or restarts its `ome-container`, OME recreates the whole Instance. | | `readyPolicy` | `AllPodReady` | An Instance is ready when all its pods are. An engine with a leader and a worker must use this value. | | `instanceReadyTimeout` | `30m` in the chart | How long a new Instance has to become ready before OME marks it `Failed`. | | `updateStrategy.type` | `SurgeThenDrain` | An update brings up a whole new Instance, then drains an old one once the new one is ready. | | `maxSurge` | `25%` of the Instances, rounded up, in the chart | How many new Instances an update brings up at once. | | `maxUnavailable` | No limit under `SurgeThenDrain` | How many Instances OME rebuilds at once when a pod is stuck, for example in `CrashLoopBackOff`. See [The crash-loop repair](../../concepts/omenative/instance-restart-policy.md#the-crash-loop-repair). | Apart from `instanceReadyTimeout` and `maxUnavailable`, these values are the defaults. `maxUnavailable: 1` lets OME rebuild one stuck Instance at a time, which `0` would block. It also keeps the engine `Ready` while one Instance is out of service. With `maxSurge: 1`, an update replaces one Instance at a time, so it needs two more GPUs while the new Instance starts. Apply the file: ```bash kubectl apply -f qwen3-multi-node-isvc.yaml ``` ```output inferenceservice.ome.io/qwen3-multi-node configured ``` [Instance restart policy](../../concepts/omenative/instance-restart-policy.md) and [OMENative update strategies](../../concepts/architecture/omenative-update-strategies.md) explain the other policies and strategies. ## Troubleshooting ### Pods stay Pending `kubectl describe pod` on a `Pending` pod shows `FailedScheduling` events that say what the pod lacks, such as a free GPU or a node where the model is `Ready`. If the pod has no events at all, check that the scheduler in its `spec.schedulerName` is running. If all the Instance's pods stay `Pending` under a gang-aware scheduler, the whole gang doesn't fit yet. Free GPUs for the leader and all its workers on nodes where the model is `Ready`. The OME scheduler also needs a topology key, as [Bring a gang-aware scheduler](../../concepts/serving/gang-scheduling.md#bring-a-gang-aware-scheduler) explains. If some pods run while others stay `Pending`, the running pods keep their GPUs while they wait. There are two causes: | Cause | Sign | Fix | | --- | --- | --- | | The PodGroup CRD is missing, so OME creates no PodGroup. | The engine's `GangSchedulingUnavailable` condition has the reason `PodGroupCRDNotInstalled`. | Install the CRD and restart the controller, as [Install the PodGroup CRD](../../concepts/serving/gang-scheduling.md#when-the-podgroup-crd-is-missing) shows. | | The pods use the default scheduler, which ignores PodGroups. | A `MaybeNoGangScheduler` Warning event on the InferenceService. | Name a gang-aware scheduler in the engine's `schedulerName` or the runtime's `spec.schedulerName`. | Read the condition's reason: ```bash kubectl get inferenceservice qwen3-multi-node -n qwen3-multi-node \ -o jsonpath='{.status.components.engine.lifecycle.conditions[?(@.type=="GangSchedulingUnavailable")].reason}' ``` ```output PodGroupCRDNotInstalled ``` Read the event's message: ```bash kubectl get events -n qwen3-multi-node --field-selector reason=MaybeNoGangScheduler \ -o jsonpath='{range .items[*]}{.message}{"\n"}{end}' ``` ```output OMENative component=engine created scheduler-plugins PodGroup objects but pod template's spec.schedulerName is "" (default kube-scheduler does not enforce PodGroup gang); set runtime.spec.schedulerName to a gang-aware scheduler (e.g. scheduler-plugins-scheduler) or install scheduler-plugins as a plugin in the default scheduler ``` If you added the Coscheduling plugin to your default scheduler, the warning is a false positive. With the chart's settings, a pod that stays unschedulable for 15 minutes makes OME fail the Instance, with an `InstanceFailed` Warning event that carries the scheduler's message. ### Workers can't reach the leader The pods run, but the leader never becomes ready, and the engine's logs show that it can't connect to the other ranks. Read them with `kubectl logs -n qwen3-multi-node qwen3-multi-node-engine-0-worker-0`, or with the leader's pod name. Check the leader address that the worker connects to: ```bash kubectl get pod qwen3-multi-node-engine-0-worker-0 -n qwen3-multi-node \ -o jsonpath='{.spec.containers[0].env[?(@.name=="OME_LEADER_ADDRESS")].value}' ``` ```output qwen3-multi-node-engine-0-leader-0.qwen3-multi-node-engine-headless ``` Then check these causes: - The runtime still passes `LWS_*` variables, which reach the engine as literal text. [Make an OMENative copy](move-from-leaderworkerset.md#step-2-make-an-omenative-copy-of-the-runtime) of the runtime. - The pods run with `hostNetwork: true` but without `dnsPolicy: ClusterFirstWithHostNet`, so they use the node's DNS, which can't resolve the leader's name. The catalog's multi-node runtimes set both. - A NetworkPolicy blocks traffic between the pods. Allow it between the pods with the label `ome.io/inferenceservice=qwen3-multi-node`. ### The whole Instance keeps restarting The Instance's pods keep terminating together and coming back. Each time OME recreates the Instance, it records a `RestartTriggered` Warning event on the InferenceService that names the cause. Read the causes: ```bash kubectl get events -n qwen3-multi-node --field-selector reason=RestartTriggered \ -o jsonpath='{range .items[*]}{.message}{"\n"}{end}' ``` For a worker whose container ran out of memory, the output is: ```output OMENative component=engine instance=0 restart triggered: pod qwen3-multi-node-engine-0-worker-0 container ome-container restarted after Ready: OOMKilled (exit 137) (incarnation=2) ``` | Cause | Fix | | --- | --- | | `OOMKilled` | The container used more memory than its limit. Raise `resources.limits.memory` in the runtime. | | A failed liveness probe | Loosen the probe, or leave probes off the workers, as the runtime in Step 1 does. | | `pod count 1 below desired 2`, or `gang member lost: 1 of 2 pods present` | A pod went missing, for example because someone deleted it or its node went away. | [Observe a restart](../../concepts/omenative/instance-restart-policy.md#observe-a-restart) shows the other restart events, and how to read an Instance's last failure. ## Clean up Delete the InferenceService, its namespace and the runtime: ```bash kubectl delete inferenceservice qwen3-multi-node -n qwen3-multi-node kubectl delete namespace qwen3-multi-node kubectl delete clusterservingruntime srt-qwen3-0-6b-multi-node ``` ```output inferenceservice.ome.io "qwen3-multi-node" deleted namespace "qwen3-multi-node" deleted clusterservingruntime.ome.io "srt-qwen3-0-6b-multi-node" deleted ``` The ClusterBaseModel `qwen3-0-6b` stays. [Serve your first model](../../getting-started/serve-your-first-model.md) shows how to delete it. ## Next steps - [Move from LeaderWorkerSet to OMENative](move-from-leaderworkerset.md): move an engine that runs as a LeaderWorkerSet onto OMENative. - [Use the OME scheduler](../operate-ome/ome-scheduler.md): place each Instance's leader and workers in one accelerator domain. It's alpha and needs Kubernetes 1.35. - [Gang scheduling](../../concepts/serving/gang-scheduling.md): schedule the pods of each Instance together. - [Set Instance readiness deadlines](set-instance-readiness-deadlines.md): decide when a slow or stuck Instance fails.