Skip to content

Commit 3784c7d

Browse files
committed
feat(flowcontrol): add per-band and global capacity utilization gauges
Signed-off-by: Alok Behera <alokbeherak061@gmail.com>
1 parent 44828e2 commit 3784c7d

5 files changed

Lines changed: 143 additions & 1 deletion

File tree

docs/metrics.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,29 @@ Exposed when the `flowControl` feature gate is enabled.
256256
* **Usage:** When saturation reaches the usage limit threshold, the dispatch cycle skips
257257
dispatching and requests remain queued. Sustained 1.0 indicates all backends are at capacity.
258258

259+
#### `flow_control_capacity_utilization_requests`
260+
261+
* **Type:** Gauge
262+
* **Labels:** `priority`, `inference_pool`
263+
* **Description:** Fraction of the configured request-count capacity currently occupied (0.0-1.0),
264+
aggregated over every flow in a priority band. This is not a per-flow-queue metric: `priority`
265+
identifies the band, and `priority=""` carries the aggregate across all bands (emitted only when
266+
a global request-count capacity is configured). A dimension with no configured capacity is
267+
omitted rather than reported as 0.
268+
* **Usage:** Lets operators alert on "the band is at N% of its request limit" without joining
269+
configured `maxRequests` values into the query. Sustained values near 1.0 precede
270+
`flow_control_requests_total{outcome="RejectedCapacity"}` rising.
271+
272+
#### `flow_control_capacity_utilization_bytes`
273+
274+
* **Type:** Gauge
275+
* **Labels:** `priority`, `inference_pool`
276+
* **Description:** Byte-size counterpart of `flow_control_capacity_utilization_requests`: the
277+
fraction of the configured byte-size capacity currently occupied (0.0-1.0), aggregated over every
278+
flow in a priority band, with `priority=""` for the aggregate across all bands.
279+
* **Usage:** Memory-pressure equivalent of the request-count ratio; a band can hit its `maxBytes`
280+
ceiling long before its `maxRequests` one when payloads are large.
281+
259282
#### `flow_control_requests_total`
260283

261284
* **Type:** Counter

pkg/epp/flowcontrol/controller/internal/processor.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,37 @@ func (p *Processor) hasCapacity(priority int, itemByteSize uint64) (bool, contra
356356
return true, stats
357357
}
358358

359+
// recordCapacityUtilization emits occupancy/configured-capacity ratio gauges per priority band (aggregated over every
360+
// flow in the band, never per flow queue) and, when a global capacity is configured, the aggregate across all bands
361+
// (priority ""). It reads a single Stats() snapshot; the data source is expected to move with the engine-merge
362+
// refactor, but the metric contract (names, labels, semantics) stays stable (#2102).
363+
// A dimension with no configured capacity (capacity 0) is omitted rather than reported as a misleading 0.
364+
func (p *Processor) recordCapacityUtilization() {
365+
stats := p.registry.Stats()
366+
367+
for priority, band := range stats.PerPriorityBandStats {
368+
priorityStr := strconv.Itoa(priority)
369+
if band.CapacityRequests > 0 {
370+
metrics.RecordFlowControlCapacityUtilizationRequests(priorityStr, p.poolName,
371+
float64(band.Len)/float64(band.CapacityRequests))
372+
}
373+
if band.CapacityBytes > 0 {
374+
metrics.RecordFlowControlCapacityUtilizationBytes(priorityStr, p.poolName,
375+
float64(band.ByteSize)/float64(band.CapacityBytes))
376+
}
377+
}
378+
379+
// Aggregate across all bands, only when a global capacity is configured.
380+
if stats.TotalCapacityRequests > 0 {
381+
metrics.RecordFlowControlCapacityUtilizationRequests("", p.poolName,
382+
float64(stats.TotalLen)/float64(stats.TotalCapacityRequests))
383+
}
384+
if stats.TotalCapacityBytes > 0 {
385+
metrics.RecordFlowControlCapacityUtilizationBytes("", p.poolName,
386+
float64(stats.TotalByteSize)/float64(stats.TotalCapacityBytes))
387+
}
388+
}
389+
359390
// dispatchCycle attempts to dispatch a single item by iterating through priority bands from highest to lowest.
360391
// It applies the configured policies for each band to select an item and then attempts to dispatch it.
361392
// It returns true if an item was successfully dispatched, and false otherwise.
@@ -380,6 +411,9 @@ func (p *Processor) dispatchCycle(ctx context.Context) bool {
380411
// Record pool saturation metric
381412
metrics.RecordFlowControlPoolSaturation(p.poolName, saturation)
382413

414+
// Record capacity utilization ratios (the demand-side twin of saturation) from the same periodic sample.
415+
p.recordCapacityUtilization()
416+
383417
priorities := p.registry.AllOrderedPriorityLevels()
384418
ceilings := p.usageLimitPolicy.ComputeLimit(ctx, saturation, priorities)
385419

pkg/epp/flowcontrol/integration_test.go

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package flowcontrol_test
1919
import (
2020
"context"
2121
"fmt"
22+
"strconv"
2223
"sync"
2324
"sync/atomic"
2425
"testing"
@@ -1163,7 +1164,10 @@ func TestFlowControlMetricsEmitted(t *testing.T) {
11631164
eppmetrics.Register()
11641165

11651166
detector := newBlockedDetector()
1166-
h := newHarness(t, harnessOpts{detector: detector})
1167+
// bandMaxRequests and maxRequests bound the band and the registry as a whole, so both the
1168+
// per-band ratio and the all-bands aggregate are computable (occupancy/configured capacity);
1169+
// without a configured capacity the ratio series is intentionally omitted.
1170+
h := newHarness(t, harnessOpts{detector: detector, bandMaxRequests: 10, maxRequests: 4})
11671171

11681172
key := flowcontrol.FlowKey{ID: "metrics-flow", Priority: 0}
11691173

@@ -1186,6 +1190,20 @@ func TestFlowControlMetricsEmitted(t *testing.T) {
11861190
require.Greater(t, queueSizeGaugeSum(t, key.ID), 0.0,
11871191
"queue_size should be > 0 while a request is actively queued")
11881192

1193+
// Both dimensions are bounded (bandMaxRequests=10, maxRequests=4 above), so with exactly one
1194+
// request queued the ratios are occupancy/configured capacity: 1/10 for the band and 1/4 for the
1195+
// all-bands aggregate. Unlike queue_size, these gauges are refreshed by the dispatch cycle rather
1196+
// than synchronously on enqueue, so they trail admission by up to one tick.
1197+
priorityStr := strconv.Itoa(key.Priority)
1198+
require.Eventually(t, func() bool {
1199+
return capacityUtilizationGauge(t, priorityStr) > 0
1200+
}, time.Second, time.Millisecond,
1201+
"band utilization ratio should be > 0 while a request is actively queued")
1202+
require.InDelta(t, 0.1, capacityUtilizationGauge(t, priorityStr), 1e-9,
1203+
"band ratio should equal occupancy/capacity (1 queued / bandMaxRequests=10)")
1204+
require.InDelta(t, 0.25, capacityUtilizationGauge(t, globalPriorityLabel), 1e-9,
1205+
"aggregate ratio should equal occupancy/global capacity (1 queued / maxRequests=4)")
1206+
11891207
// Unblock the detector so the request finalizes deterministically via dispatch.
11901208
detector.Unblock(1)
11911209

@@ -1201,6 +1219,37 @@ func TestFlowControlMetricsEmitted(t *testing.T) {
12011219
// gauge is already back at 0 -- no polling needed.
12021220
require.Zero(t, queueSizeGaugeSum(t, key.ID),
12031221
"queue_size should return to 0 after the request finalizes")
1222+
1223+
// The utilization gauges are dispatch-cycle driven, so they trail the drain by up to one tick.
1224+
require.Eventually(t, func() bool {
1225+
return capacityUtilizationGauge(t, priorityStr) == 0 &&
1226+
capacityUtilizationGauge(t, globalPriorityLabel) == 0
1227+
}, time.Second, time.Millisecond,
1228+
"band and aggregate utilization ratios should return to 0 after the queue drains")
1229+
}
1230+
1231+
// globalPriorityLabel is the priority label value carried by the all-bands aggregate series.
1232+
const globalPriorityLabel = ""
1233+
1234+
// capacityUtilizationGauge returns the request-count capacity utilization ratio recorded for the given
1235+
// priority band, or -1 if no series exists for that band.
1236+
func capacityUtilizationGauge(t *testing.T, priority string) float64 {
1237+
t.Helper()
1238+
families, err := ctrlmetrics.Registry.Gather()
1239+
require.NoError(t, err)
1240+
for _, f := range families {
1241+
if f.GetName() != "llm_d_epp_flow_control_capacity_utilization_requests" {
1242+
continue
1243+
}
1244+
for _, m := range f.GetMetric() {
1245+
for _, lp := range m.GetLabel() {
1246+
if lp.GetName() == "priority" && lp.GetValue() == priority {
1247+
return m.GetGauge().GetValue()
1248+
}
1249+
}
1250+
}
1251+
}
1252+
return -1
12041253
}
12051254

12061255
// ============================================================================

pkg/epp/metrics/llm_d_router_metrics.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,24 @@ var (
380380
[]string{"inference_pool"},
381381
)
382382

383+
llmdFlowControlCapacityUtilizationRequests = prometheus.NewGaugeVec(
384+
prometheus.GaugeOpts{
385+
Subsystem: LLMDRouterEndpointPickerSubsystem,
386+
Name: "flow_control_capacity_utilization_requests",
387+
Help: metricsutil.HelpMsgWithStability("Fraction of the configured request-count capacity currently occupied, aggregated over every flow in a priority band (0.0-1.0). The aggregate across all bands is reported with priority=\"\" and only when a global request-count capacity is configured; a dimension with no configured capacity is omitted rather than reported as 0.", compbasemetrics.ALPHA),
388+
},
389+
[]string{"priority", "inference_pool"},
390+
)
391+
392+
llmdFlowControlCapacityUtilizationBytes = prometheus.NewGaugeVec(
393+
prometheus.GaugeOpts{
394+
Subsystem: LLMDRouterEndpointPickerSubsystem,
395+
Name: "flow_control_capacity_utilization_bytes",
396+
Help: metricsutil.HelpMsgWithStability("Fraction of the configured byte-size capacity currently occupied, aggregated over every flow in a priority band (0.0-1.0). The aggregate across all bands is reported with priority=\"\" and only when a global byte-size capacity is configured; a dimension with no configured capacity is omitted rather than reported as 0.", compbasemetrics.ALPHA),
397+
},
398+
[]string{"priority", "inference_pool"},
399+
)
400+
383401
llmdFlowControlRequestsTotal = prometheus.NewCounterVec(
384402
prometheus.CounterOpts{
385403
Subsystem: LLMDRouterEndpointPickerSubsystem,

pkg/epp/metrics/metrics.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,8 @@ func Register(customCollectors ...prometheus.Collector) {
472472
metrics.Registry.MustRegister(llmdFlowControlQueueBytes)
473473
metrics.Registry.MustRegister(flowControlPoolSaturation)
474474
metrics.Registry.MustRegister(llmdFlowControlPoolSaturation)
475+
metrics.Registry.MustRegister(llmdFlowControlCapacityUtilizationRequests)
476+
metrics.Registry.MustRegister(llmdFlowControlCapacityUtilizationBytes)
475477
metrics.Registry.MustRegister(flowControlRequestEnqueueDuration)
476478
metrics.Registry.MustRegister(llmdFlowControlRequestEnqueueDuration)
477479
metrics.Registry.MustRegister(llmdFlowControlRequestsTotal)
@@ -542,6 +544,8 @@ func Reset() {
542544
llmdFlowControlQueueBytes.Reset()
543545
flowControlPoolSaturation.Reset()
544546
llmdFlowControlPoolSaturation.Reset()
547+
llmdFlowControlCapacityUtilizationRequests.Reset()
548+
llmdFlowControlCapacityUtilizationBytes.Reset()
545549
flowControlRequestEnqueueDuration.Reset()
546550
llmdFlowControlRequestEnqueueDuration.Reset()
547551
flowControlDispatchCycleDuration.Reset()
@@ -906,6 +910,20 @@ func RecordFlowControlPoolSaturation(inferencePool string, saturation float64) {
906910
llmdFlowControlPoolSaturation.WithLabelValues(inferencePool).Set(saturation)
907911
}
908912

913+
// RecordFlowControlCapacityUtilizationRequests sets the request-count capacity utilization ratio
914+
// (occupancy/configured capacity, 0.0-1.0) for a priority band, or the aggregate across all bands when priority is "".
915+
// Callers only invoke this for a bounded dimension, so an unbounded one yields no series rather than a misleading 0.
916+
func RecordFlowControlCapacityUtilizationRequests(priority, inferencePool string, ratio float64) {
917+
llmdFlowControlCapacityUtilizationRequests.WithLabelValues(priority, inferencePool).Set(ratio)
918+
}
919+
920+
// RecordFlowControlCapacityUtilizationBytes sets the byte-size capacity utilization ratio (occupancy/configured
921+
// capacity, 0.0-1.0) for a priority band, or the aggregate across all bands when priority is "". Callers only invoke
922+
// this for a bounded dimension, so an unbounded one yields no series rather than a misleading 0.
923+
func RecordFlowControlCapacityUtilizationBytes(priority, inferencePool string, ratio float64) {
924+
llmdFlowControlCapacityUtilizationBytes.WithLabelValues(priority, inferencePool).Set(ratio)
925+
}
926+
909927
// IncFlowControlRequestsTotal increments the total request counter for a given outcome.
910928
func IncFlowControlRequestsTotal(outcome, priority, inferencePool string) {
911929
llmdFlowControlRequestsTotal.WithLabelValues(outcome, priority, inferencePool).Inc()

0 commit comments

Comments
 (0)