Skip to content

Commit 4ae02e8

Browse files
committed
add EPP request/response processing latency metrics
Signed-off-by: satyamg1620 <Satyam.Gupta.3@ibm.com>
1 parent 745bae5 commit 4ae02e8

7 files changed

Lines changed: 150 additions & 2 deletions

File tree

pkg/epp/handlers/server.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ type RequestContext struct {
136136
RequestDroppedReason errcommon.RequestDroppedReason
137137
modelServerStreaming bool
138138

139+
// responseProcessingDuration accumulates the time EPP spends in its response
140+
// handlers across all response events, excluding model server generation time.
141+
responseProcessingDuration time.Duration
142+
139143
Response *Response
140144

141145
reqHeaderResp *extProcPb.ProcessingResponse
@@ -465,6 +469,7 @@ func (s *StreamingServer) Process(srv extProcPb.ExternalProcessor_ProcessServer)
465469
case *extProcPb.ProcessingRequest_RequestTrailers:
466470
// This is currently unused.
467471
case *extProcPb.ProcessingRequest_ResponseHeaders:
472+
respHeaderStart := time.Now()
468473
for _, header := range v.ResponseHeaders.Headers.GetHeaders() {
469474
value := string(header.RawValue)
470475
loggerTrace.Info("header", "key", header.Key, "value", value)
@@ -478,12 +483,14 @@ func (s *StreamingServer) Process(srv extProcPb.ExternalProcessor_ProcessServer)
478483
reqCtx.RequestState = ResponseReceived
479484
reqCtx = s.HandleResponseHeaders(ctx, reqCtx, v)
480485
reqCtx.respHeaderResp = s.generateResponseHeaderResponse(reqCtx)
486+
reqCtx.responseProcessingDuration += time.Since(respHeaderStart)
481487

482488
case *extProcPb.ProcessingRequest_ResponseBody:
483489
endOfStream := v.ResponseBody.EndOfStream
484490
chunk := v.ResponseBody.Body
485491

486492
if reqCtx.modelServerStreaming {
493+
respBodyStart := time.Now()
487494
if endOfStream {
488495
reqCtx.ResponseComplete = true
489496
reqCtx.ResponseCompleteTimestamp = time.Now()
@@ -493,6 +500,10 @@ func (s *StreamingServer) Process(srv extProcPb.ExternalProcessor_ProcessServer)
493500
chunk = rewriteModelName(chunk, reqCtx.TargetModelName, reqCtx.IncomingModelName)
494501
// For streaming response, we send response chunk back to envoy every time we received it.
495502
reqCtx.respBodyResp = generateResponseBodyResponses(chunk, endOfStream, reqCtx.Response.DynamicMetadata)
503+
reqCtx.responseProcessingDuration += time.Since(respBodyStart)
504+
if endOfStream {
505+
metrics.RecordResponseProcessingLatency(reqCtx.responseProcessingDuration)
506+
}
496507
} else {
497508
respBody = append(respBody, chunk...)
498509
if endOfStream {
@@ -553,6 +564,7 @@ func (s *StreamingServer) finishResponse(ctx context.Context, reqCtx *RequestCon
553564
return
554565
}
555566

567+
start := time.Now()
556568
reqCtx.ResponseComplete = true
557569
reqCtx.ResponseCompleteTimestamp = time.Now()
558570
reqCtx = s.HandleResponseBody(ctx, reqCtx, body, true)
@@ -562,6 +574,8 @@ func (s *StreamingServer) finishResponse(ctx context.Context, reqCtx *RequestCon
562574
// For non-streaming response, we send response back to envoy after receiving all the response body.
563575
reqCtx.respBodyResp = generateResponseBodyResponses(body, setEos, reqCtx.Response.DynamicMetadata)
564576
}
577+
reqCtx.responseProcessingDuration += time.Since(start)
578+
metrics.RecordResponseProcessingLatency(reqCtx.responseProcessingDuration)
565579
}
566580

567581
// rewriteModelName replaces occurrences of the target (internal) model name with the

pkg/epp/metrics/llm_d_router_metrics.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,30 @@ var (
252252
},
253253
[]string{"extension_point", "plugin_type", "plugin_name"},
254254
)
255+
256+
llmdRequestProcessingLatency = prometheus.NewHistogramVec(
257+
prometheus.HistogramOpts{
258+
Subsystem: LLMDRouterEndpointPickerSubsystem,
259+
Name: "request_processing_duration_seconds",
260+
Help: metricsutil.HelpMsgWithStability("EPP request processing latency distribution in seconds, from request receipt until an endpoint is selected, excluding flow-control admission wait.", compbasemetrics.ALPHA),
261+
Buckets: []float64{
262+
0.0001, 0.0002, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1,
263+
},
264+
},
265+
[]string{},
266+
)
267+
268+
llmdResponseProcessingLatency = prometheus.NewHistogramVec(
269+
prometheus.HistogramOpts{
270+
Subsystem: LLMDRouterEndpointPickerSubsystem,
271+
Name: "response_processing_duration_seconds",
272+
Help: metricsutil.HelpMsgWithStability("EPP response processing latency distribution in seconds, accumulated across response handlers and excluding model server generation time.", compbasemetrics.ALPHA),
273+
Buckets: []float64{
274+
0.0001, 0.0002, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1,
275+
},
276+
},
277+
[]string{},
278+
)
255279
)
256280

257281
// --- llm-d Info Metrics ---

pkg/epp/metrics/metrics.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,8 @@ func Register(customCollectors ...prometheus.Collector) {
455455
metrics.Registry.MustRegister(llmdSchedulerAttemptsTotal)
456456
metrics.Registry.MustRegister(pluginProcessingLatencies)
457457
metrics.Registry.MustRegister(llmdPluginProcessingLatencies)
458+
metrics.Registry.MustRegister(llmdRequestProcessingLatency)
459+
metrics.Registry.MustRegister(llmdResponseProcessingLatency)
458460
metrics.Registry.MustRegister(inferenceExtensionInfo)
459461
metrics.Registry.MustRegister(llmdInferenceExtensionInfo)
460462
metrics.Registry.MustRegister(flowControlRequestQueueDuration)
@@ -521,6 +523,8 @@ func Reset() {
521523
llmdSchedulerAttemptsTotal.Reset()
522524
pluginProcessingLatencies.Reset()
523525
llmdPluginProcessingLatencies.Reset()
526+
llmdRequestProcessingLatency.Reset()
527+
llmdResponseProcessingLatency.Reset()
524528
inferenceExtensionInfo.Reset()
525529
llmdInferenceExtensionInfo.Reset()
526530
flowControlRequestQueueDuration.Reset()
@@ -715,6 +719,18 @@ func RecordSchedulerE2ELatency(duration time.Duration) {
715719
llmdSchedulerE2ELatency.WithLabelValues().Observe(duration.Seconds())
716720
}
717721

722+
// RecordRequestProcessingLatency records the EPP request processing latency,
723+
// measured from request receipt until an endpoint is selected.
724+
func RecordRequestProcessingLatency(duration time.Duration) {
725+
llmdRequestProcessingLatency.WithLabelValues().Observe(duration.Seconds())
726+
}
727+
728+
// RecordResponseProcessingLatency records the EPP response processing latency,
729+
// accumulated across response handlers for a single request.
730+
func RecordResponseProcessingLatency(duration time.Duration) {
731+
llmdResponseProcessingLatency.WithLabelValues().Observe(duration.Seconds())
732+
}
733+
718734
// RecordSchedulerAttempt records a scheduling attempt with status and endpoint information.
719735
func RecordSchedulerAttempt(err error, targetModelName string, result *fwksched.SchedulingResult) {
720736
if err != nil {

pkg/epp/metrics/metrics_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,60 @@ func TestSchedulerE2ELatency(t *testing.T) {
845845
}
846846
}
847847

848+
func TestRequestProcessingLatency(t *testing.T) {
849+
Reset()
850+
durations := []time.Duration{
851+
200 * time.Microsecond,
852+
800 * time.Microsecond,
853+
1500 * time.Microsecond,
854+
3 * time.Millisecond,
855+
8 * time.Millisecond,
856+
15 * time.Millisecond,
857+
30 * time.Millisecond,
858+
75 * time.Millisecond,
859+
150 * time.Millisecond,
860+
}
861+
for _, duration := range durations {
862+
RecordRequestProcessingLatency(duration)
863+
}
864+
865+
want, err := os.Open("testdata/llm_d_request_processing_duration_seconds_metric")
866+
if err != nil {
867+
t.Fatal(err)
868+
}
869+
defer want.Close()
870+
if err := promtestutil.GatherAndCompare(metrics.Registry, want, "llm_d_router_epp_request_processing_duration_seconds"); err != nil {
871+
t.Error(err)
872+
}
873+
}
874+
875+
func TestResponseProcessingLatency(t *testing.T) {
876+
Reset()
877+
durations := []time.Duration{
878+
200 * time.Microsecond,
879+
800 * time.Microsecond,
880+
1500 * time.Microsecond,
881+
3 * time.Millisecond,
882+
8 * time.Millisecond,
883+
15 * time.Millisecond,
884+
30 * time.Millisecond,
885+
75 * time.Millisecond,
886+
150 * time.Millisecond,
887+
}
888+
for _, duration := range durations {
889+
RecordResponseProcessingLatency(duration)
890+
}
891+
892+
want, err := os.Open("testdata/llm_d_response_processing_duration_seconds_metric")
893+
if err != nil {
894+
t.Fatal(err)
895+
}
896+
defer want.Close()
897+
if err := promtestutil.GatherAndCompare(metrics.Registry, want, "llm_d_router_epp_response_processing_duration_seconds"); err != nil {
898+
t.Error(err)
899+
}
900+
}
901+
848902
func TestFlowControlDispatchCycleLengthMetric(t *testing.T) {
849903
Reset()
850904
scenarios := []struct {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# HELP llm_d_router_epp_request_processing_duration_seconds [ALPHA] EPP request processing latency distribution in seconds, from request receipt until an endpoint is selected, excluding flow-control admission wait.
2+
# TYPE llm_d_router_epp_request_processing_duration_seconds histogram
3+
llm_d_router_epp_request_processing_duration_seconds_bucket{le="0.0001"} 0
4+
llm_d_router_epp_request_processing_duration_seconds_bucket{le="0.0002"} 1
5+
llm_d_router_epp_request_processing_duration_seconds_bucket{le="0.0005"} 1
6+
llm_d_router_epp_request_processing_duration_seconds_bucket{le="0.001"} 2
7+
llm_d_router_epp_request_processing_duration_seconds_bucket{le="0.002"} 3
8+
llm_d_router_epp_request_processing_duration_seconds_bucket{le="0.005"} 4
9+
llm_d_router_epp_request_processing_duration_seconds_bucket{le="0.01"} 5
10+
llm_d_router_epp_request_processing_duration_seconds_bucket{le="0.02"} 6
11+
llm_d_router_epp_request_processing_duration_seconds_bucket{le="0.05"} 7
12+
llm_d_router_epp_request_processing_duration_seconds_bucket{le="0.1"} 8
13+
llm_d_router_epp_request_processing_duration_seconds_bucket{le="+Inf"} 9
14+
llm_d_router_epp_request_processing_duration_seconds_sum 0.2835
15+
llm_d_router_epp_request_processing_duration_seconds_count 9
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# HELP llm_d_router_epp_response_processing_duration_seconds [ALPHA] EPP response processing latency distribution in seconds, accumulated across response handlers and excluding model server generation time.
2+
# TYPE llm_d_router_epp_response_processing_duration_seconds histogram
3+
llm_d_router_epp_response_processing_duration_seconds_bucket{le="0.0001"} 0
4+
llm_d_router_epp_response_processing_duration_seconds_bucket{le="0.0002"} 1
5+
llm_d_router_epp_response_processing_duration_seconds_bucket{le="0.0005"} 1
6+
llm_d_router_epp_response_processing_duration_seconds_bucket{le="0.001"} 2
7+
llm_d_router_epp_response_processing_duration_seconds_bucket{le="0.002"} 3
8+
llm_d_router_epp_response_processing_duration_seconds_bucket{le="0.005"} 4
9+
llm_d_router_epp_response_processing_duration_seconds_bucket{le="0.01"} 5
10+
llm_d_router_epp_response_processing_duration_seconds_bucket{le="0.02"} 6
11+
llm_d_router_epp_response_processing_duration_seconds_bucket{le="0.05"} 7
12+
llm_d_router_epp_response_processing_duration_seconds_bucket{le="0.1"} 8
13+
llm_d_router_epp_response_processing_duration_seconds_bucket{le="+Inf"} 9
14+
llm_d_router_epp_response_processing_duration_seconds_sum 0.2835
15+
llm_d_router_epp_response_processing_duration_seconds_count 9

pkg/epp/requestcontrol/director.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,13 @@ func (d *Director) getInferenceObjective(ctx context.Context, reqCtx *handlers.R
232232
// HandleRequest orchestrates the request lifecycle.
233233
// It always returns the requestContext even in the error case, as the request context is used in error handling.
234234
func (d *Director) HandleRequest(ctx context.Context, reqCtx *handlers.RequestContext, inferenceRequestBody *fwkrh.InferenceRequestBody) (_ *handlers.RequestContext, err error) {
235+
start := time.Now()
236+
var admissionWait time.Duration
237+
// request_processing_duration captures EPP's own processing cost, so the
238+
// flow-control admission wait (tracked separately by
239+
// flow_control_request_queue_duration_seconds) is subtracted out.
240+
defer func() { metrics.RecordRequestProcessingLatency(time.Since(start) - admissionWait) }()
241+
235242
tracer := tracing.Tracer("llm-d-router/pkg/epp/requestcontrol")
236243
ctx, span := tracer.Start(ctx, "gateway.request_orchestration", trace.WithSpanKind(trace.SpanKindServer))
237244
defer func() {
@@ -284,8 +291,11 @@ func (d *Director) HandleRequest(ctx context.Context, reqCtx *handlers.RequestCo
284291
}
285292

286293
// Admit may block until flow control admits the request.
287-
if err := d.admissionController.Admit(ctx, reqCtx, priority); err != nil {
288-
return reqCtx, err
294+
admitStart := time.Now()
295+
admitErr := d.admissionController.Admit(ctx, reqCtx, priority)
296+
admissionWait = time.Since(admitStart)
297+
if admitErr != nil {
298+
return reqCtx, admitErr
289299
}
290300

291301
endpointCandidates := d.endpointCandidates.Locate(ctx, reqCtx.Request.Metadata)

0 commit comments

Comments
 (0)