Chapter 6. Validate inference workload changes with controlled deployment
You can use controlled deployment with Distributed Inference with llm-d to validate engine upgrades, model version rotations, and configuration changes on a fraction of production traffic before committing to a full rollout. You deploy two or more versions of an inference workload side by side in the same routing group, control the traffic fraction each version receives using declarative weight-based traffic splitting, and compare per-version Prometheus metrics in real time.
6.1. Controlled deployment for inference workloads Copy linkLink copied to clipboard!
You can use controlled deployment with Distributed Inference with llm-d to validate engine upgrades, model version rotations, and configuration changes on a fraction of production traffic before committing to a full rollout. Controlled deployment replaces all-or-nothing updates with a declarative, reversible workflow that avoids the risk of degrading inference quality for all users when a new version performs worse than expected.
Controlled deployment uses a symmetric group-and-weight model. Each LLMInferenceService independently declares the routing group it belongs to and its proportional share of traffic. There is no distinction between a "baseline" and a "canary" version: all members of a routing group are peers that share weighted traffic distribution through a Gateway API HTTPRoute.
6.1.1. How the group and weight model works Copy linkLink copied to clipboard!
You assign a routing group by setting the spec.router.route.group field on each LLMInferenceService that participates in traffic splitting. You set the proportional traffic share by configuring the spec.router.route.weight field. The controller translates declared weights into Gateway API HTTPRoute weighted backendRef entries by using proportional semantics.
Weights are ratios, not percentages. There is no requirement that weights sum to 100. For example, if version A has weight 9 and version B has weight 1, the gateway distributes approximately 90% of traffic to version A and 10% to version B. You can use any integer values between 0 and 1,000,000 to express the required ratio. A weight of 0 removes the member from new request routing after gateway propagation completes. In-flight requests and requests arriving during the configuration propagation window can still reach the member. The propagation window is typically seconds, with no guaranteed upper bound. The backendRef remains in the HTTPRoute at weight 0 rather than being removed.
6.1.2. Version isolation Copy linkLink copied to clipboard!
Each version in a controlled deployment is a fully independent LLMInferenceService resource with its own runtime pods, InferencePool, and inference scheduler instance. This per-version isolation ensures that a failed version cannot affect the other version’s scheduling, KV cache state, or resource allocation.
Running multiple versions requires provisioning separate compute resources, including GPU nodes, for each version.
6.1.3. Per-member HTTPRoute lifecycle Copy linkLink copied to clipboard!
Each LLMInferenceService in a routing group creates and owns its own HTTPRoute. Every member’s HTTPRoute carries weighted backendRef entries for all active, non-stopped members of the group that serve the same model. Gateway API precedence rules select the oldest matching route as the active route for traffic decisions.
The controller uses finalizers to manage HTTPRoute lifecycle safely. When you delete a version, the controller removes the corresponding backendRef from peer members' HTTPRoutes before allowing the deletion to proceed. A member’s own HTTPRoute is deleted when the member itself is removed.
6.1.4. Observability Copy linkLink copied to clipboard!
Per-version metric collection requires creating PodMonitor and ServiceMonitor resources for each version. A PodMonitor with a relabeling rule injects an llm_isvc_name label into vLLM metrics, enabling cross-version comparison of inference health metrics such as time to first token, throughput, error rate, and queue depth. For more information about configuring per-version observability, see Monitor per-version inference metrics during controlled deployment.
6.1.5. Controlled deployment lifecycle Copy linkLink copied to clipboard!
A controlled deployment follows four phases:
-
Deploy: Create a second
LLMInferenceServicewith the same routing group name as the production version. Start with a low weight, such as 1, so only a small fraction of traffic reaches the new version. -
Observe: Compare per-version inference metrics, such as time to first token, throughput, error rate, and queue depth, using Prometheus queries filtered by
llm_isvc_name. Use the metrics comparison to make an evidence-based promotion or rollback decision. - Promote or roll back: Shift traffic by adjusting weights on both versions. To promote, set the old version weight to 0. To roll back, set the new version weight to 0 and restore the original version weight.
- Clean up: After promotion, optionally force-stop the old version to reclaim GPU resources. Delete the old version only after verifying that all group members have reconciled.
Promotion by weight change and deletion by resource cleanup are separate operations. You can separate them by hours or days, keeping the old version available for rollback while it receives no traffic.
6.1.6. Constraints Copy linkLink copied to clipboard!
The following constraints apply to controlled deployments:
- All members of a routing group must be in the same namespace.
- Controlled deployment requires Gateway API mode. Ingress mode is not supported.
-
Controlled deployment requires controller-managed routes. Custom
HTTPRouterefs, also known as bring-your-own (BYO) routes, are not supported with traffic splitting. -
All members of a routing group must have the same authentication posture: the
security.opendatahub.io/enable-authannotation must have the same value on every member. A mismatch produces anAuthPostureMismatchwarning event.
6.1.7. Model access patterns Copy linkLink copied to clipboard!
When you deploy multiple versions in a routing group, clients can access the model through different URL patterns:
- Per-participant path
-
A direct URL to a specific
LLMInferenceServiceinstance, following the pattern/_<namespace>_/_<llmisvc-name>_. This path is version-specific and changes when you rotate versions. - Publisher path
-
A stable, version-independent URL that follows the pattern
/publishers/_<namespace>_/models/_<model-name>_. This path survives version rotations because it references the model name rather than theLLMInferenceServiceinstance name. Publisher paths require model-level RBAC authorization. - Model-routing header
- A request-level header that routes traffic to a specific model through the gateway scheduler. Model-routing uses the model name for routing decisions and does not require a version-specific URL.
6.1.8. Scope limitations Copy linkLink copied to clipboard!
The following capabilities are not supported in controlled deployment:
- Automated promotion or rollback based on metric thresholds. All promotion and rollback decisions are manual.
- A/B testing with user segmentation or cookie-based session persistence.
- KV cache migration between versions.
- Prefill-decode disaggregated controlled deployment. Although the routing group controller does not block disaggregated workloads, this combination has not been validated.
6.2. Deploy a canary version of an inference workload Copy linkLink copied to clipboard!
You can deploy a canary version of an inference workload alongside an existing production version to test changes on a fraction of live traffic. By assigning both versions to the same routing group and configuring their weights, you control how much traffic each version receives while avoiding an all-or-nothing rollout.
Prerequisites
-
You have a production
LLMInferenceServicedeployed and serving traffic. -
Your
LLMInferenceServiceuses Gateway API mode with controller-managed routes. Ingress mode and customHTTPRouterefs, also known as bring-your-own (BYO) routes, are not supported with traffic splitting. - You have sufficient compute resources, such as GPUs, to run two versions of the inference workload simultaneously.
- Prometheus metrics collection is enabled for your Distributed Inference with llm-d deployment.
- You have access to the OpenShift CLI (`oc`).
Procedure
Add the
groupandweightfields to your existing productionLLMInferenceServiceif they are not already set.Open your production
LLMInferenceServicemanifest and addspec.router.route.groupandspec.router.route.weight:apiVersion: serving.kserve.io/v1alpha2 kind: LLMInferenceService metadata: name: <production-llmisvc-name> namespace: <namespace> spec: model: name: <model-name> uri: <model-uri> router: route: group: <group-name> weight: 9 gateway: {} scheduler: {} # ...existing spec fields...where:
_<production-llmisvc-name>_-
Specifies the name of your existing production
LLMInferenceService. _<namespace>_- Specifies the namespace where the production deployment runs.
_<model-name>_- Specifies the model name.
_<model-uri>_- Specifies the URI from where the model is downloaded.
_<group-name>_-
Specifies the routing group name. Use 1 to 63 lowercase alphanumeric characters or hyphens, matching the pattern
^[a-z0-9]([a-z0-9-]*[a-z0-9])?$.
Apply the updated manifest:
$ oc apply -f <production-llmisvc-file>.yamlCreate a manifest for the canary
LLMInferenceServicewith the same routing group name as the production version, starting at weight 1:apiVersion: serving.kserve.io/v1alpha2 kind: LLMInferenceService metadata: name: <canary-llmisvc-name> namespace: <namespace> spec: model: name: <model-name> uri: <canary-model-uri> replicas: <replica-count> router: route: group: <group-name> weight: 1 gateway: {} scheduler: {} template: containers: - name: main resources: limits: nvidia.com/gpu: "<gpu-count>" # ...resource configuration...where:
_<canary-llmisvc-name>_- Specifies a name for the canary version that is distinct from the production version name.
_<canary-model-uri>_- Specifies the model URI for the canary version, such as a new model revision or engine build.
_<group-name>_- Specifies the same routing group name used by the production version.
_<model-name>_- Specifies the model name for the canary version. In a typical canary deployment, use the same model name as the production version.
ImportantStart the canary weight at a small nonzero value such as 1 rather than 0. Deploying at weight 0 and then increasing the weight later can cause transient connection failures while the backend warms up. A small nonzero weight ensures the backend cluster is warmed immediately with minimal traffic. With a production weight of 9 and a canary weight of 1, approximately 10% of traffic goes to the canary version.
Apply the canary manifest:
$ oc apply -f <canary-llmisvc-file>.yamlWait for the
GroupReadycondition to becomeTrue:$ oc wait llminferenceservice <canary-llmisvc-name> -n <namespace> \ --for=condition=GroupReady --timeout=300sVerify the routing group membership by listing all
LLMInferenceServiceresources in the namespace:$ oc get llminferenceservice -n <namespace> \ -o custom-columns=NAME:.metadata.name,GROUP:.status.router.group.name,WEIGHT:.spec.router.route.weight,READY:.status.conditions[?(@.type=="GroupReady")].statusThe output lists all group members with their configured weights and
GroupReadystatus:NAME GROUP WEIGHT READY _<production-llmisvc-name>_ _<group-name>_ 9 True _<canary-llmisvc-name>_ _<group-name>_ 1 TrueVerify that both versions belong to the same routing group and that
GroupReadyisTrueon both.Adjust the traffic split to shift more traffic to the canary by updating the weight fields on both versions.
For example, to shift to a 50/50 split:
$ oc patch llminferenceservice <production-llmisvc-name> -n <namespace> \ --type merge -p {"spec":{"router":{"route":{"weight":5}}}} $ oc patch llminferenceservice <canary-llmisvc-name> -n <namespace> \ --type merge -p {"spec":{"router":{"route":{"weight":5}}}}
Verification
Verify that the canary version is serving inference requests:
$ curl -s -H "Authorization: Bearer <token>" \ "$(oc get llminferenceservice <canary-llmisvc-name> -n <namespace> \ -o jsonpath={.status.url})/v1/models"A successful response confirms that the canary version is ready to handle traffic.
All members of a routing group must have matching security.opendatahub.io/enable-auth annotation values. If the authentication posture does not match across members, the controller emits an AuthPostureMismatch warning event. Check for mismatch events by running:
$ oc get events -n <namespace> --field-selector reason=AuthPostureMismatch
6.3. Monitor per-version inference metrics during controlled deployment Copy linkLink copied to clipboard!
You can compare inference metrics across versions during a controlled deployment to make evidence-based promotion or rollback decisions. Per-version filtering requires a PodMonitor that injects an llm_isvc_name label into vLLM metrics through Prometheus relabeling.
Prerequisites
- Prometheus metrics collection is enabled for your Distributed Inference with llm-d deployment.
-
You have an active controlled deployment with two or more
LLMInferenceServiceversions in the same routing group. - You have access to the OpenShift web console or a Prometheus-compatible query interface.
- You have access to the OpenShift CLI (`oc`).
Procedure
Create a
PodMonitorto scrape vLLM metrics with per-version labeling.vLLM exposes metrics on port 8000, but its
model_namelabel is the same across versions that serve the same model. To distinguish versions, thePodMonitorcopies the pod’sapp.kubernetes.io/namelabel into anllm_isvc_namemetric label:apiVersion: monitoring.coreos.com/v1 kind: PodMonitor metadata: name: llmisvc-vllm namespace: <namespace> spec: selector: matchLabels: kserve.io/component: workload podMetricsEndpoints: - portNumber: 8000 path: /metrics interval: 10s params: format: - prometheus relabelings: - sourceLabels: [__meta_kubernetes_pod_label_app_kubernetes_io_name] targetLabel: llm_isvc_nameApply the
PodMonitor:$ oc apply -f <podmonitor-file>.yamlVerify that per-version metrics are available in Prometheus by checking for distinct
llm_isvc_namevalues:vllm:request_success_total{llm_isvc_name=~"<v1-name>|<v2-name>"}Replace
_<v1-name>_and_<v2-name>_with theLLMInferenceServicenames for each version. If thellm_isvc_namelabel is missing, verify that thePodMonitoris active and that the relabeling rule is correct.Verify that the actual traffic distribution matches your configured weights by comparing request rates across versions:
sum by(llm_isvc_name) ( rate(vllm:request_success_total{llm_isvc_name=~"<v1-name>|<v2-name>"}[5m]) )The ratio of request rates between versions should approximate the ratio of configured weights. For example, a 9:1 weight split should produce approximately 90% and 10% of total requests per version.
Compare time to first token across versions by running the following PromQL query in the OpenShift web console or your Prometheus interface:
histogram_quantile(0.95, sum by(le, llm_isvc_name) ( rate(vllm:time_to_first_token_seconds_bucket{llm_isvc_name=~"<v1-name>|<v2-name>"}[5m]) ) )Compare additional key metrics using PromQL queries filtered by
llm_isvc_name.The following table lists recommended metrics for promotion decisions.
Table 6.1. Key metrics for cross-version comparison Metric PromQL query pattern Generation throughput
sum by(llm_isvc_name) (rate(vllm:generation_tokens_total{llm_isvc_name=~"_<v1-name>_|_<v2-name>_"}[5m]))Request success rate
sum by(llm_isvc_name) (rate(vllm:request_success_total{llm_isvc_name=~"_<v1-name>_|_<v2-name>_"}[5m]))Queue depth
sum by(llm_isvc_name) (vllm:num_requests_waiting{llm_isvc_name=~"_<v1-name>_|_<v2-name>_"})End-to-end latency (P95)
histogram_quantile(0.95, sum by(le, llm_isvc_name) (rate(vllm:e2e_request_latency_seconds_bucket{llm_isvc_name=~"_<v1-name>_|_<v2-name>_"}[5m])))KV cache utilization
avg by(llm_isvc_name) (vllm:kv_cache_usage_perc{llm_isvc_name=~"_<v1-name>_|_<v2-name>_"})If end-to-end latency is higher than expected, isolate whether the cause is a capacity issue or an engine bottleneck by comparing queue time against prefill and decode time:
Table 6.2. Latency breakdown queries Component PromQL query pattern Queue time (P95)
histogram_quantile(0.95, sum by(le, llm_isvc_name) (rate(vllm:request_queue_time_seconds_bucket{llm_isvc_name=~"_<v1-name>_|_<v2-name>_"}[5m])))Prefill time (P95)
histogram_quantile(0.95, sum by(le, llm_isvc_name) (rate(vllm:request_prefill_time_seconds_bucket{llm_isvc_name=~"_<v1-name>_|_<v2-name>_"}[5m])))Decode time (P95)
histogram_quantile(0.95, sum by(le, llm_isvc_name) (rate(vllm:request_decode_time_seconds_bucket{llm_isvc_name=~"_<v1-name>_|_<v2-name>_"}[5m])))If queue time is significantly higher than prefill and decode time combined, the version needs more replicas. If prefill or decode time is high but queue time is low, the latency is an engine-level bottleneck rather than a capacity issue.
Based on the metric comparison, decide whether to promote or roll back.
Table 6.3. Decision criteria for controlled deployment Signal Promote Roll back Error rate
Equal to or lower than baseline
Higher than baseline
Time to first token
Within acceptable threshold of baseline
Significantly higher than baseline
End-to-end latency
Within acceptable threshold of baseline
Significantly higher than baseline
Queue depth
Stable and not growing
Growing, indicating the version cannot handle its traffic share
KV cache utilization
Within expected bounds
Approaching capacity or significantly higher than baseline
Pod health
All pods running and ready
Pod restarts, readiness failures, or reduced pod count
Automated promotion or rollback based on metric thresholds is not supported. All promotion and rollback decisions during controlled deployment are manual. Use the metrics comparison to inform your decision, then follow the promotion or rollback procedures.
6.4. Promote or roll back a controlled deployment Copy linkLink copied to clipboard!
After you deploy a canary version and compare per-version inference metrics, you can promote the canary to full traffic or roll back to the previous version. You can also force-stop a version to reclaim GPU resources, and delete old versions after successful promotion.
Prerequisites
- You have deployed a canary version as described in Deploy a canary version of an inference workload.
-
The
GroupReadycondition isTrueon both versions. - You have access to the OpenShift CLI (`oc`).
Procedure
To promote the canary version, set the production version weight to 0 so that all traffic goes to the canary:
$ oc patch llminferenceservice <production-llmisvc-name> -n <namespace> \ --type merge -p {"spec":{"router":{"route":{"weight":0}}}}The production version pods remain active but idle. Because the production version still exists in the routing group at weight 0, you can roll back by restoring its weight.
To roll back during a controlled deployment, restore the original version weight and set the canary version weight to 0:
$ oc patch llminferenceservice <production-llmisvc-name> -n <namespace> \ --type merge -p {"spec":{"router":{"route":{"weight":9}}}} $ oc patch llminferenceservice <canary-llmisvc-name> -n <namespace> \ --type merge -p {"spec":{"router":{"route":{"weight":0}}}}NoteRollback options depend on the state of the original version:
- If the original version still exists with weight 0, adjust weights back to restore traffic.
-
If the original version was stopped with the
serving.kserve.io/stopannotation, remove the annotation and restore the weight. - If the original version was deleted, you must redeploy it from the original manifest.
Optional: To reclaim GPU resources from a version that is no longer receiving traffic, force-stop it by applying the
serving.kserve.io/stopannotation:$ oc annotate llminferenceservice <llmisvc-name> -n <namespace> \ serving.kserve.io/stop=trueA stopped member remains in the routing group but is omitted from
backendRefentries in peer members'HTTPRoutes, regardless of thespec.router.route.weightvalue. The memberstoppedfield instatus.router.group.membersis set totrue.To resume a stopped version, remove the annotation:
$ oc annotate llminferenceservice <llmisvc-name> -n <namespace> \ serving.kserve.io/stop-To delete an old version after successful promotion, delete the
LLMInferenceServiceresource:$ oc delete llminferenceservice <old-llmisvc-name> -n <namespace>WarningDo not delete a version that has a weight greater than 0. Deleting a member with active traffic causes
500errors for in-flight requests until the remaining group members reconcile. Always set the weight to 0 and verify that theGroupReadycondition reflects the updated group membership before deleting.The control plane holds the deletion until peer members remove the corresponding
backendReffrom theirHTTPRoutes, so in-flight requests to other members are not disrupted.
Verification
After promotion or rollback, verify the routing group state:
$ oc get llminferenceservice -n <namespace> \ -o custom-columns=NAME:.metadata.name,GROUP:.status.router.group.name,WEIGHT:.spec.router.route.weight,READY:.status.conditions[?(@.type=="GroupReady")].statusAfter force-stopping a version, verify that the member shows as stopped by querying a peer member that is still running in the same routing group:
$ oc get llminferenceservice <peer-llmisvc-name> -n <namespace> \ -o jsonpath={.status.router.group.members} | jq .The peer’s
status.router.group.memberslists the stopped member with"stopped": true. Theweightfield reports the member’s declaredspec.router.route.weightvalue.NoteQuery a peer member, not the stopped service itself. The stopped service’s own
status.routeris cleared when the stop annotation is applied.
The following table summarizes each lifecycle operation, its precondition, and expected outcome.
| Operation | Command | Precondition | Expected outcome |
|---|---|---|---|
| Promote canary |
|
Both versions at | All traffic goes to canary. Old version pods remain active but idle. |
| Roll back |
| Old version still exists in group | All traffic returns to old version. Canary pods remain active but idle. |
| Force-stop |
| Version weight is 0 |
GPU resources are reclaimed. Member remains in group with |
| Delete |
|
Version weight is 0 and | Resource removed. Control plane holds deletion until peers reconcile. |
6.5. Authorization model for controlled deployment Copy linkLink copied to clipboard!
When you deploy multiple versions of an inference workload in a routing group, you can choose between two RBAC authorization patterns: instance-level access that targets a specific LLMInferenceService by name, or model-level access that targets a model name shared across all routing group members. Model-level access provides a stable authorization identity that does not change when you rotate versions.
6.5.1. Instance-level and model-level access patterns Copy linkLink copied to clipboard!
Instance-level RBAC uses SubjectAccessReview (SAR) checks against the serving.kserve.io/llminferenceservices resource with the get verb. The resource name is the LLMInferenceService name extracted from the per-participant path URL. This pattern works for standalone deployments, but breaks during version rotations because the LLMInferenceService name changes when you create a new version.
Model-level RBAC uses SAR checks against the serving.opendatahub.io/models virtual resource with the post verb. The model name is extracted from the publisher-path URL pattern /publishers/_<namespace>_/models/_<model-name>_. Because the model name is defined by spec.model.name, which is the same across all routing group members, access grants survive version rotations without requiring per-instance RBAC updates for every new version.
| Access pattern | URL pattern | SAR resource | SAR verb |
|---|---|---|---|
| Per-participant (instance-level) |
|
|
|
| Publisher path (model-level) |
|
|
|
6.5.2. The models virtual resource Copy linkLink copied to clipboard!
The models resource in the serving.opendatahub.io API group is a virtual resource. It has no backing Custom Resource Definition (CRD), and oc get models.serving.opendatahub.io returns a "not found" error. This is by design: the resource exists only as a SAR authorization contract that the controller-managed AuthPolicy evaluates at request time.
The resourceNames values used in RBAC roles for model-level access follow the publisher-path format: publishers/_<namespace>_/models/_<model-name>_. These are path-shaped strings, not standard Kubernetes resource names.
6.5.3. Cross-tenant deny rule Copy linkLink copied to clipboard!
The controller creates an AuthPolicy rule named deny-misrouted-model-header that blocks requests meeting all of the following conditions:
-
The path is not a publisher path: the path does not start with
/publishers/. -
The path is not a batch path: the path does not start with
/v1/filesor/v1/batches. - A valid model routing header is present in the request.
This rule prevents a request from being routed by one tenant’s model header while being authorized by a different tenant’s per-participant path identity. The deny rule fires at priority 0, before any SAR-based authorization rules, and returns an immediate 403 Forbidden response without an API server round-trip.
6.5.4. Backward compatibility Copy linkLink copied to clipboard!
Model-level RBAC is additive. Existing per-participant path RBAC continues to work unchanged. You can use instance-level access for standalone deployments and model-level access for routing groups, or both patterns together in the same cluster.
Namespace viewers, editors, and administrators automatically receive model-level access through ClusterRole aggregation. The kserve-models-view ClusterRole aggregates to view, kserve-models-edit aggregates to edit, and kserve-models-admin aggregates to admin. No additional RBAC configuration is needed for users who already have namespace-level access.
6.5.5. Known limitations Copy linkLink copied to clipboard!
-
Model names containing a literal
/v1/segment are not supported because the authorization rule extracts the model name from the URL path using/v1/as a delimiter. -
Direct
/v1/inference endpoints with a model routing header are denied by thedeny-misrouted-model-headerrule. This is a planned follow-up for backend-based routing (BBR) integration.
6.5.6. Grant model-level access for inference workloads Copy linkLink copied to clipboard!
You can grant model-level RBAC access so that inference consumers can use publisher-path URLs to access models without needing per-instance RBAC updates on every version rotation. Model-level access uses the serving.opendatahub.io/models virtual resource for authorization, providing a stable identity that survives version changes in routing groups.
Namespace viewers, editors, and administrators automatically receive model-level access through ClusterRole aggregation without additional configuration. You only need to create explicit Role and RoleBinding resources when you want to grant model-level access to users or service accounts that do not have namespace-level roles.
Prerequisites
-
Authentication is enabled on your
LLMInferenceServiceresources. To verify, runoc get llminferenceservice _<name>_ -n _<namespace>_ -o jsonpath='{.metadata.annotations.security\.opendatahub\.io/enable-auth}'and confirm the result istrueor empty, which indicates the default enabled state. For more information, see Enable authentication and authorization for an LLM inference service. - You have access to the OpenShift CLI (`oc`).
-
You have
adminorcluster-adminpermissions on the namespace where theLLMInferenceServiceresources are deployed.
Procedure
Determine whether to use model-level or instance-level RBAC for your access pattern.
Table 6.6. When to use each RBAC pattern Access pattern Use model-level RBAC Use instance-level RBAC Controlled deployment with version rotations
Yes. Publisher-path URLs reference the model name, which is stable across versions.
Not recommended. Per-participant URLs reference the
LLMInferenceServicename, which changes with each new version.Standalone deployment with a single version
Optional. Model-level access works for standalone deployments.
Yes. Per-participant URLs are stable when there is no version rotation.
Batch processing with delegation
Yes. Use the
delegatesub-resource for batch processor service accounts.Not recommended. The batch processor should use model-level access for controlled deployments.
Create a
Rolethat grantspostaccess to themodelsresource in the target namespace:apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: model-access namespace: <namespace> rules: - apiGroups: - serving.opendatahub.io resources: - models verbs: - postThis grants access to all models in the namespace.
NoteThe
postverb and themodelsresource in theserving.opendatahub.ioAPI group are virtual constructs enforced by the gateway authorization policy, not standard Kubernetes API resources. Runningoc auth can-i post models.serving.opendatahub.ioproduces warnings about an unknown verb and resource type. These warnings are expected. To verify that model-level RBAC is configured correctly, send an authenticated inference request as shown in the Verification section.To restrict access to specific models, add a
resourceNamesfield with publisher-qualified model names:rules: - apiGroups: - serving.opendatahub.io resources: - models verbs: - post resourceNames: - "publishers/<namespace>/models/<model-name>"Create a
RoleBindingto bind the role to the target user or service account:apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: model-access-binding namespace: <namespace> subjects: - kind: User name: <username> apiGroup: rbac.authorization.k8s.io roleRef: kind: Role name: model-access apiGroup: rbac.authorization.k8s.ioApply the Role and RoleBinding:
$ oc apply -f <role-file>.yaml $ oc apply -f <rolebinding-file>.yamlOptional: To grant delegation access for batch processing, create an additional Role that includes the
delegatesub-resource:apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: model-access-delegate namespace: <namespace> rules: - apiGroups: - serving.opendatahub.io resources: - models/delegate verbs: - post-delegateBind this role to the batch processor service account. Regular users who lack the
post-delegatepermission cannot forward requests on behalf of other users.
Verification
Send an authenticated request to a publisher-path URL to verify access:
$ curl -H "Authorization: Bearer <token>" \ https://<gateway-url>/publishers/<namespace>/models/<model-name>/v1/modelsA
200 OKresponse confirms that model-level RBAC is configured correctly. A403 Forbiddenresponse indicates that the user or service account lacks the requiredpostpermission on themodelsresource. A500 Internal Server Errorresponse might indicate that theLLMInferenceServicebackend is not ready rather than an RBAC misconfiguration. Before troubleshooting RBAC, verify that theLLMInferenceServiceshowsReady=Trueby runningoc get llminferenceservice _<name>_ -n _<namespace>_.
6.6. Controlled deployment API fields Copy linkLink copied to clipboard!
You can configure controlled deployment by using the spec.router.route.group and spec.router.route.weight fields on LLMInferenceService resources. The controller uses these fields to manage Gateway API HTTPRoute weighted backendRef entries for proportional traffic splitting across routing group members.
6.6.1. Spec fields for controlled deployment Copy linkLink copied to clipboard!
The following fields are available on the LLMInferenceService resource under spec.router.route.
| Field | Type | Required | Description |
|---|---|---|---|
|
| string | Yes, when using traffic splitting |
The routing group name that this |
|
| int32 | Yes, when using traffic splitting |
The proportional traffic share for this member. Follows Gateway API |
6.6.2. Admission validation rules Copy linkLink copied to clipboard!
The admission webhook validates the following cross-field constraints when you create or update an LLMInferenceService with traffic splitting fields.
| Configuration | Validation result |
|---|---|
|
Neither |
Valid. The |
|
|
Rejected. The error message states that |
|
|
Rejected. The error message states that |
|
Both | Valid, if no conflicting route configuration exists. |
|
| Rejected. Traffic splitting requires controller-managed routes. Custom HTTPRoute refs are not supported. |
6.6.3. Status fields for routing groups Copy linkLink copied to clipboard!
The controller reports the observed routing group state in status.router.group.
| Field | Type | Description |
|---|---|---|
|
| string | The name of the routing group this member belongs to. |
|
| array | A list of all observed group members, including their names, weights, stopped status, and resolved backend references. |
|
| string |
The name of the group member |
|
| int32 |
The declared traffic weight for this member, as set in |
|
| boolean |
Set to |
|
| object |
The resolved |
6.6.4. Status conditions for routing groups Copy linkLink copied to clipboard!
The controller sets the following conditions to report routing group health.
| Condition | Description |
|---|---|
|
|
Set to |
|
|
Set to |
6.6.5. Weight semantics Copy linkLink copied to clipboard!
Weights follow Gateway API proportional semantics as defined in Gateway Enhancement Proposal (GEP) 718:
- Weights are ratios, not percentages. A weight of 9 and a weight of 1 produce a 90/10 traffic split, identical to a weight of 90 and a weight of 10.
- There is no constraint that weights must sum to 100 or any other value.
-
A weight of 0 removes the member from new request routing after gateway propagation completes, but in-flight requests can still reach the member during the propagation window. The
backendRefremains in theHTTPRouteat weight 0 rather than being removed. This is useful for completing the promotion lifecycle before deleting a version. - The valid range is 0 to 1,000,000.
6.6.6. Status addresses and URL patterns Copy linkLink copied to clipboard!
The status.addresses field reports the network endpoints where the service is reachable. Each address entry includes a URL and can include an origin reference identifying the networking resource that produced it.
When an LLMInferenceService participates in a routing group, the following URL patterns are relevant:
| URL pattern | Description | Stable across version rotations |
|---|---|---|
|
|
Per-participant path. Routes to a specific | No. The URL changes when the instance name changes. |
|
| Publisher path. Routes to the model by name through weighted traffic splitting. | Yes. The URL references the model name, which is the same across all routing group members. |
Publisher-path URLs require model-level RBAC authorization. For more information, see Authorization model for controlled deployment.
6.7. Controlled deployment metrics for promotion decisions Copy linkLink copied to clipboard!
You can use the following vLLM Prometheus metrics to compare inference performance across versions during a controlled deployment. Per-version filtering uses the llm_isvc_name label, which is injected by the PodMonitor relabeling rule described in Monitor per-version inference metrics during controlled deployment.
6.7.1. Key metrics for cross-version comparison Copy linkLink copied to clipboard!
The following table lists the vLLM metrics most relevant to promotion and rollback decisions during controlled deployment. Filter each query by llm_isvc_name to isolate per-version data.
| Metric name | Type | Description | PromQL pattern for cross-version comparison |
|---|---|---|---|
|
| histogram | Time from request receipt to the first generated token. |
|
|
| counter | Total number of generated tokens. |
|
|
| counter | Total number of successful inference requests. Use this metric to compare request success throughput across versions. |
|
|
| gauge | Number of requests waiting in the inference queue. |
|
|
| histogram | End-to-end inference request latency, from request receipt to final response. |
|
|
| gauge | Percentage of KV cache currently in use. |
|
|
| histogram | Time a request spent waiting in the queue before processing began. Compare against prefill and decode time to determine whether high latency is a capacity issue or an engine bottleneck. |
|
|
| histogram | Time spent on the prefill phase of a request. |
|
|
| histogram | Time spent on the decode phase of a request. |
|
Replace _<v1-name>_ and _<v2-name>_ with the LLMInferenceService names for each version.
Additional resources
6.8. Model-level RBAC roles for inference access Copy linkLink copied to clipboard!
OpenShift AI provides aggregate ClusterRoles that grant model-level access for publisher-path URLs. These ClusterRoles aggregate into the standard Kubernetes admin, edit, and view ClusterRoles so that namespace-level users automatically receive model-level access.
6.8.1. Aggregate ClusterRoles Copy linkLink copied to clipboard!
The following aggregate ClusterRoles are available for model-level inference access.
| ClusterRole | Verbs granted | Aggregates to | Description |
|---|---|---|---|
|
|
Inherits from |
|
Grants full model access. Aggregation role that inherits all rules from |
|
|
|
| Grants model inference access via publisher-path URLs. Users can send inference requests to models. |
|
|
|
|
Grants model inference access via publisher-path URLs. The |
Both kserve-models-edit and kserve-models-view grant the post verb. The models virtual resource has no distinct read semantics because accessing a model through a publisher-path URL is an inference action.
6.8.2. Virtual resource details Copy linkLink copied to clipboard!
The authorization rules evaluate against the following virtual resource.
| Property | Value |
|---|---|
| API group |
|
| Resource |
|
| Sub-resource |
|
| Verbs |
|
| Backing Custom Resource Definition (CRD) | None. This is a SubjectAccessReview (SAR)-only authorization contract. |
6.8.3. AuthPolicy rules for model access Copy linkLink copied to clipboard!
The controller generates the following AuthPolicy rules for LLMInferenceService resources with authentication enabled. When authentication is not set to anonymous, the policy applies at the Gateway level for all LLMInferenceService resources as a global rule.
| Rule name | Fires when | SAR resource | SAR verb | Priority |
|---|---|---|---|---|
|
|
Path starts with |
|
| 1 |
|
|
Path starts with |
|
| 1 |
|
|
All of the following conditions are true: the path does not match the publisher-path pattern | N/A (immediate deny) | N/A | 0 |
|
|
Path has at least 2 segments, does not start with |
|
| 1 |
|
|
Same conditions as |
|
| 1 |
The model routing header is x-gateway-model-name by default. You can override the header name per-cluster by setting ingress.modelBasedRoutingHeaderName in the inferenceservice-config ConfigMap. A valid header value matches the pattern ^publishers/[^/]+/models/.+$.
6.8.4. Publisher-path URL format Copy linkLink copied to clipboard!
Publisher-path URLs follow this pattern:
/publishers/<namespace>/models/<model-name>/v1/<api-endpoint>
For example, the chat completions endpoint for a model named RedHatAI/Qwen3-8B-FP8-dynamic in the my-project namespace has the following publisher-path URL:
/publishers/my-project/models/RedHatAI/Qwen3-8B-FP8-dynamic/v1/chat/completions
The model name is extracted from the URL path segment between /models/ and /v1/. Multi-segment model names such as RedHatAI/Qwen3-8B-FP8-dynamic are supported.
Additional resources
6.9. Troubleshooting controlled deployment Copy linkLink copied to clipboard!
If your controlled deployment is not working as expected, use the following troubleshooting steps to diagnose and resolve common issues.
Traffic is not splitting between versions:
Verify that the
HTTPRoutehas weighted backend references for both versions:$ oc get httproute -n <namespace> -o yamlIf only one backend reference is displayed, check that both
LLMInferenceServiceresources declare the samespec.router.route.groupvalue and that both havespec.router.route.weightset.Verify that both versions are ready:
$ oc get llminferenceservice -n <namespace>A version that is not ready does not receive traffic even if its weight is nonzero.
The
GroupReadycondition isFalse:Check the condition details for the reason:
$ oc get llminferenceservice <llmisvc-name> -n <namespace> \ -o jsonpath={.status.conditions[?(@.type=="GroupReady")]}Common reasons:
-
FinalizationPending: A group member is being deleted and finalization has not completed. Wait for the deletion to finish. -
If the condition is absent, verify that the
groupandweightfields are set on theLLMInferenceService.
-
The
GroupDegradedcondition showsMemberDivergence:Group members declare different
spec.model.namevalues. The controller excludes divergent members from the traffic split and continues routing with matching members.Verify the model names on all group members:
$ oc get llminferenceservice -n <namespace> \ -o custom-columns=NAME:.metadata.name,MODEL:.spec.model.name,GROUP:.spec.router.route.groupUpdate the divergent member’s
spec.model.nameto match the other group members.The canary version is not receiving traffic:
Check that the canary weight is greater than 0:
$ oc get llminferenceservice <canary-llmisvc-name> -n <namespace> \ -o jsonpath={.spec.router.route.weight}Verify that the canary version has ready pods. Check the
LLMInferenceServicestatus:$ oc get llminferenceservice <canary-llmisvc-name> -n <namespace> \ -o jsonpath={.status.workloads.primary.readyReplicas}Or list the pods directly:
$ oc get pods -n <namespace> -l app.kubernetes.io/name=<canary-llmisvc-name>If the version has zero ready pods, the gateway does not route traffic to it. Check the pod status for errors:
$ oc get pods -n <namespace> -l app.kubernetes.io/name=<canary-llmisvc-name>,app.kubernetes.io/part-of=llminferenceserviceThe
HTTPRoutewas not created:Controlled deployment requires Gateway API routing. Verify that you are not using Ingress-based routing or custom
HTTPRoutereferences:$ oc get llminferenceservice <llmisvc-name> -n <namespace> -o yaml | grep -A5 routerThe
spec.routersection should includeroute.groupandroute.weightand should not includeingressorroute.http.refs.Check the KServe controller logs for errors:
$ oc logs -n redhat-ods-applications deployment/kserve-controller-manager --tail=50The old version still receives traffic after setting its weight to 0:
Active requests on the old version complete before traffic fully shifts. Wait for in-flight requests to finish.
Verify that the
HTTPRoutereflects the updated weights:$ oc get httproute -n <namespace> -o yamlIf weights are not updated, check for errors in the KServe controller logs:
$ oc logs -n redhat-ods-applications deployment/kserve-controller-manager --tail=50A stopped version is unreachable after removing the stop annotation:
After removing the
serving.kserve.io/stopannotation, the version must become ready before it receives traffic. Wait for the version to become ready:$ oc wait llminferenceservice <llmisvc-name> -n <namespace> \ --for=condition=Ready --timeout=300sIf the version does not become ready, check the pod status for errors:
$ oc get pods -n <namespace> -l app.kubernetes.io/name=<llmisvc-name>,app.kubernetes.io/part-of=llminferenceserviceValidation error when creating an
LLMInferenceServicewithgrouporweight:Common validation failures:
-
weight requires grouporgroup requires weight: Both fields must be specified together. -
spec.router.route.group in body should match: The value must be a DNS-compatible label: lowercase alphanumeric characters or hyphens, 1 to 63 characters, starting and ending with an alphanumeric character. -
spec.router.route.weight in body should be less than or equal to 1000000: The weight must be in the range 0 to 1,000,000. -
traffic splitting requires Gateway API routing: Controlled deployment requires controller-managed Gateway API routes, not Ingress routing. -
traffic splitting cannot be used with custom HTTPRoute refs: Use controller-managed routes instead of customHTTPRoutereferences.
-