Bring your own autoscaler Unreleased: coming in v1.3

Turn off OME's autoscaler for a component with the External class, and scale it with your own autoscaler through the scaleTargetRef that OME publishes.

Set a component's autoscaler.class to External to scale it with an autoscaler that you run: a HorizontalPodAutoscaler or KEDA ScaledObject that you write, or a controller of your own. OME then creates no autoscaler for the component, and publishes the object for yours to scale in status.components.<component>.scaleTargetRef. The autoscaler field and the autoscaler and scaleTargetRef status fields are alpha, so they can change between releases, but they need no feature flag. On v1.2.2, the ome.io/autoscalerClass: external annotation stops OME from creating a HorizontalPodAutoscaler for a RawDeployment InferenceService.

Step 1: Declare the class

Set class: External in the component's autoscaler block. Save this InferenceService as chat.yaml:

chat.yaml
apiVersion: ome.io/v1beta1
kind: InferenceService
metadata:
  name: chat
  namespace: prod
spec:
  deploymentMode: OMENative
  model:
    name: llama-3-2-3b-instruct
  runtime:
    name: srt-llama-3-2-3b-instruct
  engine:
    minReplicas: 2
    autoscaler:
      class: External

External needs no other settings. OME creates the InferenceReplica with the engine's minReplicas, 2 Instances here, and then your autoscaler sets the count. A RawDeployment component starts with 1 pod, whatever its minReplicas. On a running component, OME deletes the autoscaler that it created, and the count stays put until yours changes it. Add the same block under decoder or router for any other component that your autoscaler scales.

To hand off the engine of all the InferenceServices that use a runtime and set no autoscaling of their own, put the block in the runtime's engineConfig instead. To hold a component at a fixed count, use class: None.

Apply the file:

Shell
kubectl apply -f chat.yaml
Output
inferenceservice.ome.io/chat created

Check the class that OME applied to the engine:

Shell
kubectl get inferenceservice chat -n prod -o jsonpath='{.status.components.engine.autoscaler}'
Output
{"class":"External","managedBy":"external","specSource":"isvc"}

specSource: isvc says that the setting came from the InferenceService. An inline autoscaler block wins over all other autoscaling settings, as Which setting wins shows.

Step 2: Read the published scale target

Read the object to scale from the engine's status:

Shell
kubectl get inferenceservice chat -n prod -o jsonpath='{.status.components.engine.scaleTargetRef}'
Output
{"apiVersion":"ome.io/v1beta1","kind":"InferenceReplica","name":"chat-engine"}

The target depends on the component's deployment mode:

Deployment mode Scale target What spec.replicas counts
OMENative The InferenceReplica <InferenceService>-<component>, in ome.io/v1beta1 Instances. An Instance is one pod, or a leader pod and its workers.
RawDeployment The Deployment <InferenceService>-<component>, in apps/v1 Pods

Your autoscaler writes the count to the target's scale subresource, as kubectl scale does, and OME adds or removes Instances to match. Write the count only that way: OME owns the rest of the InferenceReplica's spec and overwrites other changes to it.

Step 3: Point your autoscaler at the target

Whichever autoscaler you run:

  • Point one autoscaler at each target. Two overwrite each other's counts.
  • Set the bounds in your autoscaler. OME passes minReplicas and maxReplicas only to the autoscalers that it creates.
  • On OMENative, keep your autoscaler's minimum at 1 or more. OME sets a count of 0 back to minReplicas, or to 1 when minReplicas is unset or 0.

This HorizontalPodAutoscaler keeps the engine's CPU use at 70%, with 3 to 8 Instances. It scales on CPU to keep the example simple, and a KEDA ScaledObject can scale on the engine's own metrics from Prometheus instead. Save it as chat-engine-byo.yaml:

chat-engine-byo.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: chat-engine-byo
  namespace: prod
spec:
  scaleTargetRef:
    apiVersion: ome.io/v1beta1
    kind: InferenceReplica
    name: chat-engine
  minReplicas: 3
  maxReplicas: 8
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

For a RawDeployment component, target apiVersion: apps/v1, kind: Deployment and name: chat-engine instead. Apply the file:

Shell
kubectl apply -f chat-engine-byo.yaml
Output
horizontalpodautoscaler.autoscaling/chat-engine-byo created

The HorizontalPodAutoscaler needs no extra permissions. It raises a count below its minReplicas at once, so it takes the engine from 2 Instances to 3. It's named chat-engine-byo because OME gives chat-engine to its own HorizontalPodAutoscaler under class: HPA, and a clash would block the hand-back.

Put the target from Step 2 in your ScaledObject's spec.scaleTargetRef: apiVersion: ome.io/v1beta1, kind: InferenceReplica and name: chat-engine. On OMENative, set its minReplicaCount to 1 or more, because it defaults to 0. To have OME create the ScaledObject instead, use class: KEDA.

Your controller needs to read the InferenceService, to find the target, and to update the target's scale subresource, which takes a rule of its own. This Role and RoleBinding grant both to the service account chat-scaler in prod. Save them as chat-scaler-rbac.yaml:

chat-scaler-rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: chat-scaler
  namespace: prod
rules:
  - apiGroups: ["ome.io"]
    resources: ["inferenceservices"]
    verbs: ["get"]
  - apiGroups: ["ome.io"]
    resources: ["inferencereplicas/scale"]
    verbs: ["get", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: chat-scaler
  namespace: prod
subjects:
  - kind: ServiceAccount
    name: chat-scaler
    namespace: prod
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: chat-scaler

For a RawDeployment component, grant deployments/scale in the apps API group instead. Apply the file:

Shell
kubectl apply -f chat-scaler-rbac.yaml
Output
role.rbac.authorization.k8s.io/chat-scaler created
rolebinding.rbac.authorization.k8s.io/chat-scaler created

Then run your controller as chat-scaler, in place of the HorizontalPodAutoscaler.

Step 4: Check the scaling

List the InferenceReplica, whose short name is irep:

Shell
kubectl get inferencereplica chat-engine -n prod
Output
NAME          COMPONENT   DESIRED   CURRENT   READY   AVAILABLE   AGE
chat-engine   engine      3         3         2       2           25m

DESIRED is the count that your autoscaler last wrote: here, the HorizontalPodAutoscaler's minimum of 3, not the engine's minReplicas of 2. CURRENT counts the Instances that exist, and READY and AVAILABLE catch up as the new Instance starts. For a RawDeployment component, kubectl get deployment chat-engine -n prod shows the same in pods. OME doesn't copy your autoscaler's decisions into its status, so read them from your autoscaler.

Hand scaling back to OME

To hand a component back to OME, delete your autoscaler first. Then set another class, or remove the autoscaler block so that the next source in Which setting wins applies.

Troubleshooting

Your count doesn't last

Something other than your autoscaler sets the count. Read the class that OME applied, which points to the cause:

Shell
kubectl get inferenceservice chat -n prod -o jsonpath='{.status.components.engine.autoscaler.class}'
  • None: when minReplicas is 1 or more, as it is by default, OME sets the count back to it. Set class: External.
  • HPA or KEDA: OME's own autoscaler scales the component too. Put the autoscaler block under the component that your autoscaler targets. specSource in the same status says where the class came from.
  • External: a second autoscaler targets the same object, or yours writes 0 to an OMENative component. Keep one autoscaler per target, with a minimum of 1 or more.

scaleTargetRef is empty

The Step 2 command prints nothing for a MultiNode (deprecated) component, which has no scale target: move it to OMENative. Otherwise, OME hasn't set up the component yet, or setting it up failed. Look for a Reconciler error line in the controller logs:

Shell
kubectl logs -n ome -l control-plane=ome-controller-manager --tail=-1

Fix the error that it names, and OME retries on its own.

Switching back to HPA or KEDA fails

A switch to class: HPA or class: KEDA fails when an object of yours has the name that OME uses for its own: OME won't take it over. For this engine, those names are chat-engine for a HorizontalPodAutoscaler and scaledobject-chat-engine for a ScaledObject. Each attempt logs a Reconciler error line that names the object and says it is not controlled by expected owner. Delete or rename your object, and OME creates its own on the next attempt.

Clean up

Delete the HorizontalPodAutoscaler and the InferenceService:

Shell
kubectl delete -f chat-engine-byo.yaml -f chat.yaml
Output
horizontalpodautoscaler.autoscaling "chat-engine-byo" deleted from prod namespace
inferenceservice.ome.io "chat" deleted from prod namespace

Delete your ScaledObject, then the InferenceService:

Shell
kubectl delete -f chat.yaml
Output
inferenceservice.ome.io "chat" deleted from prod namespace

Stop your controller. Then delete the Role and RoleBinding, and the InferenceService:

Shell
kubectl delete -f chat-scaler-rbac.yaml -f chat.yaml
Output
role.rbac.authorization.k8s.io "chat-scaler" deleted from prod namespace
rolebinding.rbac.authorization.k8s.io "chat-scaler" deleted from prod namespace
inferenceservice.ome.io "chat" deleted from prod namespace

Next steps