Configure pod disruption budgets

Control how many of a component's pods a node drain can take down, with a budget on each component or defaults for each deployment mode.

A PodDisruptionBudget limits how many of a component's pods evictions can take down at once, as when kubectl drain empties a node. On a Helm install, OME gives each RawDeployment and OMENative component a default budget of maxUnavailable: 1, so evictions take its pods one at a time. You can set your own budget on a component, or change the default for all components in a mode.

A PodDisruptionBudget limits evictions only. Rolling updates and OME's own changes delete pods rather than evict them, so the budget doesn't slow them down.

How OME picks a budget

The engine, the decoder and the router each take minAvailable or maxUnavailable, as in spec.engine.minAvailable. Set one of the two, to a number of pods or to a percentage of the component's pods from 0% to 100%. A runtime can set them too, in its engineConfig, decoderConfig or routerConfig, and a field that the InferenceService sets overrides the same field from the runtime.

A component that sets neither field gets its mode's default. Since v1.3, the ome-resources chart sets the RawDeployment and OMENative defaults to maxUnavailable: 1, and Step 2 changes them. A manifest install has no defaults, so only the components that set a field get a PodDisruptionBudget. On v1.2.2, a RawDeployment component that sets neither field always gets maxUnavailable: 1.

Budget Of four pods, evictions can take down Notes
maxUnavailable: 1 One at a time The chart's default. A one-pod component can still lose its only pod.
minAvailable: 50% Two at a time A percentage follows the replica count, rounded up to whole pods.
minAvailable: 0 All four Opts the component out of its mode's default.
minAvailable: 100% None Blocks evictions, so node drains stall.

Step 1: Set a budget on a component

Give the engine four pods, and keep at least half of them available. In qwen3-0-6b-isvc.yaml, raise minReplicas and maxReplicas, and add minAvailable:

qwen3-0-6b-isvc.yaml
apiVersion: ome.io/v1beta1
kind: InferenceService
metadata:
  name: qwen3-0-6b
  namespace: qwen3-0-6b
spec:
  model:
    name: qwen3-0-6b
  runtime:
    name: srt-qwen3-0-6b
  engine:
    minReplicas: 4
    maxReplicas: 4
    minAvailable: 50%

To cap how many pods can be down instead, set maxUnavailable in place of minAvailable. Apply the file:

Shell
kubectl apply -f qwen3-0-6b-isvc.yaml
Output
inferenceservice.ome.io/qwen3-0-6b configured

OME scales the Deployment qwen3-0-6b-engine to four pods, and creates or updates the PodDisruptionBudget qwen3-0-6b-engine. Wait for the four pods to be ready:

Shell
kubectl wait deployment qwen3-0-6b-engine -n qwen3-0-6b --for=jsonpath='{.status.readyReplicas}'=4 --timeout=20m
Output
deployment.apps/qwen3-0-6b-engine condition met

List the PodDisruptionBudgets of the InferenceService:

Shell
kubectl get pdb -n qwen3-0-6b -l ome.io/inferenceservice=qwen3-0-6b
Output
NAME                MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS   AGE
qwen3-0-6b-engine   50%             N/A               2                     45m

Kubernetes resolves 50% against the Deployment's four replicas, so two pods must stay available. ALLOWED DISRUPTIONS is how many pods evictions can take down now: two, while all four pods are healthy. At 0, Kubernetes refuses to evict the engine's pods.

OME sets the budget and selector back when you edit the PodDisruptionBudget, so change the budget in the InferenceService instead.

Step 2: Set the defaults for each modeUnreleased: coming in v1.3

A mode's default applies in every namespace. Like a component's budget, it takes minAvailable or maxUnavailable. Set the defaults through the chart on a Helm install, or in the ConfigMap on a manifest install.

Set the defaults under ome.controller.podDisruptionBudget in your values file. This one sets the RawDeployment default to maxUnavailable: 25%, and keeps the chart's default for OMENative:

values.yaml
ome:
  controller:
    podDisruptionBudget:
      rawDeployment:
        maxUnavailable: 25%

A mode that you leave out keeps the chart's maxUnavailable: 1. To turn a mode's default off, set the mode to null, as in omeNative: null.

Keep your other install values in the file, since helm upgrade resets any value that the file leaves out. The CHART column of helm list -n ome shows the chart version you run, such as ome-resources-1.3.0. Upgrade the release with that version:

Shell
helm upgrade --install ome oci://ghcr.io/moirai-internal/charts/ome-resources \
  --namespace ome --version 1.3.0 -f values.yaml

Helm prints Release "ome" has been upgraded. Happy Helming! and the release's new revision. The upgrade restarts the manager, which applies the new defaults.

Set the podDisruptionBudget key of the inferenceservice-config ConfigMap, in the manager's namespace ome, to the JSON that the chart would write. This value sets the RawDeployment default to maxUnavailable: 25%, and gives OMENative the chart's maxUnavailable: 1:

Shell
kubectl patch configmap inferenceservice-config -n ome --type merge \
  -p '{"data":{"podDisruptionBudget":"{\"omeNative\":{\"maxUnavailable\":1},\"rawDeployment\":{\"maxUnavailable\":\"25%\"}}"}}'
Output
configmap/inferenceservice-config patched

Write a percentage as a JSON string, as in "25%". A mode that you leave out has no default. Restart the manager so that it applies the new defaults now:

Shell
kubectl rollout restart deployment ome-controller-manager -n ome
Output
deployment.apps/ome-controller-manager restarted

On a Helm install, the next helm upgrade writes the chart's value back over your patch.

Wait for the new manager pods:

Shell
kubectl rollout status deployment ome-controller-manager -n ome

When they're running, the command ends with:

Output
deployment "ome-controller-manager" successfully rolled out

Check the defaults that the manager reads:

Shell
kubectl get configmap inferenceservice-config -n ome -o jsonpath='{.data.podDisruptionBudget}'
Output
{"omeNative":{"maxUnavailable":1},"rawDeployment":{"maxUnavailable":"25%"}}

The engine keeps its own budget from Step 1. Other RawDeployment components that set neither field now get maxUnavailable: 25%.

How OMENative counts podsUnreleased: coming in v1.3

On OMENative, OME writes the budget as an integer minAvailable, counted in pods. An OMENative component runs Instances, and a multi-pod Instance has a leader and worker.size workers. OME multiplies the Instances that the component should run by the pods in each, and rounds a percentage up. For four Instances of a leader and two workers each, 12 pods in all:

Budget that you set minAvailable that OME writes
minAvailable: 2 2
minAvailable: 90% 11
maxUnavailable: 1, the chart's default 11
maxUnavailable: 25% 9

kubectl get pdb shows the number under MIN AVAILABLE, and N/A under MAX UNAVAILABLE, whichever field you set. OME updates the number as the component scales, including when an autoscaler scales it.

The budget counts pods, not Instances. By default, when a pod of a ready multi-pod Instance goes missing, OME drains the Instance and recreates all its pods. So one eviction can take the whole Instance down. For a multi-pod component, maxUnavailable: 1 lets evictions take down one Instance at a time.

To pace OMENative's own updates, set lifecycle.updateStrategy.rollingUpdate.maxUnavailable, which counts Instances.

When a component moves between RawDeployment and OMENative, its PodDisruptionBudget protects only the old pods until the new workload's replicas are ready. OME then moves it to the new pods with the new mode's budget, or deletes it when that mode has none.

Troubleshooting

A node drain doesn't finish

kubectl drain keeps retrying an eviction that a PodDisruptionBudget refuses, and prints Cannot evict pod as it would violate the pod's disruption budget. List the InferenceService's PodDisruptionBudgets as in Step 1.

A component whose ALLOWED DISRUPTIONS is 0 has no pod to spare. Either some of its pods are already unavailable, or its budget allows no disruption, as minAvailable: 100% does. Wait for the pods to be ready, lower the budget, or add replicas. On OMENative, the drain can also wait while OME recreates a multi-pod Instance after an eviction.

No PodDisruptionBudget appears

When kubectl get pdb -n qwen3-0-6b -l ome.io/inferenceservice=qwen3-0-6b leaves out a component, check these:

  • The component sets neither field, and its mode has no default, as on a manifest install. The check at the end of Step 2 prints the defaults, if any. Set a budget as in Step 1, or defaults as in Step 2.
  • The component runs in the MultiNode mode (deprecated), which gets none. Move it to OMENative to give it one.
  • OME logs an error for the component, as A component stops updating describes.

The webhook rejects the budgetUnreleased: coming in v1.3

kubectl prints an error that includes admission webhook "inferenceservice.ome-webhook-server.validator" denied the request:, followed by a reason that names the field. For a component that sets both fields, the reason is:

Output
spec.engine.minAvailable and spec.engine.maxUnavailable cannot both be set

For a value other than a whole number or a percentage from 0% to 100%, it's:

Output
spec.engine.minAvailable must be a non-negative integer or a percentage from 0% to 100%

Keep one field, with a value such as 2 or 50%, and apply the InferenceService again. A percentage is a whole number followed by %.

A component stops updatingUnreleased: coming in v1.3

OME stops creating and updating the component's workload. It also skips the components after it, in the order engine, decoder, router. It records no event, so look for the error in the manager's logs:

Shell
kubectl logs -n ome -l control-plane=ome-controller-manager --tail=-1 | grep qwen3-0-6b

The lines include Failed to reconcile component and an error that starts with one of these:

Error starts with Cause Fix
failed to resolve engine PodDisruptionBudget The runtime sets one field for the component and the InferenceService sets the other, or the runtime's value is invalid. Set the runtime's field in the InferenceService in place of the other one, or fix it in the runtime.
failed to preflight engine PodDisruptionBudget A PodDisruptionBudget that OME didn't create has the component's name. Delete it with kubectl delete pdb qwen3-0-6b-engine -n qwen3-0-6b.

For the decoder or the router, the error names that component. OME retries, so the components update once you fix the cause.

Every InferenceService stops updatingUnreleased: coming in v1.3

When the podDisruptionBudget defaults are invalid, OME stops creating and updating the workloads of all InferenceServices, including the ones with their own budgets. Deleting an InferenceService still works. OME records no event, so look for the error in the manager's logs:

Shell
kubectl logs -n ome -l control-plane=ome-controller-manager --tail=-1 | grep 'fails to create InferenceServicesConfig'

The error starts with fails to create InferenceServicesConfig:, followed by the reason. After a Helm change, the reason is likely exactly one of podDisruptionBudget.rawDeployment.minAvailable or podDisruptionBudget.rawDeployment.maxUnavailable must be set. It means your values gave a mode minAvailable without maxUnavailable: null, as the warning in Step 2 explains. Other reasons name an invalid value, an unknown key or malformed JSON. Keys are case-sensitive.

Fix the values or the ConfigMap as in Step 2, and restart the manager after a kubectl edit. The InferenceServices then update again.

Clean up

Set the engine back to one pod without a budget: in qwen3-0-6b-isvc.yaml, remove minAvailable, and set minReplicas and maxReplicas back to 1. Then apply the file:

Shell
kubectl apply -f qwen3-0-6b-isvc.yaml
Output
inferenceservice.ome.io/qwen3-0-6b configured

The engine goes back to its mode's default. When the mode has none, OME deletes the PodDisruptionBudget qwen3-0-6b-engine.

If you changed the defaults in Step 2, put them back:

Remove podDisruptionBudget from your values file, and upgrade the release as in Step 2. The chart writes its defaults back and restarts the manager.

Remove the key:

Shell
kubectl patch configmap inferenceservice-config -n ome --type json -p '[{"op":"remove","path":"/data/podDisruptionBudget"}]'
Output
configmap/inferenceservice-config patched

Then restart the manager as in Step 2.

Next steps