Replace the scaling metric with an occupancy ratio (in-flight / served requests) - #89
Open
kondratyevd wants to merge 5 commits into
Open
Replace the scaling metric with an occupancy ratio (in-flight / served requests)#89kondratyevd wants to merge 5 commits into
kondratyevd wants to merge 5 commits into
Conversation
The default KEDA/Envoy load metric was mean inference queue time per batch
execution. It is non-linear in load (near zero once capacity is adequate,
explosive near saturation) and its scale depends on the model mixture, so a
single threshold cannot serve different deployments: in A/B runs on Geddes
it caused a premature mid-run scale-down under high load and six minutes of
under-scaling at moderate load.
The new default metric estimates the number of replicas the current
in-flight work needs, via Little's law (L = rate of a cumulative time
counter):
R_needed = L_envoy / max(L_service / R_healthy, 1)
L_envoy in-flight requests between Envoy and Triton
(rate of envoy_cluster_upstream_rq_time_sum, ms -> /1e3)
L_service requests actively executing across all models and pods
(rate of request-duration minus queue-duration, us -> /1e6)
R_healthy Triton endpoints Envoy routes to (max of
envoy_cluster_membership_healthy across Envoy pods)
Every term is a time integral, so models are weighted by the time they
consume and the metric carries no model-specific constants; it reads ~1
with no queueing and grows linearly with the overload factor. KEDA now
consumes it as metricType AverageValue (desired = ceil(metric/threshold),
independent of pods that never scheduled); the Envoy rate limiter compares
the per-replica form against a separate serverAdmissionThreshold so that
new clients are only rejected when scaling can no longer keep up, not at
the normal operating point. serverLoadThreshold now defaults to 2
(tolerated sojourn inflation); the rate() window is configurable via
serverLoadRateInterval (keep >= 4x the Prometheus scrape interval).
A custom serverLoadMetric is still used verbatim by both consumers, with
keda.metricType available to restore the old Value semantics.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Prometheus Operator install floats on whatever prometheus-community/kube-prometheus-stack is latest. 90.0.0, published between 2026-09-05 and 2026-09-07, made the control-plane ServiceMonitors authenticate via a Secret that is only rendered when prometheus.enabled and prometheus.serviceAccount.create are both true. These call sites pass prometheus.enabled=false -- they want the operator and its CRDs, nothing else -- so templating now fails: The control-plane ServiceMonitors authenticate by default with the Secret created by prometheus.serviceAccount.createTokenSecret, which is only rendered when prometheus.enabled and prometheus.serviceAccount.create are also true. Bisected: 89.2.4 and every earlier release template fine with these flags; 90.0.0 is the first that does not. Pinning 89.2.4 keeps CI reproducible and matches how the chart's own dependencies are pinned. 89.2.4 still ships the servicemonitors CRD and the operator Deployment, which is all these steps need. The alternative -- tracking latest and disabling every control-plane exporter (kubelet, kubeApiServer, kubeControllerManager, kubeScheduler, kubeProxy, kubeEtcd, coreDns) -- also works on 90.0.0 but adds seven flags to each call site and would not protect against the next upstream change. This break is independent of this branch: it fails identically on main, which has not run CI since 90.0.0 was published. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kondratyevd
force-pushed
the
occupancy-ratio-metric
branch
from
September 8, 2026 20:42
b916525 to
444bdc3
Compare
Trim the helper header, values comments, and configuration-guide section to what a reader needs: what each term measures, what the floors do, and how the thresholds are consumed. Drop the derivation asides and the repeated interpretation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Replaces the default KEDA/Envoy scaling metric (mean inference queue time per batch execution, threshold 100) with an occupancy ratio: the mean number of requests in flight between Envoy and the Triton fleet, divided by the fleet's serving occupancy. The metric estimates how many replicas the current in-flight work needs, so the autoscaler can size the fleet in one proportional step.
Why
The queue-latency metric is non-linear in load and its scale depends on the model mixture:
The occupancy ratio fixed both failure modes in back-to-back A/B runs (same jobs, same cluster): equal or lower GPU-minutes, −7–8% wall time, −17–21% p90 latency, steady fixed-point behaviour at both 5 replicas (capacity-capped) and 3 replicas (free choice).
How the PromQL query is constructed
Little's law (
L = λW) turns cumulative time counters into mean occupancies:rate(<cumulative time counter>)is "seconds of request time per second of wall clock", i.e. the mean number of requests inside that system. The query combines three measured quantities:L_envoysum(rate(envoy_cluster_upstream_rq_time_sum{...triton_grpc_service}[1m])) / 1e3(counter in ms)L_servicesum(rate(nv_inference_request_duration_us − nv_inference_queue_duration_us)) / 1e6R_healthymax(envoy_cluster_membership_healthy{...})(max across Envoy pods — they report the same cluster)and renders the extensive form
clamp_min(v, s)is PromQL formax(v, s). The two floors are physical, not tuning: each healthy replica can execute at least one request concurrently (serving capacity is never below the replica count — this is what makes scale-down work at low load), and at zero replicas the metric degrades to "requests in flight" rather than dividing by zero (never+Inf, which KEDA would read as "scale to max"). Because every term is a time integral, models are weighted by the time they consume — the metric contains no model-specific constants and reads the same for any mixture: ~1 when nothing queues, ~2 when requests wait as long as they are served, linear in the overload factor above that.Consumers
metricType: AverageValue:desired = ceil(R_needed / serverLoadThreshold)— independent of pods that never scheduled (with the oldValuetype, pending pods inflate the request; observed as 3 unschedulable pods held for 10 minutes in testing).serverLoadThresholddefaults to 2 (tolerated sojourn inflation; 1.5 trades GPUs for latency).R_needed / R_healthy) against a newserverAdmissionThreshold(default 3). It is deliberately above the scaling threshold: the autoscaler settles the system nearserverLoadThreshold, so gating admission at the same value would reject new clients during normal operation.serverLoadRateInterval(default1m) exposes therate()window — keep it ≥ 4× the Prometheus scrape interval; shorter windows double the noise without detecting load faster (measured: cv 18% vs 8% at 30s vs 1m, same threshold-crossing sample).Compatibility
serverLoadMetricis still used verbatim by both consumers;keda.metricType: Valuerestores the old HPA semantics for per-replica custom metrics.serverLoadThresholdchanges 100 → 2 with the new default metric; values files that only setserverLoadThreshold: 100(without a custom metric) should drop it — the example values files invalues/are updated accordingly.envoy.enabled, the default) and Prometheus scraping of both Envoy and Triton, which the bundled Prometheus config already does.Validation
helm templateverified for: defaults,values-minimal-full.yaml(scale-from-zero CI values),values-geddes-cms.yaml, custom-metric override, and the external-Envoy-config path;helm lintpasses for every file invalues/.promtool query instant.🤖 Generated with Claude Code