ModelDeployment Custom Resource
This document applies to the Modelplane main branch and not to the latest release v0.5.
Deploy a model to the fleet, from one pod to disaggregated prefill and decode.
Concept guide: Deploy a Model →
#Metadata
#Example
Manifest
apiVersion: modelplane.ai/v1alpha1
kind: ModelDeployment
metadata:
name: qwen3-8b
namespace: ml-team
spec:
replicas: 2
template:
spec:
engines:
- name: qwen3-8b
members:
- role: Standalone
nodeSelector:
devices:
- name: gpu
count: 1
selectors:
- cel: |
device.capacity["gpu.nvidia.com"].memory.compareTo(quantity("20Gi")) >= 0
template:
spec:
containers:
- name: engine
image: vllm/vllm-openai:v0.23.0
args:
- --model=Qwen/Qwen3-8B
- --served-model-name=$(MODELPLANE_SERVED_MODEL_NAME)
#Spec
How many ModelReplicas to fan out to. Each replica is a complete serving instance scheduled to one InferenceCluster. 0 parks the deployment: its replicas and endpoints are removed but the object and its spec remain, and conditions report NoReplicasDesired.
Template for the ModelReplicas this deployment fans out to. Everything but the replica count lives here, mirroring a Kubernetes Deployment’s spec.template.
Labels to set on the ModelReplicas and ModelEndpoints this deployment composes, alongside the labels Modelplane manages. Use them to organize and select your own resources (kubectl get modelreplica -l tier=prod) or to route to a subset of endpoints from a ModelService. Keys under the modelplane.ai/ prefix are reserved.
Optional label selector to filter InferenceClusters. If omitted, all ready clusters are candidates.
How many identical copies of this engine to run per ModelReplica. A fixed number, sized once per deployment; scaling happens by adding ModelReplicas (spec.replicas), never by varying copies. Maps to the composed Deployment’s or LeaderWorkerSet’s replica count, or a Grove PodCliqueSet’s podCliqueScalingGroups[].replicas (its spec.replicas stays 1). Defaults to 1.
Identifies the engine within the deployment. Becomes part of the composed workload names, so it must be a DNS label.
The engine’s phase in a PrefillDecode deployment, Prefill or Decode. Set only when serving.mode is PrefillDecode, where exactly one engine takes each phase.
Reference to a ModelCache in the same namespace. Optional for single-node engines; required for any engine that spans multiple nodes (a Leader with one or more Workers), since every pod in the gang mounts it.
ModelCache resource name in the same namespace.
How the deployment is served from the cluster edge to its engines. Modelplane fronts the serving pods with an InferencePool and an endpoint picker in both modes. Unified (the default) serves prefill and decode on each engine. PrefillDecode serves them from the two engines marking those phases, with routing that sequences prefill then decode. Omitted means Unified.
The endpoint picker Modelplane runs in front of the serving pods. Required exactly when router is EndpointPicker. Modelplane composes the picker’s InferencePool, Service and RBAC around it.
The picker’s configuration, such as an llm-d EndpointPickerConfig. Modelplane mounts it at $(MODELPLANE_PICKER_CONFIG) without reading it. Omitted for a picker that takes no configuration.
Pod template for the picker. A curated subset of PodTemplateSpec, like a member’s. The user writes the picker’s command line, since each picker spells it differently.
Unified serves prefill and decode on one engine. PrefillDecode splits them across two engines, each marking its phase as Prefill or Decode.
The router the user writes. Required under PrefillDecode. Omitted, under Unified, Modelplane routes the deployment with its default picker configuration. EndpointPicker runs the endpoint picker set in endpointPicker, and the user writes any sidecar the picker needs as a container in the member templates.
Taint effect to match. Empty matches all effects.
Taint key to match. Empty with operator Exists tolerates every taint.
Exists matches any taint with the key (ignoring value); Equal matches key and value.
#Status
#Subresources
#EngineMember object
One member of an engine’s gang — a role (Standalone, Leader, or Worker) with its hardware and pod template.
The per-node device request for this member’s pods: what devices each pod needs from its node. The scheduler matches it against a candidate pool’s InferenceClass devices (surfaced on InferenceCluster status.gpuPools) and places the member on a pool that satisfies it, preferring one pool for the whole engine and splitting members across pools only when no pool satisfies them all. claim: DRA requests also become DeviceRequests in the ResourceClaim the member’s pods bind GPUs through. A GPU request’s count is the GPUs per node. Omitted, the member claims no devices and schedules onto its engine’s pool - a coordinator-only leader. At least one member per engine must carry a nodeSelector, and at least one member’s requests must resolve to a claimable (claim: DRA) device; an engine that matches only synthetic devices leaves its pods nothing to claim, so the scheduler treats such a pool as ineligible and the deployment reports InsufficientCapacity.
The member’s role in the engine. Standalone is a lone pod; a Leader coordinates and serves while its Workers join it. Defaults to Standalone.
Pod template for this member’s engine pods. A curated subset of PodTemplateSpec.
Settings for a Worker member. Valid only on a member whose role is Worker.
How many nodes this member spans - how big the engine is. Each node runs one worker pod, so the engine’s gang spans 1 (the Leader) plus this many worker pods. Defaults to 1.
#NodeSelector object
The per-node device request for this member’s pods: what devices each pod needs from its node. The scheduler matches it against a candidate pool’s InferenceClass devices (surfaced on InferenceCluster status.gpuPools) and places the member on a pool that satisfies it, preferring one pool for the whole engine and splitting members across pools only when no pool satisfies them all. claim: DRA requests also become DeviceRequests in the ResourceClaim the member’s pods bind GPUs through. A GPU request’s count is the GPUs per node. Omitted, the member claims no devices and schedules onto its engine’s pool - a coordinator-only leader. At least one member per engine must carry a nodeSelector, and at least one member’s requests must resolve to a claimable (claim: DRA) device; an engine that matches only synthetic devices leaves its pods nothing to claim, so the scheduler treats such a pool as ineligible and the deployment reports InsufficientCapacity.
How many matching devices a node must have. For a GPU request this is the per-node GPU count.
Name of this request. Mirrors a DRA DeviceRequest name; carried through to the ResourceClaim.
A DRA CEL expression evaluated against one device. Reads device.driver, device.attributes["
#PodTemplate object
Pod template for this member’s engine pods. A curated subset of PodTemplateSpec.
Metadata applied to the member’s pods. Useful for labels and annotations that control cluster-level features like service mesh injection. Label keys under the modelplane.ai/ prefix are reserved.
Pod spec for this member’s engine pods.
Reference a pod field via the downward API, e.g. status.podIP, metadata.name, or metadata.namespace.
Container image.
Container name. The container named “engine” is the inference engine.
How Kubernetes checks that a container beside the engine is ready, such as a sidecar on port 8000. The engine container can’t set one: Modelplane probes the engine on its http port.