Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions apis/capacityreservations/composition.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: capacityreservations.modelplane.ai
spec:
compositeTypeRef:
apiVersion: modelplane.ai/v1alpha1
kind: CapacityReservation
mode: Pipeline
pipeline:
- functionRef:
name: modelplane-modelplanecompose-capacity-reservation
step: compose-capacity-reservation
111 changes: 111 additions & 0 deletions apis/capacityreservations/definition.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
apiVersion: apiextensions.crossplane.io/v2
kind: CompositeResourceDefinition
metadata:
name: capacityreservations.modelplane.ai
spec:
group: modelplane.ai
names:
categories: [crossplane, modelplane, platform]
kind: CapacityReservation
plural: capacityreservations
scope: Cluster
versions:
- name: v1alpha1
served: true
referenceable: true
additionalPrinterColumns:
- name: PROVIDER
type: string
jsonPath: .spec.provider
schema:
openAPIV3Schema:
type: object
description: >-
An existing cloud capacity reservation that InferenceCluster node
pools can reference by name. Modelplane doesn't create or modify the
reservation; it only directs node pools to consume it.
required: [spec]
properties:
spec:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider making spec immutable (self == oldSelf). The docs say “don’t change the reservation an existing pool references”, but nothing stops someone from editing the CapacityReservation itself. That would change the pools of every cluster that references it at once....

type: object
required: [provider]
x-kubernetes-validations:
- rule: "self.provider != 'GKE' || has(self.gke)"
message: spec.gke is required when spec.provider is GKE.
- rule: "self.provider != 'EKS' || has(self.eks)"
message: spec.eks is required when spec.provider is EKS.
properties:
provider:
type: string
description: >-
The cloud the reservation belongs to. A node pool can only
reference a reservation whose provider matches its cluster's
source.
enum: [GKE, EKS]
gke:
type: object
description: >-
A Compute Engine reservation, consumed as a specific
reservation. Any reservation type works, including future
reservations in calendar mode once their reservation period
starts. Required when provider is GKE.
required: [zone, name]
properties:
zone:
type: string
description: >-
Zone the reservation is in (e.g. us-central1-a). A node
pool backed by the reservation must be restricted to
exactly this zone.
minLength: 1
maxLength: 63
name:
type: string
description: Name of the reservation.
pattern: '^[a-z]([-a-z0-9]{0,61}[a-z0-9])?$'
maxLength: 63
project:
type: string
description: >-
Project that owns the reservation, for a reservation
shared from another project. The reservation must be
shared with the cluster's project, and the cluster
project's GKE service agent must be allowed to list
reservations in this project. Omit for a reservation
in the cluster's own project.
pattern: '^[a-z][-a-z0-9]{4,28}[a-z0-9]$'
maxLength: 30
eks:
type: object
description: >-
An EC2 Capacity Reservation. Required when provider is EKS.
required: [region, type, id]
properties:
region:
type: string
description: AWS region the reservation is in (e.g. us-east-2).
minLength: 1
maxLength: 32
type:
type: string
description: >-
The kind of Capacity Reservation. CapacityBlock is a
Capacity Block for ML. OnDemand is an On-Demand
Capacity Reservation.
enum: [CapacityBlock, OnDemand]
id:
type: string
description: >-
ID of the Capacity Reservation (e.g.
cr-0123456789abcdef0). Purchasing a Capacity Block
yields this ID.
pattern: '^cr-[0-9a-f]+$'
minLength: 4
maxLength: 64
status:
type: object
properties:
conditions:
type: array
items:
type: object
40 changes: 23 additions & 17 deletions apis/eksclusters/definition.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -185,29 +185,35 @@ spec:
by the instance type.
minLength: 1
maxLength: 63
capacityBlock:
capacityReservation:
type: object
description: >-
Capacity Block reservation backing this node group.
Large GPU instances (e.g. p5en.48xlarge) are rarely
available on demand; AWS allocates them via Capacity
Blocks for ML. Set this to back the node group with a
Capacity Block you have purchased. When set, Modelplane
composes a launch template targeting the reservation
and creates the node group with CAPACITY_BLOCK capacity
type. The node group's zones must match the
reservation's Availability Zone, and nodeCount must not
exceed the reserved instance count. Omit for on-demand
node groups.
Capacity Reservation backing this node group. When set,
Modelplane composes a launch template that launches
instances only into the reservation. A Capacity Block
node group also uses the CAPACITY_BLOCK capacity type.
The node group's zones must match the reservation's
Availability Zone, and nodeCount must not exceed the
reserved instance count. Omit for on-demand node
groups.
required:
- capacityReservationId
- type
- id
properties:
capacityReservationId:
type:
type: string
description: >-
The ID of the Capacity Reservation backing the
Capacity Block (e.g. cr-0123456789abcdef0).
Purchasing a Capacity Block yields this ID.
The kind of Capacity Reservation. CapacityBlock is
a Capacity Block for ML. OnDemand is an On-Demand
Capacity Reservation.
enum:
- CapacityBlock
- OnDemand
id:
type: string
description: >-
The ID of the Capacity Reservation (e.g.
cr-0123456789abcdef0).
pattern: "^cr-[0-9a-f]+$"
minLength: 4
maxLength: 64
Expand Down
24 changes: 24 additions & 0 deletions apis/gkeclusters/definition.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,30 @@ spec:
type: string
minLength: 1
maxLength: 63
reservation:
type: object
description: >-
Compute Engine reservation backing this node pool. The
pool consumes only this reservation, as a specific
reservation, so its size is limited to the
reservation's capacity. The pool's zones must be the
reservation's zone. Omit for on-demand pools.
required:
- name
properties:
name:
type: string
description: Name of the reservation.
minLength: 1
maxLength: 63
project:
type: string
description: >-
Project that owns the reservation, for a
reservation shared from another project. Omit for
a reservation in the cluster's project.
minLength: 1
maxLength: 30
type: object
status:
description: GKEClusterStatus defines the observed state of GKECluster.
Expand Down
36 changes: 17 additions & 19 deletions apis/inferenceclusters/definition.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ spec:
x-kubernetes-validations:
- rule: "self.cluster.source != 'Nebius' || !has(self.nodePools) || self.nodePools.all(p, !has(p.fabric) || p.fabric.type != 'InfiniBand' || has(p.fabric.infiniband))"
message: fabric.infiniband is required when fabric.type is InfiniBand and cluster.source is Nebius.
- rule: "self.cluster.source in ['GKE', 'EKS'] || !has(self.nodePools) || self.nodePools.all(p, !has(p.reservationRef))"
message: nodePools[].reservationRef is only supported when cluster.source is GKE or EKS.
properties:
cluster:
type: object
Expand Down Expand Up @@ -479,29 +481,25 @@ spec:
region have every GPU type.
items:
type: string
capacityBlock:
reservationRef:
type: object
description: >-
Capacity Block reservation backing this node pool.
EKS only. Large GPU instances (e.g. p5en.48xlarge)
are rarely available on demand; AWS allocates them
via Capacity Blocks for ML. Set this to back the
pool with a Capacity Block you have purchased. The
pool's zones must match the reservation's
Availability Zone, and nodeCount must not exceed the
reserved instance count. Omit for on-demand pools.
required:
- capacityReservationId
CapacityReservation backing this node pool. GKE and
EKS only. The pool only uses the referenced
reservation, and never falls back to on-demand
capacity, so nodeCount must not exceed the reserved
instance count. The reservation's provider must match
the cluster's source. On GKE the pool's zones must be
exactly the reservation's zone; on EKS they must be
the reservation's Availability Zone. Omit for
on-demand pools.
required: [name]
properties:
capacityReservationId:
name:
type: string
description: >-
The ID of the Capacity Reservation backing the
Capacity Block (e.g. cr-0123456789abcdef0).
Purchasing a Capacity Block yields this ID.
pattern: "^cr-[0-9a-f]+$"
minLength: 4
maxLength: 64
description: Name of the CapacityReservation.
minLength: 1
maxLength: 253
fabric:
type: object
description: >-
Expand Down
4 changes: 4 additions & 0 deletions crossplane-project.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ spec:
tarball:
name: compose-aks-cluster
pathPrefix: _output/functions/compose-aks-cluster
- source: Tarball
tarball:
name: compose-capacity-reservation
pathPrefix: _output/functions/compose-capacity-reservation
- source: Tarball
tarball:
name: compose-civo-cluster
Expand Down
26 changes: 26 additions & 0 deletions docs/content/platform/inference-cluster.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,32 @@ the rank a gang computes until then.
Composing a full Dynamo graph deployment, for its frontend and request routing,
is planned.

## Capacity reservations

Large GPU machines are often only available through a capacity reservation. On
GKE and EKS a node pool can consume one you've already bought. Register the
reservation as a `CapacityReservation`, then reference it from the pool's
`reservationRef`:

{{< manifests path="concepts/capacity-reservation-gke.yaml" apply="false" >}}

A pool that references a reservation only gets nodes from that reservation, and
never falls back to on-demand capacity. Keep its `nodeCount` within the
reserved instance count. On GKE, restrict the pool's `zones` to the
reservation's zone. On EKS, restrict them to the reservation's Availability
Zone.

A `CapacityReservation` describes any GKE reservation, including a future
reservation in calendar mode once its reservation period starts. On EKS it
describes a Capacity Block for ML (`type: CapacityBlock`) or an On-Demand
Capacity Reservation (`type: OnDemand`). Modelplane doesn't create, change, or
delete the reservation itself.

When a reservation ends, the cloud removes the nodes that consume it. To move

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This holds for Capacity Blocks and GKE calendar-mode reservations. It doesn't hold for On-Demand Capacity Reservations: when one ends, its instances keep running as on-demand instances (AWS docs). Compute Engine won't delete a specific reservation that VMs still consume (GCP docs). Maybe "When a Capacity Block or a calendar-mode reservation ends, the cloud removes the nodes that consume it."

to a new reservation, add a node pool that references it, ideally before the
old reservation ends, then remove the old pool. Don't change the reservation

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can enforce this instead of documenting it. nodePools is a map list keyed by name, so a transition rule on each pool works. This one rejects changing or removing a pool's reservationRef, and still allows adding one, which an existing EKS pool needs after upgrading from capacityBlock. I tried it against an apiserver:

x-kubernetes-validations:
- rule: "!has(oldSelf.reservationRef) || (has(self.reservationRef) && self.reservationRef.name == oldSelf.reservationRef.name)"
  message: A node pool's reservationRef can't be changed or removed. Add a new pool instead.

The same change can also come in through the CapacityReservation. Editing spec.eks.id or spec.gke.name would point the pools that reference it at another reservation. A self == oldSelf rule on spec.gke and spec.eks, plus self.provider == oldSelf.provider on spec, closes that path. It fits a resource that stands for one cloud reservation.

an existing pool references.

## Examples

{{< tabs >}}
Expand Down
2 changes: 1 addition & 1 deletion docs/content/recipes/qwen3-coder.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ cache.
Both deployments were run end to end; the `InferenceClass` and
`ModelDeployment` are the exact manifests from those runs. Apply the platform
side first, then the ML side. Edit the EC2 capacity reservation placeholder in
the `InferenceCluster` before applying it.
the `CapacityReservation` before applying it.

## Validated deployments

Expand Down
1 change: 1 addition & 0 deletions docs/data/apigroups.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ groups:
# Link each kind back to its concept doc in the Documentation tab (by content
# path). Kinds without an entry (machine-generated / infra) just omit the link.
concepts:
CapacityReservation: /platform/inference-cluster
InferenceGateway: /platform/inference-gateway
InferenceClass: /platform/inference-class
InferenceCluster: /platform/inference-cluster
Expand Down
30 changes: 30 additions & 0 deletions docs/manifests/concepts/capacity-reservation-gke.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# A GKE InferenceCluster whose L4 pool consumes a Compute Engine reservation.
# The pool is restricted to the reservation's zone.
apiVersion: modelplane.ai/v1alpha1
kind: CapacityReservation
metadata:
name: l4-reservation
spec:
provider: GKE
gke:
zone: us-central1-a
name: l4-reservation
# project: shared-reservations # for a reservation shared from another project
---
apiVersion: modelplane.ai/v1alpha1
kind: InferenceCluster
metadata:
name: gke-reserved
spec:
cluster:
source: GKE
gke:
region: us-central1
nodePools:
- name: l4-pool
className: gke-l4-1x-g2
nodeCount: 2
maxNodeCount: 2
zones: [us-central1-a]
reservationRef:
name: l4-reservation
20 changes: 16 additions & 4 deletions docs/manifests/recipes/qwen3-coder/inference-cluster.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
# An EKS InferenceCluster with a two-node H200 pool over EFA, validated serving
# Qwen3-Coder-480B as a multi-node gang. The H200 nodes come from an EC2
# Capacity Block reserved for ML.
# Capacity Block reserved for ML, registered as a CapacityReservation that the
# pool references.
#
# fabric.type: EFA turns on Elastic Fabric Adapter for the gang's cross-node
# traffic; without it multi-node NCCL falls back to TCP, which is slow and
# unstable.
apiVersion: modelplane.ai/v1alpha1
kind: CapacityReservation
metadata:
name: h200-capacity-block
spec:
provider: EKS
eks:
region: us-east-2
type: CapacityBlock
id: cr-0123456789abcdef0 # replace with your reservation ID
---
apiVersion: modelplane.ai/v1alpha1
kind: InferenceCluster
metadata:
name: eks-coder
Expand All @@ -23,8 +35,8 @@ spec:
minNodeCount: 2
maxNodeCount: 2
zones:
- us-east-2b
- us-east-2b # the Capacity Block's Availability Zone
fabric:
type: EFA
capacityBlock:
capacityReservationId: cr-0123456789abcdef0 # replace with your reservation ID
reservationRef:
name: h200-capacity-block
Loading
Loading