Skip to content

Commit 0a53e4c

Browse files
committed
feature: add slo metrics to flowcontrol
Signed-off-by: Loic Marchal <lmarchal@redhat.com>
1 parent aa3e272 commit 0a53e4c

7 files changed

Lines changed: 300 additions & 31 deletions

File tree

pkg/epp/flowcontrol/controller/controller.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"github.com/llm-d/llm-d-router/pkg/epp/flowcontrol/contracts"
3737
"github.com/llm-d/llm-d-router/pkg/epp/flowcontrol/controller/internal"
3838
"github.com/llm-d/llm-d-router/pkg/epp/flowcontrol/types"
39+
fwkrequest "github.com/llm-d/llm-d-router/pkg/epp/framework/common/request"
3940
"github.com/llm-d/llm-d-router/pkg/epp/framework/interface/flowcontrol"
4041
"github.com/llm-d/llm-d-router/pkg/epp/metrics"
4142
)
@@ -214,21 +215,27 @@ func (fc *FlowController) EnqueueAndWait(
214215
flowKey := req.FlowKey()
215216
priority := strconv.Itoa(flowKey.Priority)
216217
reqBytes := req.ByteSize()
218+
sloClass := metrics.ClassifySLO(extractHeader(req, fwkrequest.TTFTSLOMsHeaderKey))
219+
metrics.RecordFlowControlSLOIncomingRequest(sloClass, req.InferencePoolName())
217220
metrics.IncFlowControlQueueSize(
218221
flowKey.ID, priority,
219222
req.InferencePoolName(),
223+
sloClass,
220224
req.ModelName(), req.TargetModelName())
221225
defer metrics.DecFlowControlQueueSize(
222226
flowKey.ID, priority,
223227
req.InferencePoolName(),
228+
sloClass,
224229
req.ModelName(), req.TargetModelName())
225230
metrics.AddFlowControlQueueBytes(
226231
flowKey.ID, priority,
227232
req.InferencePoolName(),
233+
sloClass,
228234
req.ModelName(), req.TargetModelName(), reqBytes)
229235
defer metrics.SubFlowControlQueueBytes(
230236
flowKey.ID, priority,
231237
req.InferencePoolName(),
238+
sloClass,
232239
req.ModelName(), req.TargetModelName(), reqBytes)
233240

234241
// 1. Create the derived context that governs this request's lifecycle (Parent Cancellation + TTL).
@@ -331,6 +338,13 @@ func (fc *FlowController) withConnectionWithFallback(
331338
return fn(conn, fallback)
332339
})
333340
}
341+
func extractHeader(req flowcontrol.FlowControlRequest, name string) string {
342+
infReq := req.InferenceRequest()
343+
if infReq == nil || infReq.Headers == nil {
344+
return ""
345+
}
346+
return fwkrequest.GetHeader(infReq.Headers, name)
347+
}
334348

335349
// tryDistribution handles a single attempt to select a shard and submit a request.
336350
// It uses the provided `conn` to access the registry data plane.

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"time"
2727

2828
"github.com/llm-d/llm-d-router/pkg/epp/flowcontrol/types"
29+
"github.com/llm-d/llm-d-router/pkg/epp/framework/common/request"
2930
"github.com/llm-d/llm-d-router/pkg/epp/framework/interface/flowcontrol"
3031
"github.com/llm-d/llm-d-router/pkg/epp/metrics"
3132
)
@@ -159,16 +160,30 @@ func (fi *FlowItem) finalizeInternal(outcome types.QueueOutcome, err error) {
159160

160161
duration := time.Since(fi.enqueueTime)
161162
flowKey := fi.originalRequest.FlowKey()
163+
outcomeStr := outcome.String()
162164
metrics.RecordFlowControlRequestQueueDuration(
163-
flowKey.ID, strconv.Itoa(flowKey.Priority), outcome.String(),
165+
flowKey.ID, strconv.Itoa(flowKey.Priority), outcomeStr,
164166
fi.originalRequest.InferencePoolName(),
165167
fi.OriginalRequest().ModelName(), fi.OriginalRequest().TargetModelName(),
166168
duration)
167169

170+
sloClass := metrics.ClassifySLO(extractHeader(fi.originalRequest, request.TTFTSLOMsHeaderKey))
171+
metrics.RecordFlowControlSLORequestQueueDuration(
172+
sloClass, outcomeStr, fi.originalRequest.InferencePoolName(),
173+
duration)
174+
168175
fi.done <- finalState
169176
close(fi.done)
170177
}
171178

179+
func extractHeader(req flowcontrol.FlowControlRequest, name string) string {
180+
infReq := req.InferenceRequest()
181+
if infReq == nil || infReq.Headers == nil {
182+
return ""
183+
}
184+
return request.GetHeader(infReq.Headers, name)
185+
}
186+
172187
// inferOutcome determines the correct QueueOutcome and Error based on the cause of finalization and whether the item
173188
// was already admitted to a queue.
174189
func inferOutcome(cause error, isQueued bool) (types.QueueOutcome, error) {

pkg/epp/framework/common/request/headers.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ package request
1818

1919
import "strings"
2020

21+
const (
22+
// TTFTSLOMsHeaderKey is the request header name for SLO time-to-first-token in milliseconds.
23+
TTFTSLOMsHeaderKey = "x-slo-ttft-ms"
24+
// TPOTSLOMsHeaderKey is the request header name for SLO time-per-output-token in milliseconds.
25+
TPOTSLOMsHeaderKey = "x-slo-tpot-ms"
26+
)
27+
2128
// GetHeader returns the value for key from headers, with case-insensitive lookup.
2229
func GetHeader(headers map[string]string, key string) string {
2330
if v, ok := headers[key]; ok {

pkg/epp/framework/plugins/flowcontrol/ordering/slodeadline/slo_deadline.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ import (
2626
"strings"
2727
"time"
2828

29+
"github.com/llm-d/llm-d-router/pkg/epp/framework/common/request"
2930
"github.com/llm-d/llm-d-router/pkg/epp/framework/interface/flowcontrol"
3031
"github.com/llm-d/llm-d-router/pkg/epp/framework/interface/plugin"
31-
"github.com/llm-d/llm-d-router/pkg/epp/metadata"
3232
)
3333

3434
const (
@@ -37,9 +37,6 @@ const (
3737
// It selects the request with the earliest SLO-based deadline.
3838
// For detailed documentation, see README.md.
3939
SLODeadlineOrderingPolicyType = "slo-deadline-ordering-policy"
40-
41-
// sloTtftHeader is the request header name for SLO time-to-first-token in milliseconds.
42-
sloTtftHeader = metadata.TTFTSLOHeaderKey
4340
)
4441

4542
func SLODeadlineOrderingPolicyFactory(name string, _ *json.Decoder, _ plugin.Handle) (plugin.Plugin, error) {
@@ -95,11 +92,11 @@ func calculateSLODeadline(item flowcontrol.QueueItemAccessor) time.Time {
9592
if infReq == nil || infReq.Headers == nil {
9693
return sloMaxDeadlineTime
9794
}
98-
sloTtft, _ := metadata.GetLowerCaseHeaderValue(infReq.Headers, sloTtftHeader)
99-
if sloTtft == "" {
95+
sloTTFT := request.GetHeader(infReq.Headers, request.TTFTSLOMsHeaderKey)
96+
if sloTTFT == "" {
10097
return sloMaxDeadlineTime
10198
}
102-
ms, err := strconv.ParseInt(strings.TrimSpace(sloTtft), 10, 64)
99+
ms, err := strconv.ParseInt(strings.TrimSpace(sloTTFT), 10, 64)
103100
if err != nil || ms < 0 {
104101
return sloMaxDeadlineTime
105102
}

pkg/epp/framework/plugins/flowcontrol/ordering/slodeadline/slo_deadline_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/stretchr/testify/assert"
2424
"github.com/stretchr/testify/require"
2525

26+
fwkrequest "github.com/llm-d/llm-d-router/pkg/epp/framework/common/request"
2627
"github.com/llm-d/llm-d-router/pkg/epp/framework/interface/flowcontrol"
2728
"github.com/llm-d/llm-d-router/pkg/epp/framework/interface/flowcontrol/mocks"
2829
"github.com/llm-d/llm-d-router/pkg/epp/framework/interface/scheduling"
@@ -55,7 +56,7 @@ func TestSLODeadlinePolicy_RequiredQueueCapabilities(t *testing.T) {
5556
func makeSLOItem(id string, received time.Time, sloTTFTMs string) flowcontrol.QueueItemAccessor {
5657
req := mocks.NewMockFlowControlRequest(10, id, testFlowKey)
5758
req.ReceivedTimestampV = received
58-
req.InferenceRequestV = &scheduling.InferenceRequest{Headers: map[string]string{sloTtftHeader: sloTTFTMs}}
59+
req.InferenceRequestV = &scheduling.InferenceRequest{Headers: map[string]string{fwkrequest.TTFTSLOMsHeaderKey: sloTTFTMs}}
5960
return &mocks.MockQueueItemAccessor{
6061
EffectiveTTLV: 0,
6162
OriginalRequestV: req,
@@ -116,7 +117,7 @@ func TestCalculateSLODeadline(t *testing.T) {
116117
// Valid header
117118
reqValid := mocks.NewMockFlowControlRequest(1, "valid", testFlowKey)
118119
reqValid.ReceivedTimestampV = now
119-
reqValid.InferenceRequestV = &scheduling.InferenceRequest{Headers: map[string]string{sloTtftHeader: "200"}}
120+
reqValid.InferenceRequestV = &scheduling.InferenceRequest{Headers: map[string]string{fwkrequest.TTFTSLOMsHeaderKey: "200"}}
120121
accValid := &mocks.MockQueueItemAccessor{OriginalRequestV: reqValid}
121122
deadline := calculateSLODeadline(accValid)
122123
assert.Equal(t, now.Add(200*time.Millisecond), deadline)
@@ -150,7 +151,7 @@ func TestCalculateSLODeadline(t *testing.T) {
150151

151152
// Invalid value
152153
reqInvalid := mocks.NewMockFlowControlRequest(3, "inv", testFlowKey)
153-
reqInvalid.InferenceRequestV = &scheduling.InferenceRequest{Headers: map[string]string{sloTtftHeader: "x"}}
154+
reqInvalid.InferenceRequestV = &scheduling.InferenceRequest{Headers: map[string]string{fwkrequest.TTFTSLOMsHeaderKey: "x"}}
154155
accInvalid := &mocks.MockQueueItemAccessor{OriginalRequestV: reqInvalid}
155156
assert.Equal(t, sloMaxDeadlineTime, calculateSLODeadline(accInvalid))
156157

pkg/epp/metrics/metrics.go

Lines changed: 88 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,27 @@ var (
312312
append([]string{"fairness_id", "priority", "outcome", "inference_pool"}, modelLabels...),
313313
)
314314

315+
flowControlSLORequestQueueDuration = prometheus.NewHistogramVec(
316+
prometheus.HistogramOpts{
317+
Subsystem: inferenceExtension,
318+
Name: "flow_control_slo_request_queue_duration_seconds",
319+
Help: metricsutil.HelpMsgWithStability("Distribution of the total time requests spend in the EPP flow control layer, partitioned by SLO class.", compbasemetrics.ALPHA),
320+
Buckets: []float64{
321+
0.0001, 0.0005, 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0,
322+
},
323+
},
324+
[]string{"slo_class", "outcome", "inference_pool"},
325+
)
326+
327+
flowControlSLOIncomingRequestsTotal = prometheus.NewCounterVec(
328+
prometheus.CounterOpts{
329+
Subsystem: inferenceExtension,
330+
Name: "flow_control_slo_incoming_requests_total",
331+
Help: metricsutil.HelpMsgWithStability("Total number of requests that entered the EPP flow control layer via EnqueueAndWait.", compbasemetrics.ALPHA),
332+
},
333+
[]string{"slo_class", "inference_pool"},
334+
)
335+
315336
// Deprecated: Use llm_d_router_epp_flow_control_dispatch_cycle_duration_seconds instead.
316337
// Tracked in: https://github.com/llm-d/llm-d-inference-scheduler/issues/1070
317338
flowControlDispatchCycleDuration = prometheus.NewHistogramVec(
@@ -348,7 +369,7 @@ var (
348369
Name: "flow_control_queue_size",
349370
Help: metricsutil.HelpMsgWithStability("[Deprecated: Use llm_d_router_epp_flow_control_queue_size] Current number of requests actively held in the Flow Control queue.", compbasemetrics.ALPHA),
350371
},
351-
append([]string{"fairness_id", "priority", "inference_pool"}, modelLabels...),
372+
append([]string{"fairness_id", "priority", "inference_pool", "slo_class"}, modelLabels...),
352373
)
353374

354375
// Deprecated: Use llm_d_router_epp_flow_control_queue_bytes instead.
@@ -359,7 +380,7 @@ var (
359380
Name: "flow_control_queue_bytes",
360381
Help: metricsutil.HelpMsgWithStability("[Deprecated: Use llm_d_router_epp_flow_control_queue_bytes] Current total size in bytes of requests actively held in the Flow Control queue.", compbasemetrics.ALPHA),
361382
},
362-
append([]string{"fairness_id", "priority", "inference_pool"}, modelLabels...),
383+
append([]string{"fairness_id", "priority", "inference_pool", "slo_class"}, modelLabels...),
363384
)
364385

365386
// Deprecated: Use llm_d_router_epp_flow_control_pool_saturation instead.
@@ -459,6 +480,8 @@ func Register(customCollectors ...prometheus.Collector) {
459480
metrics.Registry.MustRegister(llmdInferenceExtensionInfo)
460481
metrics.Registry.MustRegister(flowControlRequestQueueDuration)
461482
metrics.Registry.MustRegister(llmdFlowControlRequestQueueDuration)
483+
metrics.Registry.MustRegister(flowControlSLORequestQueueDuration)
484+
metrics.Registry.MustRegister(flowControlSLOIncomingRequestsTotal)
462485
metrics.Registry.MustRegister(flowControlDispatchCycleDuration)
463486
metrics.Registry.MustRegister(llmdFlowControlDispatchCycleDuration)
464487
metrics.Registry.MustRegister(flowControlQueueSize)
@@ -525,6 +548,8 @@ func Reset() {
525548
llmdInferenceExtensionInfo.Reset()
526549
flowControlRequestQueueDuration.Reset()
527550
llmdFlowControlRequestQueueDuration.Reset()
551+
flowControlSLORequestQueueDuration.Reset()
552+
flowControlSLOIncomingRequestsTotal.Reset()
528553
flowControlQueueSize.Reset()
529554
llmdFlowControlQueueSize.Reset()
530555
flowControlQueueBytes.Reset()
@@ -779,6 +804,59 @@ func RecordFlowControlRequestQueueDuration(
779804
).Observe(duration.Seconds())
780805
}
781806

807+
// SLO class constants label for flow control SLO metrics (bounded buckets for the TTFT SLO header in ms).
808+
const (
809+
SLOClassNone = "none"
810+
SLOClassBelowMS200 = "below_ms_200"
811+
SLOClassMS200to399 = "ms_200_399"
812+
SLOClassMS400to599 = "ms_400_599"
813+
SLOClassMS600to799 = "ms_600_799"
814+
SLOClassMS800to1000 = "ms_800_1000"
815+
SLOClassAboveMS1000 = "above_ms_1000"
816+
)
817+
818+
// ClassifySLO maps a raw SLO header value (in milliseconds) to a bounded SLO class label.
819+
// Returns SLOClassNone when the header is absent or unparseable.
820+
func ClassifySLO(rawHeaderValue string) string {
821+
if rawHeaderValue == "" {
822+
return SLOClassNone
823+
}
824+
ms, err := strconv.ParseInt(rawHeaderValue, 10, 64)
825+
if err != nil || ms < 0 {
826+
return SLOClassNone
827+
}
828+
switch {
829+
case ms < 200:
830+
return SLOClassBelowMS200
831+
case ms < 400:
832+
return SLOClassMS200to399
833+
case ms < 600:
834+
return SLOClassMS400to599
835+
case ms < 800:
836+
return SLOClassMS600to799
837+
case ms <= 1000:
838+
return SLOClassMS800to1000
839+
default:
840+
return SLOClassAboveMS1000
841+
}
842+
}
843+
844+
// RecordFlowControlSLORequestQueueDuration records the queue duration for a request partitioned by its
845+
// SLO class (derived from the TTFT SLO header "x-slo-ttft-ms").
846+
func RecordFlowControlSLORequestQueueDuration(
847+
sloClass, outcome, inferencePool string,
848+
duration time.Duration,
849+
) {
850+
flowControlSLORequestQueueDuration.WithLabelValues(
851+
sloClass, outcome, inferencePool,
852+
).Observe(duration.Seconds())
853+
}
854+
855+
// RecordFlowControlSLOIncomingRequest increments the count of requests that entered flow control at EnqueueAndWait.
856+
func RecordFlowControlSLOIncomingRequest(sloClass, inferencePool string) {
857+
flowControlSLOIncomingRequestsTotal.WithLabelValues(sloClass, inferencePool).Inc()
858+
}
859+
782860
// RecordFlowControlDispatchCycleDuration records the duration of a dispatch cycle in the Flow Control layer.
783861
func RecordFlowControlDispatchCycleDuration(duration time.Duration) {
784862
flowControlDispatchCycleDuration.WithLabelValues().Observe(duration.Seconds())
@@ -800,26 +878,26 @@ func RecordFlowControlRequestEnqueueDuration(
800878
}
801879

802880
// IncFlowControlQueueSize increments the Flow Control queue size gauge.
803-
func IncFlowControlQueueSize(fairnessID, priority, inferencePool, modelName, targetModelName string) {
804-
flowControlQueueSize.WithLabelValues(fairnessID, priority, inferencePool, modelName, targetModelName).Inc()
881+
func IncFlowControlQueueSize(fairnessID, priority, inferencePool, sloClass, modelName, targetModelName string) {
882+
flowControlQueueSize.WithLabelValues(fairnessID, priority, inferencePool, sloClass, modelName, targetModelName).Inc()
805883
llmdFlowControlQueueSize.WithLabelValues(fairnessID, priority, inferencePool, modelName, targetModelName).Inc()
806884
}
807885

808886
// DecFlowControlQueueSize decrements the Flow Control queue size gauge.
809-
func DecFlowControlQueueSize(fairnessID, priority, inferencePool, modelName, targetModelName string) {
810-
flowControlQueueSize.WithLabelValues(fairnessID, priority, inferencePool, modelName, targetModelName).Dec()
887+
func DecFlowControlQueueSize(fairnessID, priority, inferencePool, sloClass, modelName, targetModelName string) {
888+
flowControlQueueSize.WithLabelValues(fairnessID, priority, inferencePool, sloClass, modelName, targetModelName).Dec()
811889
llmdFlowControlQueueSize.WithLabelValues(fairnessID, priority, inferencePool, modelName, targetModelName).Dec()
812890
}
813891

814892
// AddFlowControlQueueBytes increments the Flow Control queue bytes gauge.
815-
func AddFlowControlQueueBytes(fairnessID, priority, inferencePool, modelName, targetModelName string, bytes uint64) {
816-
flowControlQueueBytes.WithLabelValues(fairnessID, priority, inferencePool, modelName, targetModelName).Add(float64(bytes))
893+
func AddFlowControlQueueBytes(fairnessID, priority, inferencePool, sloClass, modelName, targetModelName string, bytes uint64) {
894+
flowControlQueueBytes.WithLabelValues(fairnessID, priority, inferencePool, sloClass, modelName, targetModelName).Add(float64(bytes))
817895
llmdFlowControlQueueBytes.WithLabelValues(fairnessID, priority, inferencePool, modelName, targetModelName).Add(float64(bytes))
818896
}
819897

820898
// SubFlowControlQueueBytes decrements the Flow Control queue bytes gauge.
821-
func SubFlowControlQueueBytes(fairnessID, priority, inferencePool, modelName, targetModelName string, bytes uint64) {
822-
flowControlQueueBytes.WithLabelValues(fairnessID, priority, inferencePool, modelName, targetModelName).Sub(float64(bytes))
899+
func SubFlowControlQueueBytes(fairnessID, priority, inferencePool, sloClass, modelName, targetModelName string, bytes uint64) {
900+
flowControlQueueBytes.WithLabelValues(fairnessID, priority, inferencePool, sloClass, modelName, targetModelName).Sub(float64(bytes))
823901
llmdFlowControlQueueBytes.WithLabelValues(fairnessID, priority, inferencePool, modelName, targetModelName).Sub(float64(bytes))
824902
}
825903

0 commit comments

Comments
 (0)