---
title: Weight traffic for heterogeneous clusters
description: Give a workload cluster whose replicas serve more requests a larger share of an alpha multi-cluster InferenceService's traffic, with capacity factors in spec.routing.capacityFactors.
status: preview
since: v1.3
---
A capacity factor gives a workload cluster whose replicas serve more requests a larger share of a multi-cluster [InferenceService](../../concepts/serving/inference-services.md)'s traffic. By default, the service's [TrafficMap](../../concepts/rollouts-and-traffic/traffic-map.md) counts every ready replica the same, which suits clusters that run the same accelerators. In the examples, a replica on `worker-b` serves three times the requests of one on `worker-a`, so `worker-b` gets a factor of 3 in `spec.routing.capacityFactors`.
Multi-cluster routing is alpha, still in development, and off by default. Its API, capacity factors included, can change without notice.
- A control-plane cluster with OME in the namespace `ome`, and `kubectl` access to it. Its `ome-resources` values set `ome.multicluster.enabled=true` and `ome.multicluster.role=control-plane`, as in [Controller manager flags](../../reference/operate-ome/controller-manager-flags.md#multi-cluster), and `ome.multicluster.config.routing.enabled=true`, as in [Turn on routing](../../concepts/rollouts-and-traffic/traffic-map.md#turn-on-routing).
- Two [WorkloadClusters](../../reference/api/ome.v1beta1.md#ome-io-v1beta1-WorkloadCluster) with different accelerators, `worker-a` and `worker-b`.
- The InferenceService `chat` in the namespace `prod` on the control-plane cluster, [eligible for routing](../../concepts/rollouts-and-traffic/traffic-map.md#turn-on-routing), with [`spec.placement.mode`](../../reference/api/ome.v1beta1.md#ome-io-v1beta1-PlacementSpec) `All` or `Split`. Once it's placed on both workload clusters, `kubectl get trafficmap chat -n prod -o jsonpath='{.spec.entries[*].cluster}'` prints `worker-a worker-b`.
- How many requests one replica serves on each workload cluster at the same latency target, measured for example with a [BenchmarkJob](../../concepts/serving/benchmarks.md) against the cluster's endpoint.
## How factors change the weights
OME counts the replicas that can serve on a cluster: the lower of its admitted and its ready replicas. It multiplies the count by the cluster's factor, 1 unless you set one, and reduces the results across clusters to their smallest whole-number ratio.
For example, `chat` has four replicas on `worker-a` and two on `worker-b`, whose replicas each serve three times as many requests:
| Cluster | Replicas that can serve | Factor | Replicas × factor | Weight |
| --- | --- | --- | --- | --- |
| `worker-a` | 4 | 1, the default | 4 | 2 |
| `worker-b` | 2 | 3 | 6 | 3 |
`worker-b` gets three of every five requests with a third of the replicas. Without the factor, the weights would be 2 and 1.
- A factor counts per replica, so a cluster's share still moves as it scales or as its replicas stop being ready.
- Only the ratios between factors matter: `worker-a: "2"` with `worker-b: "6"` gives the same weights as `worker-b: "3"` alone.
- Factors change only the weights. OME ignores them when it places the InferenceService, in mode `Split` too: to change where replicas run, change `spec.placement`.
- A capacity poll, a routing health probe or a drain can lower a cluster's weight, or hold it at 0, whatever its factor. See [How OME sets the weights](../../concepts/rollouts-and-traffic/traffic-map.md#how-ome-sets-the-weights).
## Step 1: Declare capacity factors
OME doesn't derive factors from the hardware: you choose them. Work out a cluster's factor as what one of its replicas serves divided by what one replica serves on your baseline cluster. If a `worker-a` replica sustains 20 requests per second at your latency target and a `worker-b` replica sustains 60, `worker-b`'s factor is 3. A slower cluster gets a factor below 1, such as `"0.5"`. Set factors only for the clusters that differ from the baseline.
Add [`spec.routing.capacityFactors`](../../reference/api/ome.v1beta1.md#ome-io-v1beta1-RoutingSpec) to the InferenceService on the control-plane cluster. Its keys are WorkloadCluster names, as `kubectl get workloadclusters` lists them. Its values are positive, quoted Kubernetes quantities, such as `"3"`, `"1.5"` or `"500m"`. The control-plane cluster accepts 0 or a negative value, but OME then stops updating the TrafficMap, as [The weights don't change](#the-weights-dont-change) shows.
```yaml title="chat.yaml"
apiVersion: ome.io/v1beta1
kind: InferenceService
metadata:
name: chat
namespace: prod
spec:
model:
name: llama-3-1-8b-instruct
runtime:
name: srt-llama-3-1-8b-instruct
engine:
minReplicas: 2
maxReplicas: 4
placement:
policy: ClusterAffinity
mode: All
clusterAffinity:
- matchFields:
- key: metadata.name
operator: In
values: [worker-a, worker-b]
routing:
capacityFactors:
worker-b: "3"
```
In mode `All`, each workload cluster scales on its own, so `worker-a` can run four replicas while `worker-b` runs two.
```bash
kubectl apply -f chat.yaml
```
```output
inferenceservice.ome.io/chat configured
```
OME rebuilds the TrafficMap when the InferenceService changes. Check that it has built the map from your change: the map's `spec.observedISVCGeneration` equals the InferenceService's `metadata.generation`.
```bash
kubectl get inferenceservice chat -n prod -o jsonpath='{.metadata.generation}{"\n"}'
```
```output
6
```
```bash
kubectl get trafficmap chat -n prod -o jsonpath='{.spec.observedISVCGeneration}{"\n"}'
```
```output
6
```
If the map's number stays lower, OME has stopped updating the map: see [The weights don't change](#the-weights-dont-change).
## Step 2: Check the weights
List the entries' replica counts, factors and weights:
```bash
kubectl get trafficmap chat -n prod \
-o jsonpath='{range .spec.entries[*]}{.cluster}{" allocated="}{.capacity.allocated}{" ready="}{.capacity.ready}{" factor="}{.capacity.factor}{" weight="}{.weight}{"\n"}{end}'
```
```output
worker-a allocated=4 ready=4 factor= weight=2
worker-b allocated=2 ready=2 factor=3 weight=3
```
- `allocated` and `ready` are the admitted and ready replicas. An empty value means 0.
- `factor` is the factor you set, in canonical form: `"1.5"` shows as `1500m`. It's empty for `worker-a`, which counts as 1.
- `weight` is the cluster's share, relative to the other entries.
If a weight is 0, or isn't what you expect, compare the entry with [Why a cluster's weight is zero](../../concepts/rollouts-and-traffic/traffic-map.md#why-a-clusters-weight-is-zero).
If you [publish a global endpoint](publish-a-global-endpoint.md), check that the publisher has written these weights with the map's [staleness checks](../../concepts/rollouts-and-traffic/traffic-map.md#staleness-checks).
## Move from the removed field
Older manifests set the factors in `spec.placement.capacityFactors`, which later became a deprecated alias of `spec.routing.capacityFactors`. The alias is gone: the InferenceService API no longer has a `spec.placement.capacityFactors` field. Move the map to `spec.routing`, with the same keys and values:
```diff
spec:
placement:
policy: ClusterAffinity
mode: All
clusterAffinity:
- matchFields:
- key: metadata.name
operator: In
values: [worker-a, worker-b]
- capacityFactors:
- worker-b: "3"
+ routing:
+ capacityFactors:
+ worker-b: "3"
```
A manifest that keeps the old field sets no factors:
- By default, `kubectl apply` fails: kubectl asks the API server for strict validation, which rejects a field the schema doesn't have. See [The API server rejects the InferenceService](#the-api-server-rejects-the-inferenceservice).
- A client that doesn't ask for strict validation gets the field dropped instead: the API server prunes `spec.placement.capacityFactors` as an unknown field and accepts the InferenceService, with at most a warning. Every cluster then counts as 1, and [Step 2](#step-2-check-the-weights) shows an empty `factor` on every entry.
## Troubleshooting
### The weights don't change
After you apply a factor, the weights stay as they were, and the map's `spec.observedISVCGeneration` stays below the InferenceService's `metadata.generation` in [Step 1's check](#step-1-declare-capacity-factors). OME has stopped updating the map, for example because a factor in `spec.routing.capacityFactors` is 0 or negative.
With the `kubectl ome` plugin, `kubectl ome placement explain chat -n prod` shows [`Routing intent`](../../reference/kubectl-ome/placement.md#explain-output-fields) as `Invalid` when a factor is the cause. The command reads alpha features.
The error message shows only in the manager's log. Read every replica's log:
```bash
kubectl logs -n ome -l control-plane=ome-controller-manager --tail=-1 | grep 'validate routing policy'
```
The log shows an error such as `validate routing policy: spec.routing.capacityFactors["worker-b"] must be positive, got "0"`. While it lasts, the TrafficMap and your gateway's route keep their last weights, and the map's `Published` condition turns `False` with reason `InvalidOwner`.
Set a positive factor, or remove the key, and apply the file again. OME then builds the map from the fixed spec. For the other errors that stop the map, see [When the map stops updating](../../concepts/rollouts-and-traffic/traffic-map.md#where-it-comes-from).
### A cluster's factor has no effect
Find the cluster's entry in [Step 2](#step-2-check-the-weights):
| What the entry shows | Cause and fix |
| --- | --- |
| Empty `factor` | The key doesn't match the WorkloadCluster's name, or the factors sit in the removed `spec.placement.capacityFactors` field. OME accepts any key, so compare yours with `kubectl get workloadclusters`, and keep the map in [`spec.routing.capacityFactors`](#move-from-the-removed-field). |
| The only entry, with weight 1 | The InferenceService serves from one workload cluster, which gets all the traffic whatever its factor. |
| Weight 0 | A drain, a probe gate or no ready replicas. See [Why a cluster's weight is zero](../../concepts/rollouts-and-traffic/traffic-map.md#why-a-clusters-weight-is-zero). |
### The API server rejects the InferenceService
`kubectl apply` fails with an error that ends with `unknown field "spec.placement.capacityFactors"`. The manifest sets the removed field, and kubectl's default strict validation has the API server reject a field the InferenceService schema doesn't have. Move the factors to `spec.routing.capacityFactors`, as [Move from the removed field](#move-from-the-removed-field) shows.
## Clean up
To go back to plain replica counts, remove `spec.routing.capacityFactors` from `chat.yaml`, and apply the file again.
## Next steps
- [Traffic map](../../concepts/rollouts-and-traffic/traffic-map.md): the inputs of a weight, and how to tell whether the table is current.
- [Tune routing capacity polling](tune-routing-capacity-polling.md): lower a cluster's count to the capacity that it reports.
- [Configure routing health probes](routing-health-probes.md): take a cluster's traffic away when its endpoint fails.
- [Drain a workload cluster](drain-a-workload-cluster.md): hold a cluster at weight 0 for maintenance.
- [Publish a global endpoint](publish-a-global-endpoint.md): write the weights into your global gateway.