# ModelDeployment

Source: /reference/modeldeployments/

Deploy a model to the fleet, from one pod to disaggregated prefill and decode.

Apply instances as `apiVersion: modelplane.ai/v1alpha1`, `kind: ModelDeployment`.

[Concept guide: Deploy a Model](/models/model-deployment/index.md)

## Example

```yaml
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)
```

## Definition

The CompositeResourceDefinition this reference is generated from, with the complete OpenAPI schema, validation rules, and defaults:

```yaml
apiVersion: apiextensions.crossplane.io/v2
kind: CompositeResourceDefinition
metadata:
  name: modeldeployments.modelplane.ai
spec:
  group: modelplane.ai
  names:
    categories: [crossplane, modelplane, models]
    kind: ModelDeployment
    plural: modeldeployments
    shortNames: [md]
  scope: Namespaced
  versions:
  - name: v1alpha1
    served: true
    referenceable: true
    additionalPrinterColumns:
    - name: REPLICAS
      type: string
      jsonPath: .status.replicas.ready
    # Serve a scale subresource so `kubectl scale` and event-driven autoscalers
    # (e.g. KEDA) can drive spec.replicas, the deployment's single scaling axis.
    # statusReplicasPath points at the count of scheduled replicas, mirroring how
    # Deployment, LeaderWorkerSet, and PodCliqueSet report status.replicas as the observed total
    # and keep readiness separate. No labelSelectorPath: replica pods run on
    # remote workload clusters, not alongside this XR, so metric-based HPA can't
    # observe them; scaling is by kubectl or external metrics.
    subresources:
      scale:
        specReplicasPath: .spec.replicas
        statusReplicasPath: .status.replicas.total
    schema:
      openAPIV3Schema:
        type: object
        required: [spec]
        properties:
          spec:
            type: object
            required: [replicas, template]
            properties:
              replicas:
                type: integer
                minimum: 0
                maximum: 100
                description: >-
                  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:
                type: object
                required: [spec]
                description: >-
                  Template for the ModelReplicas this deployment fans out to.
                  Everything but the replica count lives here, mirroring a
                  Kubernetes Deployment's spec.template.
                properties:
                  metadata:
                    type: object
                    # Reject malformed labels on the ModelDeployment itself,
                    # rather than letting them fail when the composed ModelReplica
                    # and ModelEndpoint are applied (a child error the user can't
                    # trace back to their input). These mirror Kubernetes label
                    # syntax: an optional DNS-subdomain prefix plus a name of up
                    # to 63 chars, a value of up to 63 chars, both from
                    # [A-Za-z0-9._-] and edged by alphanumerics.
                    x-kubernetes-validations:
                    - rule: "!has(self.labels) || self.labels.all(k, !k.startsWith('modelplane.ai/'))"
                      message: "template.metadata.labels keys must not use the reserved modelplane.ai/ prefix"
                    - rule: "!has(self.labels) || self.labels.all(k, k.matches('^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?[A-Za-z0-9]([-A-Za-z0-9_.]{0,61}[A-Za-z0-9])?$'))"
                      message: "template.metadata.labels keys must be valid Kubernetes label keys"
                    - rule: "!has(self.labels) || self.labels.all(k, k.split('/')[0].size() <= 253)"
                      message: "template.metadata.labels key prefixes must be DNS subdomains of at most 253 characters"
                    - rule: "!has(self.labels) || self.labels.all(k, self.labels[k].matches('^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$'))"
                      message: "template.metadata.labels values must be valid Kubernetes label values"
                    properties:
                      labels:
                        type: object
                        description: >-
                          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.
                        maxProperties: 32
                        additionalProperties:
                          type: string
                          maxLength: 63
                  spec:
                    type: object
                    required: [engines]
                    # PrefillDecode serving runs one Prefill and one Decode
                    # engine, each marking its phase. Validate the pairing here,
                    # where both serving and the engine list are visible. serving
                    # is optional and defaults to Unified, under which no engine
                    # may carry a phase.
                    x-kubernetes-validations:
                    - rule: "!has(self.serving) || self.serving.mode != 'PrefillDecode' || size(self.engines) == 2"
                      message: "PrefillDecode serving requires exactly two engines (one Prefill, one Decode)"
                    - rule: "!has(self.serving) || self.serving.mode != 'PrefillDecode' || self.engines.exists_one(e, has(e.phase) && e.phase == 'Prefill')"
                      message: "PrefillDecode serving requires exactly one engine with phase Prefill"
                    - rule: "!has(self.serving) || self.serving.mode != 'PrefillDecode' || self.engines.exists_one(e, has(e.phase) && e.phase == 'Decode')"
                      message: "PrefillDecode serving requires exactly one engine with phase Decode"
                    - rule: "(has(self.serving) && self.serving.mode == 'PrefillDecode') || !self.engines.exists(e, has(e.phase))"
                      message: "engine phase may only be set when serving.mode is PrefillDecode"
                    properties:
                      clusterSelector:
                        type: object
                        description: >-
                          Optional label selector to filter InferenceClusters.
                          If omitted, all ready clusters are candidates.
                        properties:
                          matchLabels:
                            type: object
                            additionalProperties:
                              type: string
                      modelCacheRef:
                        type: object
                        required: [name]
                        description: >-
                          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.
                        properties:
                          name:
                            type: string
                            description: ModelCache resource name in the same namespace.
                            minLength: 1
                      serving:
                        type: object
                        x-kubernetes-validations:
                        - rule: "!has(self.endpointPicker) || (has(self.router) && self.router == 'EndpointPicker')"
                          message: "serving.endpointPicker may only be set when serving.router is EndpointPicker"
                        - rule: "!has(self.router) || has(self.endpointPicker)"
                          message: "serving.router EndpointPicker requires serving.endpointPicker with its template"
                        - rule: "self.mode != 'PrefillDecode' || (has(self.router) && self.router == 'EndpointPicker')"
                          message: "PrefillDecode serving requires serving.router EndpointPicker and serving.endpointPicker"
                        description: >-
                          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.
                        required: [mode]
                        properties:
                          mode:
                            type: string
                            enum: [Unified, PrefillDecode]
                            default: Unified
                            description: >-
                              Unified serves prefill and decode on one engine.
                              PrefillDecode splits them across two engines, each marking
                              its phase as Prefill or Decode.
                          router:
                            type: string
                            enum: [EndpointPicker]
                            description: >-
                              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.
                          endpointPicker:
                            type: object
                            description: >-
                              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.
                            required: [template]
                            properties:
                              template:
                                type: object
                                description: >-
                                  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.
                                required: [spec]
                                properties:
                                  spec:
                                    type: object
                                    required: [containers]
                                    properties:
                                      containers:
                                        type: array
                                        minItems: 1
                                        maxItems: 1
                                        x-kubernetes-list-type: map
                                        x-kubernetes-list-map-keys: [name]
                                        description: >-
                                          The picker container, named "epp". Modelplane sets MODELPLANE_POOL_NAME,
                                          MODELPLANE_POOL_NAMESPACE and, when config is set,
                                          MODELPLANE_PICKER_CONFIG in its environment, for its args to pass on as
                                          $(MODELPLANE_POOL_NAME). The picker serves the gateway's gRPC on 9002,
                                          gRPC health on 9003 and Prometheus metrics on 9090. Modelplane declares
                                          those ports, and probes readiness on 9003.
                                        x-kubernetes-validations:
                                        - rule: "self.all(c, c.name == 'epp')"
                                          message: "the endpoint picker's container must be named 'epp'"
                                        items:
                                          type: object
                                          required: [name, image]
                                          properties:
                                            name:
                                              type: string
                                              minLength: 1
                                              maxLength: 63
                                            image:
                                              type: string
                                              minLength: 1
                                            command:
                                              type: array
                                              items:
                                                type: string
                                            args:
                                              type: array
                                              items:
                                                type: string
                                            env:
                                              type: array
                                              items:
                                                type: object
                                                required: [name]
                                                properties:
                                                  name:
                                                    type: string
                                                  value:
                                                    type: string
                                                  valueFrom:
                                                    type: object
                                                    properties:
                                                      secretKeyRef:
                                                        type: object
                                                        required: [name, key]
                                                        properties:
                                                          name:
                                                            type: string
                                                          key:
                                                            type: string
                                                          optional:
                                                            type: boolean
                                                      configMapKeyRef:
                                                        type: object
                                                        required: [name, key]
                                                        properties:
                                                          name:
                                                            type: string
                                                          key:
                                                            type: string
                                                          optional:
                                                            type: boolean
                                                      fieldRef:
                                                        type: object
                                                        required: [fieldPath]
                                                        properties:
                                                          fieldPath:
                                                            type: string
                                                          apiVersion:
                                                            type: string
                                            resources:
                                              type: object
                                              x-kubernetes-validations:
                                              - rule: "(!has(self.requests) || self.requests.all(k, k in ['cpu', 'memory', 'ephemeral-storage'])) && (!has(self.limits) || self.limits.all(k, k in ['cpu', 'memory', 'ephemeral-storage']))"
                                                message: "resources take only cpu, memory and ephemeral-storage; a member gets devices from its node pool's device requests"
                                              properties:
                                                requests:
                                                  type: object
                                                  maxProperties: 3
                                                  additionalProperties:
                                                    anyOf: [{type: integer}, {type: string}]
                                                    x-kubernetes-int-or-string: true
                                                limits:
                                                  type: object
                                                  maxProperties: 3
                                                  additionalProperties:
                                                    anyOf: [{type: integer}, {type: string}]
                                                    x-kubernetes-int-or-string: true
                                      imagePullSecrets:
                                        type: array
                                        items:
                                          type: object
                                          required: [name]
                                          properties:
                                            name:
                                              type: string
                              config:
                                type: object
                                x-kubernetes-preserve-unknown-fields: true
                                minProperties: 1
                                description: >-
                                  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.
                      tolerations:
                        type: array
                        description: >-
                          Tolerations that let this deployment's replicas
                          schedule onto, and stay on, InferenceClusters carrying
                          matching taints, following the Kubernetes toleration
                          model. A replica tolerating a cluster's NoSchedule
                          taint can be placed there; one tolerating a NoExecute
                          taint is not drained when that taint is applied.
                        x-kubernetes-list-type: atomic
                        items:
                          type: object
                          x-kubernetes-validations:
                          - rule: "self.operator != 'Equal' || (has(self.key) && self.key != '')"
                            message: "an Equal toleration must set a key"
                          - rule: "self.operator != 'Exists' || !has(self.value)"
                            message: "an Exists toleration must not set a value"
                          properties:
                            key:
                              type: string
                              description: >-
                                Taint key to match. Empty with operator Exists
                                tolerates every taint.
                            operator:
                              type: string
                              enum: [Exists, Equal]
                              default: Equal
                              description: >-
                                Exists matches any taint with the key (ignoring
                                value); Equal matches key and value.
                            value:
                              type: string
                            effect:
                              type: string
                              enum: [NoSchedule, NoExecute]
                              description: >-
                                Taint effect to match. Empty matches all effects.
                      engines:
                        type: array
                        description: >-
                          A ModelReplica's inference engines. An engine is one serving
                          unit: a single Standalone pod, or a gang of a Leader and one
                          or more Workers coordinating across nodes. Modelplane
                          composes the whole array once per ModelReplica; an engine
                          composes to a Deployment (Standalone) or, for a Leader/Worker
                          gang, a LeaderWorkerSet or a Grove PodCliqueSet depending on
                          the cluster's stack, but the workload kind is an implementation
                          detail. Modelplane is unopinionated about the engine itself:
                          parallelism, quantization, and KV transfer all live in the
                          members' engine flags, written by the user, never injected by
                          Modelplane.
                        minItems: 1
                        maxItems: 8
                        x-kubernetes-list-type: map
                        x-kubernetes-list-map-keys: [name]
                        items:
                          type: object
                          required: [name, members]
                          # An engine is exactly one Standalone member, or one Leader
                          # and one or more Workers. No other combination of roles
                          # forms a valid serving unit.
                          x-kubernetes-validations:
                          - rule: >-
                              (self.members.size() == 1 && self.members[0].role == 'Standalone')
                              || (self.members.exists_one(m, m.role == 'Leader')
                                  && self.members.exists(m, m.role == 'Worker')
                                  && !self.members.exists(m, m.role == 'Standalone'))
                            message: >-
                              an engine must be either a single Standalone member or one
                              Leader and one or more Workers
                          # nodeSelector is the only path to a GPU, so an engine where
                          # no member carries one would deploy with no devices at all -
                          # a mistake, not a deployment. A member may omit its own
                          # selector (a coordinator-only leader claims nothing), but
                          # some member must claim.
                          - rule: "self.members.exists(m, has(m.nodeSelector))"
                            message: >-
                              at least one member of an engine must carry a nodeSelector
                              requesting devices
                          properties:
                            name:
                              type: string
                              description: >-
                                Identifies the engine within the deployment. Becomes
                                part of the composed workload names, so it must be a
                                DNS label.
                              minLength: 1
                              maxLength: 63
                            copies:
                              type: integer
                              description: >-
                                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.
                              default: 1
                              minimum: 1
                              maximum: 64
                            phase:
                              type: string
                              enum: [Prefill, Decode]
                              description: >-
                                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.
                            members:
                              type: array
                              description: >-
                                The engine's pods. Either a single Standalone member,
                                or one Leader and one or more Workers.
                              minItems: 1
                              maxItems: 2
                              x-kubernetes-list-type: atomic
                              items:
                                type: object
                                required: [template]
                                # worker is meaningful only for a Worker member: a
                                # Standalone and a Leader are always exactly one pod on
                                # one node.
                                x-kubernetes-validations:
                                - rule: "!has(self.worker) || self.role == 'Worker'"
                                  message: "worker may only be set on a Worker member"
                                # The ports of a serving pod: a Standalone member or a Leader. A Worker
                                # doesn't serve, so these skip it. Keep in sync with apis/modelreplicas/definition.yaml.
                                # - At most one container declares 8000, where requests arrive.
                                # - If a container beside the engine declares 8000, the engine declares
                                #   its http port elsewhere, and if the engine's http port is elsewhere,
                                #   a container beside it declares 8000.
                                # - The engine may name port 8000 only http.
                                - rule: "(has(self.role) && self.role == 'Worker') || !has(self.template.spec) || self.template.spec.containers.filter(c, has(c.ports) && c.ports.exists(p, p.containerPort == 8000)).size() <= 1"
                                  message: "at most one container in a serving pod may declare containerPort 8000"
                                - rule: "(has(self.role) && self.role == 'Worker') || !has(self.template.spec) || !self.template.spec.containers.exists(c, c.name != 'engine' && has(c.ports) && c.ports.exists(p, p.containerPort == 8000)) || self.template.spec.containers.exists(c, c.name == 'engine' && has(c.ports) && c.ports.exists(p, has(p.name) && p.name == 'http' && p.containerPort != 8000))"
                                  message: "when another container declares containerPort 8000, the engine container must declare its 'http' port on a different port"
                                - rule: "(has(self.role) && self.role == 'Worker') || !has(self.template.spec) || !self.template.spec.containers.exists(c, c.name == 'engine' && has(c.ports) && c.ports.exists(p, has(p.name) && p.name == 'http' && p.containerPort != 8000)) || self.template.spec.containers.exists(c, c.name != 'engine' && has(c.ports) && c.ports.exists(p, p.containerPort == 8000))"
                                  message: "when the engine container's 'http' port isn't 8000, another container in its pod must declare containerPort 8000"
                                - rule: "(has(self.role) && self.role == 'Worker') || !has(self.template.spec) || !self.template.spec.containers.exists(c, c.name == 'engine' && has(c.ports) && c.ports.exists(p, p.containerPort == 8000 && has(p.name) && p.name != 'http'))"
                                  message: "the engine container may name containerPort 8000 only 'http'"
                                properties:
                                  role:
                                    type: string
                                    description: >-
                                      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.
                                    enum: [Standalone, Leader, Worker]
                                    default: Standalone
                                  nodeSelector:
                                    type: object
                                    description: >-
                                      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.
                                    required: [devices]
                                    properties:
                                      devices:
                                        type: array
                                        description: >-
                                          Device requests. A pool matches a request when
                                          it has a device whose count covers the request
                                          and whose driver, attributes, and capacity
                                          satisfy every selector.
                                        minItems: 1
                                        maxItems: 16
                                        x-kubernetes-list-type: map
                                        x-kubernetes-list-map-keys: [name]
                                        items:
                                          type: object
                                          required: [name, selectors]
                                          properties:
                                            name:
                                              type: string
                                              description: >-
                                                Name of this request. Mirrors a DRA
                                                DeviceRequest name; carried through to
                                                the ResourceClaim.
                                              minLength: 1
                                              maxLength: 63
                                            count:
                                              type: integer
                                              description: >-
                                                How many matching devices a node must
                                                have. For a GPU request this is the
                                                per-node GPU count.
                                              default: 1
                                              minimum: 1
                                              maximum: 64
                                            selectors:
                                              type: array
                                              description: >-
                                                Selectors a device must satisfy, all
                                                ANDed. Each is a one-of; today only cel
                                                is supported.
                                              minItems: 1
                                              maxItems: 8
                                              x-kubernetes-list-type: atomic
                                              items:
                                                type: object
                                                # A selector must carry at least one
                                                # selector kind (today only cel). Without
                                                # this an empty {} selector would match
                                                # every device, and since nodeSelector is
                                                # the only path to a GPU that silently
                                                # claims an arbitrary one.
                                                minProperties: 1
                                                properties:
                                                  cel:
                                                    type: string
                                                    description: >-
                                                      A DRA CEL expression evaluated
                                                      against one device. Reads
                                                      device.driver,
                                                      device.attributes["<driver>"].<name>
                                                      (typed), and
                                                      device.capacity["<driver>"].<name>
                                                      (a Quantity), with quantity() and
                                                      semver() helpers, e.g.
                                                      device.capacity["gpu.nvidia.com"].memory.compareTo(quantity("141Gi")) >= 0.
                                                    minLength: 1
                                                    maxLength: 10240
                                  # This object has no schema default on purpose: the
                                  # apiserver applies defaults before CEL validation, so
                                  # defaulting it would inject it onto Standalone and
                                  # Leader members and trip the rule that forbids it
                                  # there.
                                  worker:
                                    type: object
                                    description: >-
                                      Settings for a Worker member. Valid only on a
                                      member whose role is Worker.
                                    required: [nodes]
                                    properties:
                                      nodes:
                                        type: integer
                                        description: >-
                                          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.
                                        minimum: 1
                                        maximum: 63
                                  template:
                                    type: object
                                    description: >-
                                      Pod template for this member's engine pods. A
                                      curated subset of PodTemplateSpec.
                                    properties:
                                      metadata:
                                        type: object
                                        description: >-
                                          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.
                                        # Mirrors the template.metadata label rules
                                        # above: reject reserved and malformed labels
                                        # (and malformed annotation keys, validated as
                                        # qualified names just like label keys)
                                        # at admission rather than composing a pod
                                        # template the workload cluster rejects (or,
                                        # for the reserved prefix, silently dropping
                                        # the label - the modelplane.ai/ labels drive
                                        # the serving Service and workload selectors,
                                        # so a user value can't be honored).
                                        x-kubernetes-validations:
                                        - rule: "!has(self.labels) || self.labels.all(k, !k.startsWith('modelplane.ai/'))"
                                          message: "member template.metadata.labels keys must not use the reserved modelplane.ai/ prefix"
                                        - rule: "!has(self.labels) || self.labels.all(k, k.matches('^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?[A-Za-z0-9]([-A-Za-z0-9_.]{0,61}[A-Za-z0-9])?$'))"
                                          message: "member template.metadata.labels keys must be valid Kubernetes label keys"
                                        - rule: "!has(self.labels) || self.labels.all(k, k.split('/')[0].size() <= 253)"
                                          message: "member template.metadata.labels key prefixes must be DNS subdomains of at most 253 characters"
                                        - rule: "!has(self.labels) || self.labels.all(k, self.labels[k].matches('^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$'))"
                                          message: "member template.metadata.labels values must be valid Kubernetes label values"
                                        - rule: "!has(self.annotations) || self.annotations.all(k, k.matches('^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?[A-Za-z0-9]([-A-Za-z0-9_.]{0,61}[A-Za-z0-9])?$'))"
                                          message: "member template.metadata.annotations keys must be valid Kubernetes annotation keys"
                                        - rule: "!has(self.annotations) || self.annotations.all(k, k.split('/')[0].size() <= 253)"
                                          message: "member template.metadata.annotations key prefixes must be DNS subdomains of at most 253 characters"
                                        properties:
                                          labels:
                                            type: object
                                            maxProperties: 32
                                            additionalProperties:
                                              type: string
                                              maxLength: 63
                                          annotations:
                                            type: object
                                            maxProperties: 32
                                            additionalProperties:
                                              type: string
                                              maxLength: 8192
                                      spec:
                                        type: object
                                        required: [containers]
                                        description: >-
                                          Pod spec for this member's engine pods.
                                        properties:
                                          containers:
                                            type: array
                                            minItems: 1
                                            maxItems: 8
                                            x-kubernetes-list-type: map
                                            x-kubernetes-list-map-keys: [name]
                                            description: >-
                                              Containers for the engine pod. Exactly one is named "engine",
                                              the inference engine. The others run beside it, such as a
                                              routing sidecar. Modelplane adds device claims, MODELPLANE_
                                              environment, ModelCache mounts, /dev/shm and the readiness
                                              probe to the engine container only.
                                            x-kubernetes-validations:
                                            - rule: "self.exists_one(c, c.name == 'engine')"
                                              message: "exactly one container must be named 'engine'"
                                            - rule: "!self.exists(c, c.name == 'engine' && has(c.readinessProbe))"
                                              message: "the engine container can't set readinessProbe; Modelplane probes it on its http port"
                                            items:
                                              type: object
                                              required: [name, image]
                                              properties:
                                                name:
                                                  type: string
                                                  minLength: 1
                                                  maxLength: 63
                                                  pattern: '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'
                                                  description: >-
                                                    Container name. The container named
                                                    "engine" is the inference engine.
                                                image:
                                                  type: string
                                                  minLength: 1
                                                  description: Container image.
                                                command:
                                                  type: array
                                                  description: >-
                                                    Container entrypoint override, passed
                                                    through verbatim. For a Leader or
                                                    Worker, the command owns cross-node
                                                    coordination: it addresses the leader
                                                    through $(MODELPLANE_LEADER_ADDRESS),
                                                    which Modelplane injects into every
                                                    engine container on either stack. The
                                                    pod's rank (0 for the Leader,
                                                    1..worker.nodes for followers) comes
                                                    from $(MODELPLANE_RANK) on a Standard
                                                    cluster; on Dynamo the command derives
                                                    it from Grove's GROVE_PCLQ_POD_INDEX,
                                                    since Grove exposes no group-wide pod
                                                    index yet.
                                                  items:
                                                    type: string
                                                args:
                                                  type: array
                                                  description: >-
                                                    Container args, passed through to the
                                                    serving engine. Includes the model
                                                    identifier (e.g. --model=...) and any
                                                    parallelism flags.

                                                    Pass --served-model-name
                                                    $(MODELPLANE_SERVED_MODEL_NAME), the
                                                    variable Modelplane injects, so the
                                                    engine answers to the name a gateway
                                                    routes to. An engine started under a
                                                    literal name may return a 404 for
                                                    every request.
                                                  items:
                                                    type: string
                                                env:
                                                  type: array
                                                  description: >-
                                                    Environment variables. Supports
                                                    valueFrom.secretKeyRef /
                                                    configMapKeyRef for secrets and
                                                    config (like HF_TOKEN), and
                                                    valueFrom.fieldRef for pod fields
                                                    (e.g. status.podIP for vLLM's
                                                    VLLM_HOST_IP on multi-NIC nodes).
                                                  items:
                                                    type: object
                                                    required: [name]
                                                    properties:
                                                      name:
                                                        type: string
                                                      value:
                                                        type: string
                                                      valueFrom:
                                                        type: object
                                                        properties:
                                                          secretKeyRef:
                                                            type: object
                                                            required: [name, key]
                                                            properties:
                                                              name:
                                                                type: string
                                                              key:
                                                                type: string
                                                              optional:
                                                                type: boolean
                                                          configMapKeyRef:
                                                            type: object
                                                            required: [name, key]
                                                            properties:
                                                              name:
                                                                type: string
                                                              key:
                                                                type: string
                                                              optional:
                                                                type: boolean
                                                          fieldRef:
                                                            type: object
                                                            description: >-
                                                              Reference a pod field via
                                                              the downward API, e.g.
                                                              status.podIP, metadata.name,
                                                              or metadata.namespace.
                                                            required: [fieldPath]
                                                            properties:
                                                              fieldPath:
                                                                type: string
                                                              apiVersion:
                                                                type: string
                                                resources:
                                                  type: object
                                                  description: >-
                                                    CPU and memory requests and limits. On the engine container,
                                                    Modelplane adds the member's device claim beside them.
                                                  x-kubernetes-validations:
                                                  - rule: "(!has(self.requests) || self.requests.all(k, k in ['cpu', 'memory', 'ephemeral-storage'])) && (!has(self.limits) || self.limits.all(k, k in ['cpu', 'memory', 'ephemeral-storage']))"
                                                    message: "resources take only cpu, memory and ephemeral-storage; a member gets devices from its node pool's device requests"
                                                  properties:
                                                    requests:
                                                      type: object
                                                      maxProperties: 3
                                                      additionalProperties:
                                                        anyOf: [{type: integer}, {type: string}]
                                                        x-kubernetes-int-or-string: true
                                                    limits:
                                                      type: object
                                                      maxProperties: 3
                                                      additionalProperties:
                                                        anyOf: [{type: integer}, {type: string}]
                                                        x-kubernetes-int-or-string: true
                                                readinessProbe:
                                                  type: object
                                                  description: >-
                                                    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.
                                                  x-kubernetes-validations:
                                                  - rule: "[has(self.httpGet), has(self.tcpSocket), has(self.grpc)].filter(h, h).size() == 1"
                                                    message: "readinessProbe sets exactly one of httpGet, tcpSocket and grpc"
                                                  properties:
                                                    httpGet:
                                                      type: object
                                                      required: [port]
                                                      properties:
                                                        path:
                                                          type: string
                                                        port:
                                                          anyOf: [{type: integer}, {type: string}]
                                                          x-kubernetes-int-or-string: true
                                                    tcpSocket:
                                                      type: object
                                                      required: [port]
                                                      properties:
                                                        port:
                                                          anyOf: [{type: integer}, {type: string}]
                                                          x-kubernetes-int-or-string: true
                                                    grpc:
                                                      type: object
                                                      required: [port]
                                                      properties:
                                                        port:
                                                          type: integer
                                                        service:
                                                          type: string
                                                    initialDelaySeconds:
                                                      type: integer
                                                      minimum: 0
                                                    periodSeconds:
                                                      type: integer
                                                      minimum: 1
                                                    timeoutSeconds:
                                                      type: integer
                                                      minimum: 1
                                                    successThreshold:
                                                      type: integer
                                                      minimum: 1
                                                    failureThreshold:
                                                      type: integer
                                                      minimum: 1
                                                ports:
                                                  type: array
                                                  maxItems: 8
                                                  x-kubernetes-list-type: map
                                                  x-kubernetes-list-map-keys: [containerPort]
                                                  x-kubernetes-validations:
                                                  - rule: "self.all(p, !has(p.name) || self.exists_one(q, has(q.name) && q.name == p.name))"
                                                    message: "port names must be unique within a container"
                                                  description: >-
                                                    Ports the container listens on. The engine container's
                                                    port named "http" is where Modelplane probes the engine
                                                    and scrapes its metrics. Without one, the engine serves
                                                    on 8000, and an unnamed port 8000 is its "http" port. A
                                                    container in front of the engine declares 8000, and the
                                                    engine then declares its "http" port elsewhere. A Worker's
                                                    ports pass through as written.
                                                  items:
                                                    type: object
                                                    required: [containerPort]
                                                    properties:
                                                      name:
                                                        type: string
                                                        minLength: 1
                                                        maxLength: 15
                                                        pattern: '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'
                                                        description: >-
                                                          Port name. The engine container's port named "http" is
                                                          where Modelplane probes the engine and scrapes its metrics.
                                                      containerPort:
                                                        type: integer
                                                        minimum: 1
                                                        maximum: 65535
                                                        description: Port number the container listens on.
                                          imagePullSecrets:
                                            type: array
                                            description: >-
                                              Image pull secrets for private registries
                                              (NGC etc.).
                                            items:
                                              type: object
                                              required: [name]
                                              properties:
                                                name:
                                                  type: string
          status:
            type: object
            properties:
              replicas:
                type: object
                properties:
                  total:
                    type: integer
                  ready:
                    type: integer
```
