Skip to content

Commit 165bfc7

Browse files
authored
Merge branch 'main' into 2169-registry-capacity-counters
2 parents 7c4a2aa + 367a663 commit 165bfc7

106 files changed

Lines changed: 1837 additions & 546 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,26 @@ llm-d Router. Go service that routes inference requests to model-serving pods vi
4141

4242
- Standard Go. `make format` and `make lint` are authoritative.
4343
- Comments are terse and only present when the WHY is non-obvious. Never paraphrase the code.
44-
- Docs and comments describe the current state on its own terms. No "previously", "now", "recently", "renamed from", "added to fix", or other temporal or conversational framing. A reader with no context for the change must still understand the text.
44+
- Docs and comments describe the current state on its own terms. No "previously", "now", "recently", "renamed from", "added to fix", "this PR", "see above", or other temporal, deictic, or conversational framing. A reader with no context for the change must still understand the text.
4545
- State each fact once, in its canonical location. Do not duplicate across struct docs, prose, tables, inline comments, and examples.
4646
- Do not use Unicode symbols or special characters in general, unless explicitly requested.
4747

48+
### Constructions to delete
49+
50+
Model-drafted prose drifts toward compressed, rhetorical phrasing. Before shipping any prose (comments, docs, commit bodies, PR descriptions), decompress it: rewrite each such sentence as a plain statement of the fact it carries, judged against what the change is actually for. The test: if a sentence sounds quotable, delete it.
51+
52+
| Tic | Example | Fix |
53+
|---|---|---|
54+
| X, not Y | "the queue is not a buffer, it is a fairness mechanism" | say what it is, once |
55+
| Coined aphorism | "a shed request is not a failure, it is the contract" | delete the sentence |
56+
| Closing zinger | a paragraph that ends on a beat instead of a fact | end on information |
57+
| Triads for rhythm | "no locks to take, no channels to drain, no state to sync" | one clause |
58+
| Portent counters | "three things follow", "two points are worth noting" | just say them |
59+
| Filler intensifiers | genuinely, precisely, critically, crucially, "worth noting" | cut |
60+
| Editorial tails | "..., which is exactly what we want" | cut, or a new sentence |
61+
| Grandeur adjectives | comprehensive, robust, seamless, production-ready | name the verified behavior |
62+
| Dash pileup | more than one dash pair per paragraph | periods |
63+
4864
### Logging
4965

5066
The codebase uses `go-logr` via controller-runtime. Verbosity constants are defined in `pkg/common/observability/logging` (`DEFAULT=2`, `VERBOSE=3`, `DEBUG=4`, `TRACE=5`).

RELEASE-NOTES.md

Lines changed: 70 additions & 0 deletions
Large diffs are not rendered by default.

cmd/epp/runner/runner.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ import (
108108
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/requesthandling/parsers/anthropic"
109109
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/requesthandling/parsers/openai"
110110
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/requesthandling/parsers/passthrough"
111+
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/requesthandling/parsers/sglanghttp"
111112
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/requesthandling/parsers/vertexai"
112113
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/requesthandling/parsers/vllmgrpc"
113114
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/requesthandling/parsers/vllmhttp"
@@ -708,6 +709,7 @@ func (r *Runner) registerInTreePlugins() {
708709
fwkplugin.Register(passthrough.PassthroughParserType, fwkplugin.StabilityBeta, passthrough.PassthroughParserPluginFactory)
709710
fwkplugin.Register(anthropic.AnthropicParserType, fwkplugin.StabilityBeta, anthropic.AnthropicParserPluginFactory)
710711
fwkplugin.Register(vllmhttp.VllmHTTPParserType, fwkplugin.StabilityBeta, vllmhttp.VllmHTTPParserPluginFactory)
712+
fwkplugin.Register(sglanghttp.SGLangHTTPParserType, fwkplugin.StabilityBeta, sglanghttp.SGLangHTTPParserPluginFactory)
711713
fwkplugin.Register(vertexai.VertexAIParserType, fwkplugin.StabilityBeta, vertexai.VertexAIParserPluginFactory)
712714

713715
// register saturation detector plugins

docs/metrics.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,55 @@ Exposed when the `flowControl` feature gate is enabled.
279279
* **Usage:** A nonzero value during a dispatch stall indicates a model-server metrics collection
280280
problem (scrape path, port, TLS, auth) rather than genuine overload.
281281

282+
#### `flow_control_capacity_utilization_requests`
283+
284+
* **Type:** Gauge
285+
* **Labels:** `priority`, `inference_pool`
286+
* **Description:** Fraction of a priority band's **effective** request-count capacity currently
287+
occupied (0.0-1.0), aggregated over every flow in the band. This is not a per-flow-queue metric:
288+
`priority` identifies the band. A band that does not configure `maxRequests` falls back to a
289+
default denominator, so every configured band reports a series. The all-bands rollup is a
290+
separate metric, `flow_control_global_capacity_utilization_requests`.
291+
* **Usage:** Lets operators alert on "the band is at N% of its request limit" without joining
292+
configured `maxRequests` values into the query. Sustained values near 1.0 precede
293+
`flow_control_requests_total{outcome="RejectedCapacity"}` rising. Note the denominator is always
294+
the band's own capacity: when a global cap sits below the sum of the band caps, admission is
295+
bounded by the global cap first, so the per-band ratio understates real pressure — read it
296+
alongside `flow_control_global_capacity_utilization_requests`. (Making a band's denominator
297+
`min(band, global)` would make one band's ratio depend on other bands' configuration, which is
298+
worse.)
299+
300+
#### `flow_control_capacity_utilization_bytes`
301+
302+
* **Type:** Gauge
303+
* **Labels:** `priority`, `inference_pool`
304+
* **Description:** Byte-size counterpart of `flow_control_capacity_utilization_requests`: the
305+
fraction of a priority band's effective byte-size capacity currently occupied (0.0-1.0),
306+
aggregated over every flow in the band, with the same default-denominator fallback.
307+
* **Usage:** Memory-pressure equivalent of the request-count ratio; a band can hit its `maxBytes`
308+
ceiling long before its `maxRequests` one when payloads are large. The same global-cap caveat
309+
applies — compare against `flow_control_global_capacity_utilization_bytes`.
310+
311+
#### `flow_control_global_capacity_utilization_requests`
312+
313+
* **Type:** Gauge
314+
* **Labels:** `inference_pool`
315+
* **Description:** Fraction of the global request-count capacity currently occupied across all
316+
priority bands (0.0-1.0). Global capacity is optional and unset by default, so this series is
317+
emitted only when it is configured — absent, not 0.
318+
* **Usage:** The rollup companion to the per-band ratio. It lives in its own metric family so that
319+
aggregations over the per-band family (`sum`, `max`, `topk`) do not double count or rank against
320+
the rollup.
321+
322+
#### `flow_control_global_capacity_utilization_bytes`
323+
324+
* **Type:** Gauge
325+
* **Labels:** `inference_pool`
326+
* **Description:** Byte-size counterpart of `flow_control_global_capacity_utilization_requests`,
327+
with the same optional-and-omitted-when-unset behaviour.
328+
* **Usage:** Shows whether the global byte ceiling, rather than any single band, is what is
329+
bounding admission.
330+
282331
#### `flow_control_requests_total`
283332

284333
* **Type:** Counter

pkg/common/envoy/chunking_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,37 @@ func TestBuildChunkedBodyResponses(t *testing.T) {
7878
}
7979
}
8080

81+
// TestBuildChunkedBodyResponses_PreservesBody confirms that the chunks reassemble into
82+
// exactly the input bytes whether or not the caller mutated the body upstream: chunking
83+
// itself is agnostic to mutation, so skipping a rewrite must not change what reaches Envoy.
84+
func TestBuildChunkedBodyResponses_PreservesBody(t *testing.T) {
85+
tests := []struct {
86+
name string
87+
body []byte
88+
}{
89+
{
90+
name: "unmutated body",
91+
body: []byte(`{"id":"cmpl-123","model":"vllm-backend-01","choices":[]}`),
92+
},
93+
{
94+
name: "mutated body",
95+
body: []byte(`{"id":"cmpl-123","model":"gpt-4-proxy","choices":[]}`),
96+
},
97+
}
98+
for _, test := range tests {
99+
t.Run(test.name, func(t *testing.T) {
100+
responses := BuildChunkedBodyResponses(test.body, true)
101+
var got []byte
102+
for _, response := range responses {
103+
got = append(got, response.BodyMutation.GetStreamedResponse().GetBody()...)
104+
}
105+
if string(got) != string(test.body) {
106+
t.Fatalf("reassembled chunks = %q, want %q", got, test.body)
107+
}
108+
})
109+
}
110+
}
111+
81112
func generateBytes(count int) []byte {
82113
arr := make([]byte, count)
83114
_, _ = rand.Read(arr)

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,39 @@ func (p *Processor) hasCapacity(priority int, itemByteSize uint64) (bool, contra
391391
return true, snapshot, nil
392392
}
393393

394+
// recordCapacityUtilization emits occupancy/effective-capacity ratio gauges per priority band (aggregated over every
395+
// flow in the band, never per flow queue), plus the all-bands rollup in its own metric family when a global capacity
396+
// is configured. It reads a single Stats() snapshot; the data source is expected to move with the engine-merge
397+
// refactor, but the metric contract (names, labels, semantics) stays stable (#2102).
398+
//
399+
// Band capacities always resolve to a value (applyDefaults supplies a fallback), so every configured band reports.
400+
// Global capacity is optional, so its series is omitted when unset rather than reported as a misleading 0.
401+
func (p *Processor) recordCapacityUtilization() {
402+
stats := p.registry.Stats()
403+
404+
for priority, band := range stats.PerPriorityBandStats {
405+
priorityStr := strconv.Itoa(priority)
406+
if band.CapacityRequests > 0 {
407+
metrics.RecordFlowControlCapacityUtilizationRequests(priorityStr, p.poolName,
408+
float64(band.Len)/float64(band.CapacityRequests))
409+
}
410+
if band.CapacityBytes > 0 {
411+
metrics.RecordFlowControlCapacityUtilizationBytes(priorityStr, p.poolName,
412+
float64(band.ByteSize)/float64(band.CapacityBytes))
413+
}
414+
}
415+
416+
// All-bands rollup, only when a global capacity is configured.
417+
if stats.TotalCapacityRequests > 0 {
418+
metrics.RecordFlowControlGlobalCapacityUtilizationRequests(p.poolName,
419+
float64(stats.TotalLen)/float64(stats.TotalCapacityRequests))
420+
}
421+
if stats.TotalCapacityBytes > 0 {
422+
metrics.RecordFlowControlGlobalCapacityUtilizationBytes(p.poolName,
423+
float64(stats.TotalByteSize)/float64(stats.TotalCapacityBytes))
424+
}
425+
}
426+
394427
// dispatchCycle attempts to dispatch a single item by iterating through priority bands from highest to lowest.
395428
// It applies the configured policies for each band to select an item and then attempts to dispatch it.
396429
// It returns true if an item was successfully dispatched, and false otherwise.
@@ -418,6 +451,9 @@ func (p *Processor) dispatchCycle(ctx context.Context) bool {
418451
// Record pool saturation metric
419452
metrics.RecordFlowControlPoolSaturation(p.poolName, saturation)
420453

454+
// Record capacity utilization ratios (the demand-side twin of saturation) from the same periodic sample.
455+
p.recordCapacityUtilization()
456+
421457
priorities := p.registry.AllOrderedPriorityLevels()
422458
ceilings := p.ceilingsBuffer(len(priorities))
423459
p.usageLimitPolicy.ComputeLimit(ctx, saturation, priorities, ceilings)

pkg/epp/flowcontrol/integration_test.go

Lines changed: 130 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"
@@ -1178,9 +1179,21 @@ func queueSizeGaugeSum(t *testing.T, fairnessID string) float64 {
11781179
// deterministic.
11791180
func TestFlowControlMetricsEmitted(t *testing.T) {
11801181
eppmetrics.Register()
1182+
// The capacity gauges are keyed only by (priority, inference_pool), which every harness in this
1183+
// package shares, so the assertions below need a clean slate.
1184+
eppmetrics.Reset()
11811185

11821186
detector := newBlockedDetector()
1183-
h := newHarness(t, harnessOpts{detector: detector})
1187+
// bandMaxRequests bounds the band and maxRequests/maxBytes bound the registry as a whole, so both
1188+
// the per-band ratios and both all-bands rollups are computable (occupancy/effective capacity).
1189+
// Band capacity always resolves to a value; the global ones are optional, which the omission test
1190+
// below covers.
1191+
h := newHarness(t, harnessOpts{
1192+
detector: detector,
1193+
bandMaxRequests: 10,
1194+
maxRequests: 4,
1195+
maxBytes: 10_000,
1196+
})
11841197

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

@@ -1203,6 +1216,26 @@ func TestFlowControlMetricsEmitted(t *testing.T) {
12031216
require.Greater(t, queueSizeGaugeSum(t, key.ID), 0.0,
12041217
"queue_size should be > 0 while a request is actively queued")
12051218

1219+
// Both dimensions are bounded (bandMaxRequests=10, maxRequests=4 above), so with exactly one
1220+
// request queued the ratios are occupancy/effective capacity: 1/10 for the band and 1/4 for the
1221+
// all-bands rollup. Unlike queue_size, these gauges are refreshed by the dispatch cycle rather
1222+
// than synchronously on enqueue, so they trail admission by up to one tick.
1223+
priorityStr := strconv.Itoa(key.Priority)
1224+
require.Eventually(t, func() bool {
1225+
return capacityUtilizationGauge(t, capacityUtilizationRequestsFamily, priorityStr) > 0
1226+
}, time.Second, time.Millisecond,
1227+
"band utilization ratio should be > 0 while a request is actively queued")
1228+
require.InDelta(t, 0.1, capacityUtilizationGauge(t, capacityUtilizationRequestsFamily, priorityStr), 1e-9,
1229+
"band ratio should equal occupancy/capacity (1 queued / bandMaxRequests=10)")
1230+
require.InDelta(t, 0.25, globalCapacityUtilizationGauge(t, globalCapacityUtilizationRequestsFamily), 1e-9,
1231+
"rollup ratio should equal occupancy/global capacity (1 queued / maxRequests=4)")
1232+
1233+
// The bytes dimension is driven by the same snapshot, so it must also be reporting a live ratio.
1234+
require.Greater(t, capacityUtilizationGauge(t, capacityUtilizationBytesFamily, priorityStr), 0.0,
1235+
"band byte-size utilization should be > 0 while a request is actively queued")
1236+
require.Greater(t, globalCapacityUtilizationGauge(t, globalCapacityUtilizationBytesFamily), 0.0,
1237+
"rollup byte-size utilization should be > 0 while a request is actively queued")
1238+
12061239
// Unblock the detector so the request finalizes deterministically via dispatch.
12071240
detector.Unblock(1)
12081241

@@ -1218,6 +1251,102 @@ func TestFlowControlMetricsEmitted(t *testing.T) {
12181251
// gauge is already back at 0 -- no polling needed.
12191252
require.Zero(t, queueSizeGaugeSum(t, key.ID),
12201253
"queue_size should return to 0 after the request finalizes")
1254+
1255+
// The utilization gauges are dispatch-cycle driven, so they trail the drain by up to one tick.
1256+
require.Eventually(t, func() bool {
1257+
return capacityUtilizationGauge(t, capacityUtilizationRequestsFamily, priorityStr) == 0 &&
1258+
globalCapacityUtilizationGauge(t, globalCapacityUtilizationRequestsFamily) == 0
1259+
}, time.Second, time.Millisecond,
1260+
"band and rollup utilization ratios should return to 0 after the queue drains")
1261+
}
1262+
1263+
// TestFlowControlCapacityUtilizationOmitsUnsetGlobal verifies that the all-bands rollup is absent
1264+
// rather than reported as 0 when no global capacity is configured. Downstream alerts may key off
1265+
// series absence, so a refactor that starts emitting 0 here would silently change their meaning.
1266+
func TestFlowControlCapacityUtilizationOmitsUnsetGlobal(t *testing.T) {
1267+
eppmetrics.Register()
1268+
eppmetrics.Reset()
1269+
1270+
detector := newBlockedDetector()
1271+
// bandMaxRequests only: the band reports as usual, the global capacity stays unset.
1272+
h := newHarness(t, harnessOpts{detector: detector, bandMaxRequests: 10})
1273+
1274+
key := flowcontrol.FlowKey{ID: "metrics-flow-no-global", Priority: 0}
1275+
priorityStr := strconv.Itoa(key.Priority)
1276+
1277+
results := make(chan dispatchResult, 1)
1278+
go func() {
1279+
reqCtx, reqCancel := context.WithTimeout(h.ctx, 5*time.Second)
1280+
defer reqCancel()
1281+
req := &testRequest{id: key.ID, key: key, byteSize: 100, ttl: 5 * time.Second}
1282+
outcome, err := h.fc.EnqueueAndWait(reqCtx, req)
1283+
results <- dispatchResult{id: key.ID, outcome: outcome, err: err}
1284+
}()
1285+
1286+
// Wait until the band series exists, which means a dispatch cycle has published a sample.
1287+
require.Eventually(t, func() bool {
1288+
return capacityUtilizationGauge(t, capacityUtilizationRequestsFamily, priorityStr) > 0
1289+
}, time.Second, time.Millisecond, "band utilization should be reported for a bounded band")
1290+
1291+
require.Equal(t, -1.0, globalCapacityUtilizationGauge(t, globalCapacityUtilizationRequestsFamily),
1292+
"no global request capacity is configured, so the rollup series must be absent, not 0")
1293+
require.Equal(t, -1.0, globalCapacityUtilizationGauge(t, globalCapacityUtilizationBytesFamily),
1294+
"no global byte capacity is configured, so the rollup series must be absent, not 0")
1295+
1296+
detector.Unblock(1)
1297+
select {
1298+
case r := <-results:
1299+
require.NoError(t, r.err)
1300+
require.Equal(t, fcTypes.QueueOutcomeDispatched, r.outcome)
1301+
case <-time.After(5 * time.Second):
1302+
t.Fatal("request did not dispatch after detector was unblocked")
1303+
}
1304+
}
1305+
1306+
// Metric family names asserted by the capacity utilization tests.
1307+
const (
1308+
capacityUtilizationRequestsFamily = "llm_d_epp_flow_control_capacity_utilization_requests"
1309+
capacityUtilizationBytesFamily = "llm_d_epp_flow_control_capacity_utilization_bytes"
1310+
globalCapacityUtilizationRequestsFamily = "llm_d_epp_flow_control_global_capacity_utilization_requests"
1311+
globalCapacityUtilizationBytesFamily = "llm_d_epp_flow_control_global_capacity_utilization_bytes"
1312+
)
1313+
1314+
// capacityUtilizationGauge returns the per-band capacity utilization ratio recorded in the given
1315+
// metric family for the given priority band, or -1 if no series exists for that band.
1316+
func capacityUtilizationGauge(t *testing.T, family, priority string) float64 {
1317+
t.Helper()
1318+
families, err := ctrlmetrics.Registry.Gather()
1319+
require.NoError(t, err)
1320+
for _, f := range families {
1321+
if f.GetName() != family {
1322+
continue
1323+
}
1324+
for _, m := range f.GetMetric() {
1325+
for _, lp := range m.GetLabel() {
1326+
if lp.GetName() == "priority" && lp.GetValue() == priority {
1327+
return m.GetGauge().GetValue()
1328+
}
1329+
}
1330+
}
1331+
}
1332+
return -1
1333+
}
1334+
1335+
// globalCapacityUtilizationGauge returns the all-bands capacity utilization ratio recorded in the
1336+
// given metric family, or -1 if the series is absent (no global capacity configured).
1337+
func globalCapacityUtilizationGauge(t *testing.T, family string) float64 {
1338+
t.Helper()
1339+
families, err := ctrlmetrics.Registry.Gather()
1340+
require.NoError(t, err)
1341+
for _, f := range families {
1342+
if f.GetName() != family {
1343+
continue
1344+
}
1345+
for _, m := range f.GetMetric() {
1346+
return m.GetGauge().GetValue()
1347+
}
1348+
}
1349+
return -1
12211350
}
12221351

12231352
// ============================================================================

pkg/epp/framework/interface/requestcontrol/types.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ type Response struct {
6363
ReqMetadata map[string]any
6464
// Token usage counts parsed from the response body.
6565
Usage requesthandling.Usage
66+
// StreamedEvents is the running count of stream data events observed so far. It is the only
67+
// length signal this record carries for a truncated stream, since a stream that never
68+
// completes carries no usage block. Consumers must not treat zero as evidence that nothing
69+
// was generated; requesthandling.ParsedResponse documents which parsers count and how the
70+
// count deviates from the token count.
71+
StreamedEvents int
6672
// TerminationCause labels how the stream ended.
6773
TerminationCause TerminationCause
6874
// DynamicMetadata is a map of metadata that can be passed to the Envoy. It is populated into the dynamic

pkg/epp/framework/interface/requesthandling/plugins.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,12 @@ type ParseResult struct {
8383
type ParsedResponse struct {
8484
// Usage is only populate when the raw response has usage.
8585
Usage *Usage
86+
// StreamedEvents is how many stream data events this parse observed, zero for a non-streamed
87+
// response or a parser that does not count. For servers that stream one content delta per
88+
// token the count approximates the tokens in the parsed chunk, but protocol events (role
89+
// deltas, finish reasons, usage-only events, Responses API lifecycle events) inflate it, a
90+
// server that batches tokens into one event deflates it, and the scan recognizes only the
91+
// "data: " framing this parser already assumes (no-space prefixes, multi-line data fields,
92+
// and keep-alive data lines skew it further).
93+
StreamedEvents int
8694
}

0 commit comments

Comments
 (0)