Skip to content
Merged
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
3 changes: 2 additions & 1 deletion apis/modelcaches/definition.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ spec:
properties:
matchLabels:
type: object
x-kubernetes-preserve-unknown-fields: true
additionalProperties:
type: string
status:
type: object
properties:
Expand Down
3 changes: 2 additions & 1 deletion apis/modeldeployments/definition.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ spec:
properties:
matchLabels:
type: object
x-kubernetes-preserve-unknown-fields: true
additionalProperties:
type: string
nodeSelector:
type: object
description: >-
Expand Down
3 changes: 2 additions & 1 deletion apis/modelservices/definition.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ spec:
properties:
matchLabels:
type: object
x-kubernetes-preserve-unknown-fields: true
additionalProperties:
type: string
status:
type: object
properties:
Expand Down
7 changes: 6 additions & 1 deletion crossplane-project.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ spec:
license: Apache-2.0
description: An open-source AI inference platform built on Crossplane.
architectures: [amd64, arm64]
# compose-model-deployment and compose-model-cache match all resources of a
# kind with a bare ResourceSelector, which Crossplane only honours from
# v2.2.1 (https://github.com/crossplane/crossplane/pull/7241).
crossplane:
version: '>=v2.2.1'
schemas:
languages: [python]
functions:
Expand Down Expand Up @@ -59,7 +64,7 @@ spec:
- type: crd
git:
repository: https://github.com/crossplane/crossplane
ref: release-2.2
ref: v2.3.2
path: cluster/crds
- type: xpkg
xpkg:
Expand Down
4 changes: 0 additions & 4 deletions examples/platform/inference-cluster-eks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@
# EFS CSI StorageClass on the cluster, then set cache.storageClassName
# to its name (default 'modelplane-rwx-efs').
#
# The modelplane.ai/cluster label is required for the ModelDeployment
# scheduler to discover this cluster.
#
# Delete this InferenceCluster with foreground cascading deletion for a
# clean teardown:
#
Expand All @@ -36,7 +33,6 @@ kind: InferenceCluster
metadata:
name: eks-us-west
labels:
modelplane.ai/cluster: "true"
modelplane.ai/region: us-west
spec:
cluster:
Expand Down
1 change: 0 additions & 1 deletion examples/platform/inference-cluster-existing.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ kind: InferenceCluster
metadata:
name: byo-us-east
labels:
modelplane.ai/cluster: "true"
modelplane.ai/region: us-east
spec:
cluster:
Expand Down
6 changes: 2 additions & 4 deletions examples/platform/inference-cluster-gke.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,13 @@
# referencing an InferenceClass that describes the hardware shape and
# how to provision it - need to be declared.
#
# The modelplane.ai/cluster label is required for the ModelDeployment
# scheduler to discover this cluster. Add additional labels to control
# placement via clusterSelector.
# Add labels to this InferenceCluster to control which deployments land on it
# via a ModelDeployment's clusterSelector.
apiVersion: modelplane.ai/v1alpha1
kind: InferenceCluster
metadata:
name: gke-us-central
labels:
modelplane.ai/cluster: "true"
modelplane.ai/region: us-central
spec:
cluster:
Expand Down
3 changes: 0 additions & 3 deletions examples/qwen-demo/03-cluster.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ kind: InferenceCluster
metadata:
name: demo-us-central
labels:
modelplane.ai/cluster: "true"
modelplane.ai/region: us-central
spec:
cluster:
Expand All @@ -33,7 +32,6 @@ kind: InferenceCluster
metadata:
name: demo-us-east
labels:
modelplane.ai/cluster: "true"
modelplane.ai/region: us-east
spec:
cluster:
Expand All @@ -55,7 +53,6 @@ kind: InferenceCluster
metadata:
name: demo-us-west
labels:
modelplane.ai/cluster: "true"
modelplane.ai/region: us-west
spec:
cluster:
Expand Down
2 changes: 1 addition & 1 deletion functions/compose-eks-cluster/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ description = "Compose an EKS cluster with networking, node groups, and IAM role
requires-python = ">=3.11,<3.14"
license = "Apache-2.0"
dependencies = [
"crossplane-function-sdk-python>=0.13.0",
"crossplane-function-sdk-python>=0.14.0",
"click>=8.1.0",
"grpcio>=1.73.1",
"crossplane-models",
Expand Down
2 changes: 1 addition & 1 deletion functions/compose-gke-cluster/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ description = "Compose a GKE cluster with networking, node pools, and service ac
requires-python = ">=3.11,<3.14"
license = "Apache-2.0"
dependencies = [
"crossplane-function-sdk-python>=0.13.0",
"crossplane-function-sdk-python>=0.14.0",
"click>=8.1.0",
"grpcio>=1.73.1",
"crossplane-models",
Expand Down
2 changes: 1 addition & 1 deletion functions/compose-inference-class/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ description = "Mark an InferenceClass as ready."
requires-python = ">=3.11,<3.14"
license = "Apache-2.0"
dependencies = [
"crossplane-function-sdk-python>=0.13.0",
"crossplane-function-sdk-python>=0.14.0",
"click>=8.1.0",
"grpcio>=1.73.1",
"crossplane-models",
Expand Down
2 changes: 1 addition & 1 deletion functions/compose-inference-cluster/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ description = "Compose an InferenceCluster from a cluster source and serving sta
requires-python = ">=3.11,<3.14"
license = "Apache-2.0"
dependencies = [
"crossplane-function-sdk-python>=0.13.0",
"crossplane-function-sdk-python>=0.14.0",
"click>=8.1.0",
"grpcio>=1.73.1",
"crossplane-models",
Expand Down
2 changes: 1 addition & 1 deletion functions/compose-inference-gateway/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ description = "Compose the control plane routing gateway with Envoy Gateway and
requires-python = ">=3.11,<3.14"
license = "Apache-2.0"
dependencies = [
"crossplane-function-sdk-python>=0.13.0",
"crossplane-function-sdk-python>=0.14.0",
"click>=8.1.0",
"grpcio>=1.73.1",
"crossplane-models",
Expand Down
13 changes: 4 additions & 9 deletions functions/compose-model-cache/function/fn.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,6 @@
# functions set this independently, so they are a contract — change together.
REMOTE_NS = "default"

# The cluster-presence label every InferenceCluster carries; the matcher always
# includes it (an empty match_labels is dropped by protobuf). Mirrors
# compose-model-deployment's cluster matching.
LABEL_KEY_CLUSTER = "modelplane.ai/cluster"
LABEL_VALUE_CLUSTER = "true"

# Hydration container. python:3.11-slim has pip; we install huggingface_hub
# at runtime. A Modelplane-owned image with the tool preinstalled is a
# follow-up (#115).
Expand Down Expand Up @@ -163,10 +157,11 @@ def resolve_inputs(self) -> bool:
Crossplane re-calls the function once it's available. A resolved-but-
empty match flows through (match_clusters() -> NoClusters condition).
"""
match_labels = {LABEL_KEY_CLUSTER: LABEL_VALUE_CLUSTER}
# require_resources with no match field matches every InferenceCluster;
# narrow only when the user sets a clusterSelector.
match_labels = None
if self.xr.spec.clusterSelector and self.xr.spec.clusterSelector.matchLabels:
match_labels.update({k: str(v) for k, v in self.xr.spec.clusterSelector.matchLabels.items()})

match_labels = dict(self.xr.spec.clusterSelector.matchLabels)
response.require_resources(
self.rsp,
name="clusters",
Expand Down
2 changes: 1 addition & 1 deletion functions/compose-model-cache/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ description = "Stage a model onto an RWX PVC on every matched InferenceCluster."
requires-python = ">=3.11,<3.14"
license = "Apache-2.0"
dependencies = [
"crossplane-function-sdk-python>=0.12.0",
"crossplane-function-sdk-python>=0.14.0",
"click>=8.1.0",
"grpcio>=1.73.1",
"crossplane-models",
Expand Down
7 changes: 3 additions & 4 deletions functions/compose-model-cache/tests/test_fn.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def _cluster_dict(name: str, pc: str, *, source: str = "GKE") -> dict:
return {
"apiVersion": "modelplane.ai/v1alpha1",
"kind": "InferenceCluster",
"metadata": {"name": name, "labels": {"modelplane.ai/cluster": "true"}},
"metadata": {"name": name},
"spec": {"cluster": {"source": source, **blocks[source]}},
"status": {"providerConfigRef": {"name": pc}},
}
Expand Down Expand Up @@ -110,13 +110,12 @@ def _req(
return req


# The function always requires every InferenceCluster carrying the cluster
# label, so every response echoes this selector under requirements.
# The function requires every InferenceCluster with a bare selector, so every
# response echoes this selector under requirements.
_CLUSTERS_SELECTOR = fnv1.ResourceSelector(
api_version="modelplane.ai/v1alpha1",
kind="InferenceCluster",
)
_CLUSTERS_SELECTOR.match_labels.labels.update({"modelplane.ai/cluster": "true"})

# The hydration shell script the Job runs (no revision, no auth secret).
_HYDRATE_CMD = (
Expand Down
24 changes: 10 additions & 14 deletions functions/compose-model-deployment/function/fn.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,14 @@
CONDITION_REASON_ALL_REPLICAS_READY = "AllReplicasReady"
CONDITION_REASON_MODEL_STARTING = "ModelStarting"

# Label keys and values. These are coordination contracts: compose-model-replica
# and compose-model-service read labels that this function writes.
# Label keys stamped on the ModelReplicas and ModelEndpoints this function
# composes, identifying the deployment and cluster they belong to.
_LABEL_CLUSTER = "modelplane.ai/cluster"
_LABEL_REPLICA = "modelplane.ai/replica"
_LABEL_DEPLOYMENT = "modelplane.ai/deployment"
# Per-cluster-local index distinguishing co-located replicas of one deployment.
# Read back by the scheduler to reconstruct a replica's (cluster, index)
# identity from observed state. Not an ordering - just a collision breaker.
_LABEL_INDEX = "modelplane.ai/replica-index"
_LABEL_VALUE_TRUE = "true"


# Scheme for gateway-facing URLs. Traffic between the control plane gateway
Expand Down Expand Up @@ -116,27 +114,26 @@ def compose(self):
def resolve_inputs(self):
"""Declare and fetch required resources. Returns False if critical
inputs are missing."""
# InferenceClusters are matched by the modelplane.ai/cluster=true
# label — a workaround for the empty match_labels protobuf bug.
cluster_match_labels: dict[str, str] = {
_LABEL_CLUSTER: _LABEL_VALUE_TRUE,
}
# Match all InferenceClusters by default — require_resources with no
# match field matches every resource of the kind — narrowing only when
# the user sets a clusterSelector.
clusters_match_labels = None
if self.xr.spec.clusterSelector and self.xr.spec.clusterSelector.matchLabels:
cluster_match_labels.update(self.xr.spec.clusterSelector.matchLabels)

clusters_match_labels = dict(self.xr.spec.clusterSelector.matchLabels)
Comment thread
negz marked this conversation as resolved.
response.require_resources(
self.rsp,
name="clusters",
api_version="modelplane.ai/v1alpha1",
kind="InferenceCluster",
match_labels=cluster_match_labels,
match_labels=clusters_match_labels,
)
# Match all ModelReplicas (across all deployments) so the scheduler can
# account for capacity already consumed by other deployments.
response.require_resources(
self.rsp,
name="all-replicas",
api_version="modelplane.ai/v1alpha1",
kind="ModelReplica",
match_labels={_LABEL_REPLICA: _LABEL_VALUE_TRUE},
)

cluster_dicts = request.get_required_resources(self.req, "clusters")
Expand Down Expand Up @@ -213,7 +210,6 @@ def compose_replicas(self, matched):
name=name.replica(self.xr.metadata.name, cluster_info),
namespace=self.xr.metadata.namespace,
labels={
_LABEL_REPLICA: _LABEL_VALUE_TRUE,
_LABEL_DEPLOYMENT: self.xr.metadata.name,
_LABEL_CLUSTER: cluster_info.name,
_LABEL_INDEX: str(cluster_info.index),
Expand Down
2 changes: 1 addition & 1 deletion functions/compose-model-deployment/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ description = "Fan out a ModelDeployment to ModelReplicas and ModelEndpoints."
requires-python = ">=3.11,<3.14"
license = "Apache-2.0"
dependencies = [
"crossplane-function-sdk-python>=0.13.0",
"crossplane-function-sdk-python>=0.14.0",
"click>=8.1.0",
"grpcio>=1.73.1",
"cel-python>=0.3.0",
Expand Down
14 changes: 2 additions & 12 deletions functions/compose-model-deployment/tests/test_fn.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ def _cluster(name: str, *, ready: bool = True, address: str | None = "10.0.0.1",
name="my-model-5ab63",
namespace="ml-team",
labels={
"modelplane.ai/replica": "true",
"modelplane.ai/deployment": "my-model",
"modelplane.ai/cluster": "cluster-a",
"modelplane.ai/replica-index": "0",
Expand Down Expand Up @@ -141,11 +140,10 @@ def _cluster(name: str, *, ready: bool = True, address: str | None = "10.0.0.1",
),
).model_dump(exclude_none=True, mode="json")

# The requirements selectors every want echoes back.
# The requirements selectors every want echoes back. Both are bare selectors
# matching all resources of the kind.
Comment thread
negz marked this conversation as resolved.
_CLUSTER_SEL = fnv1.ResourceSelector(api_version="modelplane.ai/v1alpha1", kind="InferenceCluster")
_CLUSTER_SEL.match_labels.labels.update({"modelplane.ai/cluster": "true"})
_REPLICA_SEL = fnv1.ResourceSelector(api_version="modelplane.ai/v1alpha1", kind="ModelReplica")
_REPLICA_SEL.match_labels.labels.update({"modelplane.ai/replica": "true"})


def _req(xr: dict, *, clusters: list[dict], replicas: list[dict] | None = None, observed: dict | None = None):
Expand Down Expand Up @@ -282,7 +280,6 @@ async def test_compose(self) -> None:
"name": "my-model-5ab63",
"namespace": "ml-team",
"labels": {
"modelplane.ai/replica": "true",
"modelplane.ai/deployment": "my-model",
"modelplane.ai/cluster": "cluster-a",
"modelplane.ai/replica-index": "0",
Expand Down Expand Up @@ -421,7 +418,6 @@ async def test_compose(self) -> None:
"name": "my-model-5ab63",
"namespace": "ml-team",
"labels": {
"modelplane.ai/replica": "true",
"modelplane.ai/deployment": "my-model",
"modelplane.ai/cluster": "cluster-a",
"modelplane.ai/replica-index": "0",
Expand Down Expand Up @@ -452,7 +448,6 @@ async def test_compose(self) -> None:
"name": "my-model-5ab63",
"namespace": "ml-team",
"labels": {
"modelplane.ai/replica": "true",
"modelplane.ai/deployment": "my-model",
"modelplane.ai/cluster": "cluster-a",
"modelplane.ai/replica-index": "0",
Expand Down Expand Up @@ -547,7 +542,6 @@ async def test_compose(self) -> None:
"name": "my-model-5ab63",
"namespace": "ml-team",
"labels": {
"modelplane.ai/replica": "true",
"modelplane.ai/deployment": "my-model",
"modelplane.ai/cluster": "cluster-a",
"modelplane.ai/replica-index": "0",
Expand Down Expand Up @@ -621,7 +615,6 @@ async def test_compose(self) -> None:
"name": "my-model-f0b76",
"namespace": "ml-team",
"labels": {
"modelplane.ai/replica": "true",
"modelplane.ai/deployment": "my-model",
"modelplane.ai/cluster": "cluster-b",
"modelplane.ai/replica-index": "0",
Expand Down Expand Up @@ -716,7 +709,6 @@ async def test_compose(self) -> None:
"name": "my-model-5ab63",
"namespace": "ml-team",
"labels": {
"modelplane.ai/replica": "true",
"modelplane.ai/deployment": "my-model",
"modelplane.ai/cluster": "cluster-a",
"modelplane.ai/replica-index": "0",
Expand Down Expand Up @@ -812,7 +804,6 @@ async def test_compose(self) -> None:
"name": "my-model-5ab63",
"namespace": "ml-team",
"labels": {
"modelplane.ai/replica": "true",
"modelplane.ai/deployment": "my-model",
"modelplane.ai/cluster": "cluster-a",
"modelplane.ai/replica-index": "0",
Expand Down Expand Up @@ -849,7 +840,6 @@ async def test_compose(self) -> None:
"name": "my-model-609c5",
"namespace": "ml-team",
"labels": {
"modelplane.ai/replica": "true",
"modelplane.ai/deployment": "my-model",
"modelplane.ai/cluster": "cluster-a",
"modelplane.ai/replica-index": "1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,6 @@ def _replica(
name=f"{deployment_name}-{cluster_name}-{index}",
namespace="ml-team",
labels={
"modelplane.ai/replica": "true",
"modelplane.ai/deployment": deployment_name,
"modelplane.ai/cluster": cluster_name,
"modelplane.ai/replica-index": str(index),
Expand Down
Loading
Loading