Skip to content

Serving-path components on an InferenceCluster's system nodes run as one pod with no disruption budget, so node maintenance takes a model offline #554

Open

Description

@dennis-upbound

What happened

On a provisioned Nebius InferenceCluster, the cloud's node-maintenance controller drained and restarted both system nodes, one after the other (NodeDraining → NodeDrained → InstanceStopping → InstanceStopped → InstanceStarting → InstanceStarted → NodeUncordoned, about 7 minutes in all). It cordoned both nodes before draining the first.

The model's GPU engine pods weren't touched: 0 restarts, Ready throughout. But the deployment's endpoint picker runs on the system nodes as a single pod. It was evicted twice, and its replacement stayed Pending for about 4 minutes:

0/4 nodes are available: 2 node(s) were unschedulable, 2 node(s) had untolerated taint(s)

For those minutes the gateway answered every request for the model with 503 upstream connect error or disconnect/reset before headers.

Why

Modelplane places the serving stack on the cluster's system nodes, and most of what's on the request path there runs as one replica, with no PodDisruptionBudget and no spread across nodes:

Component On the request path Replicas PDB
Endpoint picker (one per ModelReplica) yes 1 none
Cluster gateway proxy (envoy-modelplane-system-cluster-gateway-*) yes 1 none
CoreDNS (installed by the cloud) yes 2, both on one node minAvailable 1
Modelplane collector no (metrics gaps) 1 none
trust-manager, cert-manager, Envoy Gateway and AI Gateway controllers, LWS controller no (control loops) 1 each none

The fleet InferenceGateway's proxy already handles this: compose-inference-gateway runs several replicas with envoyPDB: {maxUnavailable: 1} and topology spread across hosts and zones. The per-cluster components don't.

Node maintenance is routine on every cloud, so this happens without anyone changing the cluster. The system pool is small (on Nebius by default, 1 to 2 nodes of 4 vCPU and 16 GB), and the cloud can cordon all of it at once.

What I expected

Draining any one system node, or cordoning all of them, doesn't interrupt serving while the GPU engines are healthy.

Related

Activity

  1. dennis-upbound commented on Oct 21, 2026

    @dennis-upbound
    CollaboratorAuthor

    Possible fixes, per component, roughly in order of impact:

    1. Endpoint picker HA. Two replicas with llm-d's leader election (active/passive, so one prefix-cache view routes at a time), a PodDisruptionBudget with maxUnavailable: 1, and a spread across nodes. With Compose the router the user writes for prefill/decode serving #529 this could be serving.endpointPicker.replicas, defaulting to 2. On failover the standby starts with a cold prefix-cache view, which approximate routing rebuilds within minutes.
    2. Cluster gateway proxy HA. Reuse the fleet gateway's EnvoyProxy settings (replicas, envoyPDB, topology spread) for the per-cluster gateway. Small, since the pattern already exists in compose-inference-gateway.
    3. At least 2 system nodes, and spread for everything Modelplane composes there. 1 and 2 only survive a drain if there's somewhere else to run; InferenceCluster: Let clusters declare their own system node pools, or none at all #550 makes the pool size configurable, and the class defaults could set a minimum of 2.
    4. CoreDNS spread. The cloud installs it with 2 replicas, but both can land on one node. A drain is fine thanks to its PDB; a node failure isn't. This is cloud add-on config rather than Modelplane's, so documenting it may be enough for now.
    5. Collector. A second replica avoids metrics gaps during a drain, but two scrapers double-count unless they're sharded. Gaps are visible, so this can wait.
    6. Controllers (cert-manager, trust-manager, Envoy Gateway and AI Gateway controllers, LWS). Off the request path; reconciles pause while they reschedule. Can wait.

    Separately, a drain of a GPU node takes a single-replica deployment down for an engine restart. That's a different problem (more replicas, or a maintenance window on the GPU node group where the cloud supports one), so it probably wants its own issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions