fix(flowcontrol): add a stock-EPP metric source to the budget cascade - #370
Merged
Conversation
shimib
requested review from
RishabhSaini,
ahg-g,
evacchi and
jtechapps
as code owners
July 28, 2026 22:58
shimib
added a commit
that referenced
this pull request
Jul 28, 2026
Signed-off-by: Shimi Bandiel <shimib@google.com>
shimib
added a commit
that referenced
this pull request
Jul 29, 2026
Signed-off-by: Shimi Bandiel <shimib@google.com>
shimib
force-pushed
the
fix/budget-primary-metric-source
branch
from
July 29, 2026 21:14
291e35b to
37f15e6
Compare
The prometheus-budget gate's primary source queries
inference_extension_flow_control_queue_size, which only exists when EPP
runs the flow control plugin. The shipped llm-d-router-endpoint-picker
does not enable it and has no flag to turn it on, so on a documented
llm-d install source 0 never resolves and the cascade falls straight
through to the vLLM source — which needs scrape-time relabeling to carry
an inference_pool label that vLLM does not emit natively. When that is
not configured either, every evaluation silently returns the fallback
budget.
Insert inference_pool_per_pod_queue_size as a middle tier:
1 - (avg by(name)(inference_pool_per_pod_queue_size{name="..."}) / max_concurrency)
It is part of EPP's base metric set, so a stock install resolves here.
Averaging over pods is what lets max_SYS = ready_pods x max_concurrency
reduce to max_concurrency, so no inference_pool_ready_pods join is
needed — which matters, because EPP's metrics refresh returns early at
zero pods and freezes the pool gauges at their last values. A drained
pool would read as idle capacity. The per-pod series comes from a
scrape-time collector that simply stops reporting instead, so the query
yields no samples and the cascade moves on.
Flow control stays at index 0 so installs that do enable the plugin see
no change; vLLM moves to index 2.
Observability, so this failure mode is visible next time:
- GateFactory logs the resolved PromQL for every Prometheus gate at
startup. Queries are built from gate_params, so previously the only
way to see what was asked of Prometheus was to read the source.
- CascadeMetricSource starts at index -1 and logs the first source
that resolves. With a zero start, a cascade served by the primary
from the outset looked like "no change" and never said which source
the budget came from.
- New llm_d_async_async_gate_metric_source_available gauge separates
"fell back because nothing resolved" from a real reading of the same
number — most importantly a fallback of 0 from a saturated pool.
Fixes #361
Signed-off-by: Shimi Bandiel <shimib@google.com>
Signed-off-by: Shimi Bandiel <shimib@google.com>
shimib
force-pushed
the
fix/budget-primary-metric-source
branch
from
July 29, 2026 21:56
37f15e6 to
6b9cc12
Compare
jtechapps
approved these changes
Jul 29, 2026
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.
Fixes #361
Problem
The
prometheus-budgetgate's primary metric source queriesinference_extension_flow_control_queue_size. That metric only exists when EPP runs the flow control plugin — and the shippedllm-d-router-endpoint-pickerdoes not enable it and offers no flag to turn it on. On a documented llm-d install, source 0 never resolves.The cascade then falls straight through to the vLLM source, which filters on an
inference_poollabel vLLM does not emit natively; it requires model server pods to carry the label plus Prometheus metric relabeling to propagate it. If that is not configured either, every gate evaluation silently returns the configuredfallbackbudget — indistinguishable from a real reading of the same number.Change
Insert a middle tier built on a metric a stock EPP always exports:
Cascade order is now:
inference_extension_flow_control_queue_sizeD = 1 − (queue_size / max_SYS)inference_pool_per_pod_queue_sizeD = 1 − (mean per-pod queue depth / max_concurrency)vllm:num_requests_runningD = 1 − (running_requests / max_SYS)inference_poollabelFlow control stays at index 0, so installs that do enable the plugin are unaffected.
Why this metric, and why
avgAveraging over pods is what makes
max_SYS = ready_pods × max_concurrencyreduce tomax_concurrency, so noinference_pool_ready_podsjoin is needed. That is not just brevity: EPP's metrics refresh returns early when the pool has no pods, freezinginference_pool_ready_podsandinference_pool_average_queue_sizeat their last values — a drained pool would read as idle capacity and open the gate onto nothing.inference_pool_per_pod_queue_sizecomes from a scrape-time collector instead, so it stops reporting entirely; the query yields no samples and the cascade moves on.avgissum/count, so the result is invariant to several EPP replicas reporting the same pods. EPP labels itsinference_pool_*series withname, notinference_pool.Observability
So this failure mode is visible next time rather than silent:
prometheus-budget metric source, one line per cascade tier, andprometheus-saturation metric source). Queries are built fromgate_params, so previously the only way to see what was actually asked of Prometheus was to read the source.prometheus-queryis deliberately not logged — its query is user-supplied verbatim.CascadeMetricSourcestarts at index-1and logs the first source that resolves (metric source resolved). With a zero start, a cascade served by the primary from the outset looked like "no change" and never reported which source the budget came from.llm_d_async_async_gate_metric_source_availablegauge — 1 when the last evaluation got a usable reading, 0 when it fell back (query error, no samples, NaN/Inf). This separates a fallback budget from a genuine reading of the same number; most importantly, a fallback of 0 from a truly saturated pool.Readiness gating (holding pod readiness until a source returns a sample) was considered and deliberately left out of scope.
Tests
TestPoolQueueSizePromQL— exact expression, required params, namespace, and an assertion that the expression depends on neither of the two gauges that freeze at zero pods.TestGateFactory_BudgetGateCascadeSources— the cascade is three sources in the documented order.TestGateFactory_BudgetGateLogsResolvedQueries/TestGateFactory_SaturationGateLogsResolvedQuery— startup logging, viafuncr.TestMetricDispatchGate_RecordsMetricSourceAvailable/..._MetricSourceAvailableFlipsBack— the new gauge across usable/error/empty/NaN, and recovery.TestCascadeMetricSource_UsesPrimaryextended, plusTestCascadeMetricSource_AllUnavailableLeavesIndexUnresolved.E2E blast radius:
test/e2e/helm/budget.yamlsetsmax_concurrency: "1". In the cascade Describe (EPP without flow control, sim at 0 waiting) source 1 yieldsD = 1 - 0/1 = 1, so the existing assertion still holds; in the second Describe EPP runs with flow control, so source 0 always answers and source 1 is never consulted. Only comments and one test name needed updating.Docs updated:
README.md,docs/dispatch-budget.md,docs/guides/e2e-deploy.md(verification steps now cover source 1 and the new gauge).