Skip to content

Provision InferenceClusters on UpCloud - #594

Open
haarchri wants to merge 8 commits into
modelplaneai:mainfrom
haarchri:feature/upcloud-provider
Open

haarchri wants to merge 8 commits into
modelplaneai:mainfrom
haarchri:feature/upcloud-provider

Conversation

@haarchri

@haarchri haarchri commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Description of your changes

Modelplane couldn't provision clusters on UpCloud. This adds source: UpCloud to InferenceCluster, backed by a new namespaced UpCloudCluster XR and a compose-upcloud-cluster function on crossplane-contrib/provider-upcloud v1.0.2. An UpCloudCluster provisions a UKS cluster on a dedicated SDN network (UKS requires one per cluster, DHCP on, default route off), a fixed system node group, and one KubernetesNodeGroup per pool. UKS has no inline pools, so unlike Civo and Vultr the system pool is itself a Ready-gated composed resource, and KubernetesNodeGroup has no name field, so each group's external name is pinned to its pool name to keep UKS node group names deterministic.

The cloud-specific decisions, and why:

  • Credentials. The UKS kubeconfig embeds static client certificates, so the ClusterProviderConfig carries no identity (same as Vultr and Civo). fetchKubeconfig: true is required or the provider publishes no kubeconfig to the connection secret. The provider's credentials Secret is JSON, {"token": ...} or {"username": ..., "password": ...}.
  • GPU stack. UKS GPU node images pre-install the NVIDIA driver and container toolkit at the default root, so the hand-written serving-stack half matches Nebius's shape: NFD plus the DRA driver, no GPU Operator. That also means no DCGM exporter, so modelplane_gpu_* and energy series are empty on UpCloud — the same gap Nebius has.
  • Autoscaling. UKS has no server-side autoscaler and the cluster autoscaler's UpCloud cloud provider is an unmerged upstream PR (feat(upcloud): add UpCloud cloudprovider kubernetes/autoscaler#10234), so pools with maxNodeCount are scaled by UpCloud's fork image (ghcr.io/upcloudltd/autoscaler) installed through the upstream chart with image/env overrides. UpCloud provisions no API credentials on the cluster, so spec.autoscaler names a token Secret that an Object copies to kube-system; provider-kubernetes resolves the copy server-side through patchesFrom, so the token never passes through the function. Two tradeoffs stated plainly: the newest fork image is built for Kubernetes 1.29, older than any UKS version this XR provisions (it talks to the UpCloud API rather than version-skewed Kubernetes APIs, but the skew is outside the autoscaler's support policy), and the fork enforces a minimum of one node per group, so no scale-to-zero. A CEL rule requires the autoscaler Secret when any pool sets maxNodeCount, failing at admission rather than on the composed XR where the user wouldn't see it.
  • ModelCache. None. UpCloud File Storage is managed NFS without a CSI driver, so volumes can only be provisioned statically; the docs and cache_storage_class docstring name the gap.
  • Fabric. Not mapped; UKS has no EFA or InfiniBand analog.
  • Zones. GPU plans are currently fi-hel2 only, with account-gated quota; the docs say so.
    Known rough edge: a pool named system on an UpCloudCluster silently replaces the injected system node group (they share the node-pool-system composition key). The other clouds can't hit this because their system pools are inline on the cluster resource. Left unguarded for now; worth deciding in review whether a CEL reservation is wanted.

No breaking changes; UpCloud is additive to the source, provider and cloud enums.

Validation: nix flake check passes (unit tests for compose-upcloud-cluster, compose-inference-cluster and compose-serving-stack, type checks, docs-manifest validation, Vale).

Fixes #

I have:

  • Read and followed Modelplane's contribution process.
  • Run nix flake check (or ./nix.sh flake check) and made sure it passes.
  • Added or updated tests covering any composition function changes.
  • Signed off every commit with git commit -s.

Tested

KUBECONFIG=upcloud.txt kubectl get pods -A                                                 
NAMESPACE                 NAME                                                              READY   STATUS    RESTARTS      AGE
cert-manager              mp-cert-manager-6c654b75b8-4j9lw                                  1/1     Running   0             25m
cert-manager              mp-cert-manager-cainjector-6f97bd496-k6npf                        1/1     Running   0             25m
cert-manager              mp-cert-manager-webhook-667c68bdb5-q6qwf                          1/1     Running   0             25m
envoy-ai-gateway-system   ai-gateway-controller-fbf6db65-znpj9                              1/1     Running   3 (25m ago)   25m
envoy-gateway-system      envoy-gateway-76c8656c8b-9j26d                                    1/1     Running   0             24m
envoy-gateway-system      envoy-modelplane-system-cluster-gateway-b26a0919-7d5865465lhksh   2/2     Running   0             21m
envoy-gateway-system      envoy-modelplane-system-inference-gateway-5dbfd6c0-8fdcbc9rwn24   3/3     Running   0             13m
envoy-gateway-system      envoy-modelplane-system-inference-gateway-5dbfd6c0-8fdcbc9zgbll   3/3     Running   0             13m
kube-system               cilium-7bz96                                                      1/1     Running   0             27m
kube-system               cilium-envoy-2fvmc                                                1/1     Running   0             27m
kube-system               cilium-envoy-5jxmv                                                1/1     Running   0             27m
kube-system               cilium-envoy-zbdqh                                                1/1     Running   0             28m
kube-system               cilium-operator-5b88b57d6d-7fk84                                  1/1     Running   0             30m
kube-system               cilium-operator-5b88b57d6d-sprvb                                  1/1     Running   0             30m
kube-system               cilium-tcxzb                                                      1/1     Running   0             28m
kube-system               cilium-x4zjm                                                      1/1     Running   0             27m
kube-system               coredns-7645c8648f-jgljj                                          1/1     Running   0             30m
kube-system               coredns-7645c8648f-nkgx6                                          1/1     Running   0             30m
kube-system               csi-upcloud-node-77cff                                            2/2     Running   0             27m
kube-system               csi-upcloud-node-lhqn9                                            2/2     Running   0             27m
kube-system               csi-upcloud-node-rkvsd                                            2/2     Running   0             28m
kube-system               csi-upcloud-snapshot-controller-75f44cbf75-dr96d                  1/1     Running   0             29m
kube-system               csi-upcloud-snapshot-controller-75f44cbf75-wqk56                  1/1     Running   0             29m
kube-system               konnectivity-agent-cqjpj                                          1/1     Running   0             27m
kube-system               konnectivity-agent-qsq6f                                          1/1     Running   0             28m
kube-system               konnectivity-agent-xr2dm                                          1/1     Running   0             27m
kube-system               upcloud-fi-hel2-df1e896d5b12-upcloud-cluster-autoscaler-84bxtg6   1/1     Running   3 (25m ago)   28m
lws-system                mp-lws-controller-manager-7db95fdfb6-t9wms                        1/1     Running   0             25m
modelplane-system         trust-manager-756bd996f4-s4pk8                                    1/1     Running   0             23m
monitoring                mp-kube-prometheus-stack-kube-state-metrics-6845b99bf8-75trm      1/1     Running   0             25m
monitoring                mp-kube-prometheus-stack-prometheus-node-exporter-4zvfn           1/1     Running   0             25m
monitoring                mp-kube-prometheus-stack-prometheus-node-exporter-dgdbq           1/1     Running   0             25m
monitoring                mp-kube-prometheus-stack-prometheus-node-exporter-sv55k           1/1     Running   0             25m
monitoring                prometheus-operator-594578d747-zlsx4                              1/1     Running   0             25m
monitoring                prometheus-prometheus-prometheus-0                                2/2     Running   0             25m
mp-ml-team-51733          qwen-demo-18490-epp-c4f8c958b-q9pnf                               1/1     Running   0             17m
mp-ml-team-51733          qwen-demo-18490-qwen-1abe1-7698bf8f-nl9j2                         1/1     Running   0             17m
node-feature-discovery    mp-node-feature-discovery-gc-d884d7585-nbnlb                      1/1     Running   0             25m
node-feature-discovery    mp-node-feature-discovery-master-7fc9fb989c-tnp6f                 1/1     Running   0             25m
node-feature-discovery    mp-node-feature-discovery-worker-lfb67                            1/1     Running   0             25m
node-feature-discovery    mp-node-feature-discovery-worker-mmcxb                            1/1     Running   0             25m
node-feature-discovery    mp-node-feature-discovery-worker-vwj5q                            1/1     Running   0             25m
nvidia-dra-driver         dra-driver-nvidia-gpu-kubelet-plugin-bd6l8                        1/1     Running   0             25m

curl -sS "http://lb-0ae251bcbc7a49bba5f26922b77bfcc0-1.upcloudlb.com/v1/chat/completions" \
  -H "Content-Type: application/json" \
  -d '{"model":"ml-team/qwen","messages":[{"role":"user","content":"Say Hello from UpCloud"}],"max_tokens":100}'
{"id":"chatcmpl-50c6e3ce-37f2-4d03-9650-27527ea00bdb","object":"chat.completion","created":1793302134,"model":"ml-team/qwen-demo","choices":[{"index":0,"message":{"role":"assistant","content":"Hello! Welcome to UpCloud. How can I assist you today?","refusal":null,"annotations":null,"audio":null,"function_call":null,"tool_calls":[],"reasoning":null},"logprobs":null,"finish_reason":"stop","stop_reason":null,"token_ids":null,"routed_experts":null}],"service_tier":null,"system_fingerprint":"vllm-0.23.0-7f04f88c","usage":{"prompt_tokens":34,"total_tokens":49,"completion_tokens":15,"prompt_tokens_details":null},"prompt_logprobs":null,"prompt_token_ids":null,"prompt_text":null,"kv_transfer_params":null}% 

@github-actions

Copy link
Copy Markdown

Docs preview: https://modelplane-docs-pr-594.vercel.app (ready once the site's Content workflow finishes)

@dennis-upbound dennis-upbound left a comment

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.

compose-upcloud-cluster: removing spec.autoscaler orphans a running autoscaler (should-fix)
The ProviderConfigs, the token Object and the Release are composed only while spec.autoscaler is set. When a user turns their last autoscaled pool into a fixed pool and drops autoscaler (which the CEL rule then allows), all three leave desired state. The Release and Object exclude Delete. The autoscaler keeps running with its old group list, and its node counts fight the nodeCount Crossplane now enforces on those pools. compose_cluster_autoscaler's docstring already handles this for the last pool going fixed. Composing the sub-block when spec.autoscaler or "release-cluster-autoscaler" in observed would cover this case the same way, with an empty group list.

Pool named system (should-fix)
On this one I'd reserve the name with CEL. The PR body says the other clouds can't hit it, but EKS and Nebius key their groups nodegroup-{name} next to nodegroup-system, and GKE uses nodepool-system, so they have the same collision. One rule on InferenceCluster spec.nodePools[].name would cover every cloud. Here the external name is also system, so a declared system pool would take over the system group in UKS as well.

Merge order with #556 (FYI)
_node_pool and compose_upcloud_cluster hard-code role="GPU". Once #556 merges, UpCloud needs the same changes the other six clouds got there: the role derived from accelerator, draDrivers tags on its stack and CPU pools.

Before ready: the checklist is unchecked, and Tested has no live UKS run yet (the e2e-clouds (upcloud) leg is KWOK).

@haarchri

Copy link
Copy Markdown
Collaborator Author

found one issue in the e2e run: UpCloudLtd/upcloud-cloud-controller-manager#14

@haarchri

Copy link
Copy Markdown
Collaborator Author

because of the LB issue - i updated the code to set annotations to override the default behaviour until UpCloudLtd/upcloud-cloud-controller-manager#3 gets shipped, disk size configurable like the other clouds because the default is to low for models and added dcgm exporter

@haarchri
haarchri marked this pull request as ready for review October 29, 2026 19:52
Signed-off-by: Christopher Haar <christopher.haar@upbound.io>
Modelplane can't provision clusters on UpCloud. This adds a namespaced
UpCloudCluster XR and a compose-upcloud-cluster function that provision
a UKS cluster on a dedicated SDN network, with a fixed system node
group and a KubernetesNodeGroup per user pool. UKS has no inline pools,
so unlike Civo and Vultr the system pool is its own Ready-gated node
group. KubernetesNodeGroup has no name field, so the function pins each
group's external name to its pool name, keeping UKS node group names
deterministic.

The UKS kubeconfig embeds static client certificates, so the connection
secret alone reaches the cluster. fetchKubeconfig must be true or the
provider publishes no kubeconfig at all.

UKS has no server-side autoscaler and the cluster autoscaler's UpCloud
cloud provider is an unmerged upstream PR (kubernetes/autoscaler#10234),
so pools with maxNodeCount are scaled by UpCloud's fork image installed
through the upstream chart. UpCloud provisions no API credentials on
the cluster, so spec.autoscaler names a token Secret that an Object
copies to kube-system, resolved server-side through patchesFrom so the
token never passes through the function. The newest fork image is built
for Kubernetes 1.29, older than any UKS version this XR provisions; it
talks to the UpCloud API rather than version-skewed Kubernetes APIs,
but the skew is outside the autoscaler's support policy.

No ModelCache storage is composed: UpCloud File Storage is managed NFS
without a CSI driver, so it only supports statically provisioned
volumes.

Signed-off-by: Christopher Haar <christopher.haar@upbound.io>
An InferenceCluster could not be provisioned on UpCloud. This wires the
new UpCloudCluster XR into the platform: source: UpCloud on
InferenceCluster (zone, version, credentials and the autoscaler's token
Secret), an upcloud provisioning block on InferenceClass naming the GPU
server plan, and a hand-written UpCloud serving-stack half.

UKS GPU node images ship the NVIDIA driver and container toolkit at the
default root, so the half matches Nebius's shape: NFD and the DRA
driver, no GPU Operator. With no operator there is no DCGM exporter
either, so the modelplane_gpu_* and energy series are empty on UpCloud,
the same gap Nebius has.

The UKS kubeconfig embeds static client certificates, so the
ClusterProviderConfig carries no identity. No cache StorageClass is
relayed: UpCloud File Storage is NFS without a CSI driver. Fabric is
not mapped; UKS has no EFA or InfiniBand analog.

A CEL rule requires the upcloud.autoscaler Secret reference when a pool
sets maxNodeCount, failing at admission rather than on the composed
UpCloudCluster's own validation, where the user wouldn't see it.

The upcloud e2e leg fakes GPU nodes the way compose-upcloud-cluster
shapes them and audits the stack's placement on them.

Signed-off-by: Christopher Haar <christopher.haar@upbound.io>
UpCloud wasn't in the providers table or any of the provisioning docs.
This adds the UKS row and tabs across the platform and getting-started
pages, the UpCloud manifests they embed, and the API-reference example.
The getting-started path includes a model deployment, which the other
recent clouds still lack.

The docs state what UpCloud doesn't support: no ModelCache (File
Storage is NFS without a CSI driver, so only static volumes), no
fabric, no GPU metrics (no GPU Operator, so no DCGM exporter), and GPU
plans in fi-hel2 only with account-gated quota. Pools with maxNodeCount
need the autoscaler token Secret, which the concepts manifest shows how
to create.

Signed-off-by: Christopher Haar <christopher.haar@upbound.io>
Signed-off-by: Christopher Haar <christopher.haar@upbound.io>
Signed-off-by: Christopher Haar <christopher.haar@upbound.io>
Signed-off-by: Christopher Haar <christopher.haar@upbound.io>
Signed-off-by: Christopher Haar <christopher.haar@upbound.io>
@haarchri
haarchri force-pushed the feature/upcloud-provider branch from 04b6b69 to 1cb4fe7 Compare October 30, 2026 06:15

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants