Skip to content

Commit 78b6b27

Browse files
committed
Measure EPP processing latency at the handler boundary
Signed-off-by: satyamg1620 <Satyam.Gupta.3@ibm.com>
1 parent d4f9522 commit 78b6b27

7 files changed

Lines changed: 59 additions & 39 deletions

File tree

docs/metrics.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,12 @@ Label `{name}` (the pool name).
112112

113113
### EPP processing overhead
114114

115-
Unlabelled. Together these cover the EPP's own cost on the request and response paths.
115+
Unlabelled.
116116

117117
| Name | Type | Notes |
118118
|---|---|---|
119-
| `request_processing_duration_seconds` | Histogram | Request orchestration latency, from receipt through endpoint selection and request preparation; excludes admission control. |
120-
| `response_processing_duration_seconds` | Histogram | Response handling latency, accumulated across the response handlers; excludes model-server generation time. |
119+
| `request_processing_duration_seconds` | Histogram | Time from request receipt until the request body has been handled. Includes admission control, so under the flow control feature gate this covers queue wait; `flow_control_request_queue_duration_seconds` separates it out. |
120+
| `response_processing_duration_seconds` | Histogram | Sum of the per-chunk handler slices for a streamed response, so model-server generation time between chunks is excluded. For a non-streaming response, the interval from response headers to completion. |
121121

122122
### Plugin, info, and model rewrite
123123

pkg/epp/handlers/server.go

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,13 @@ 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.
139+
// responseProcessingDuration is the EPP cost of handling the response. For a
140+
// streamed response it is the sum of the per-chunk handler slices, since the
141+
// gaps between chunks are model server generation time. For a non-streaming
142+
// response it is the single interval from responseHeadersReceivedAt onward,
143+
// during which the response is entirely in EPP's hands.
141144
responseProcessingDuration time.Duration
145+
responseHeadersReceivedAt time.Time
142146

143147
Response *Response
144148

@@ -269,6 +273,14 @@ func (s *StreamingServer) Process(srv extProcPb.ExternalProcessor_ProcessServer)
269273
},
270274
}
271275

276+
// Request-phase failures (parser resolution, body parsing, admission
277+
// rejection) leave the switch before the success path, so both call this.
278+
// Flow-control rejections carry the queue wait and are the slowest samples;
279+
// dropping them would bias the histogram low.
280+
recordRequestProcessing := sync.OnceFunc(func() {
281+
metrics.RecordRequestProcessingLatency(time.Since(reqCtx.RequestReceivedTimestamp))
282+
})
283+
272284
// Record EPP response processing latency once when the stream ends. Using a
273285
// defer (rather than emitting on end-of-stream) ensures aborted streams
274286
// (client cancel or upstream error before EOS) are also recorded, so the
@@ -476,11 +488,13 @@ func (s *StreamingServer) Process(srv extProcPb.ExternalProcessor_ProcessServer)
476488
if parseResult.SkipResponseProcessing {
477489
reqCtx.RequestState = RequestResponseProcessingSkipped
478490
}
491+
492+
recordRequestProcessing()
479493
}
480494
case *extProcPb.ProcessingRequest_RequestTrailers:
481495
// This is currently unused.
482496
case *extProcPb.ProcessingRequest_ResponseHeaders:
483-
respHeaderStart := time.Now()
497+
respHeadersReceivedAt := time.Now()
484498
for _, header := range v.ResponseHeaders.Headers.GetHeaders() {
485499
value := string(header.RawValue)
486500
loggerTrace.Info("header", "key", header.Key, "value", value)
@@ -494,7 +508,8 @@ func (s *StreamingServer) Process(srv extProcPb.ExternalProcessor_ProcessServer)
494508
reqCtx.RequestState = ResponseReceived
495509
reqCtx = s.HandleResponseHeaders(ctx, reqCtx, v)
496510
reqCtx.respHeaderResp = s.generateResponseHeaderResponse(reqCtx)
497-
reqCtx.responseProcessingDuration += time.Since(respHeaderStart)
511+
reqCtx.responseHeadersReceivedAt = respHeadersReceivedAt
512+
reqCtx.responseProcessingDuration += time.Since(respHeadersReceivedAt)
498513

499514
case *extProcPb.ProcessingRequest_ResponseBody:
500515
endOfStream := v.ResponseBody.EndOfStream
@@ -532,6 +547,7 @@ func (s *StreamingServer) Process(srv extProcPb.ExternalProcessor_ProcessServer)
532547

533548
// Handle the err and fire an immediate response.
534549
if err != nil {
550+
recordRequestProcessing()
535551
if logger.V(logutil.DEBUG).Enabled() {
536552
logger.V(logutil.DEBUG).Error(err, "Failed to process request", "request", req)
537553
} else {
@@ -582,7 +598,13 @@ func (s *StreamingServer) finishResponse(ctx context.Context, reqCtx *RequestCon
582598
// For non-streaming response, we send response back to envoy after receiving all the response body.
583599
reqCtx.respBodyResp = generateResponseBodyResponses(body, setEos, reqCtx.Response.DynamicMetadata)
584600
}
585-
reqCtx.responseProcessingDuration += time.Since(start)
601+
if modelStreaming || reqCtx.responseHeadersReceivedAt.IsZero() {
602+
reqCtx.responseProcessingDuration += time.Since(start)
603+
} else {
604+
// Supersedes the header slice already accumulated: the interval since the
605+
// response headers arrived covers it and the body wait in between.
606+
reqCtx.responseProcessingDuration = time.Since(reqCtx.responseHeadersReceivedAt)
607+
}
586608
}
587609

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

pkg/epp/metrics/llm_d_router_metrics.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -284,9 +284,9 @@ var (
284284
prometheus.HistogramOpts{
285285
Subsystem: LLMDRouterEndpointPickerSubsystem,
286286
Name: "request_processing_duration_seconds",
287-
Help: metricsutil.HelpMsgWithStability("EPP request orchestration latency distribution in seconds, from request receipt through endpoint selection and request preparation, excluding admission-control time.", compbasemetrics.ALPHA),
287+
Help: metricsutil.HelpMsgWithStability("EPP request processing latency distribution in seconds, from request receipt until the request body has been handled, including admission control.", compbasemetrics.ALPHA),
288288
Buckets: []float64{
289-
0.0001, 0.0002, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1,
289+
0.0005, 0.001, 0.002, 0.005, 0.01, 0.015, 0.025, 0.04, 0.06, 0.1, 0.25, 0.5, 1, 2.5, 5, 10,
290290
},
291291
},
292292
[]string{},
@@ -296,9 +296,9 @@ var (
296296
prometheus.HistogramOpts{
297297
Subsystem: LLMDRouterEndpointPickerSubsystem,
298298
Name: "response_processing_duration_seconds",
299-
Help: metricsutil.HelpMsgWithStability("EPP response processing latency distribution in seconds, accumulated across response handlers and excluding model server generation time.", compbasemetrics.ALPHA),
299+
Help: metricsutil.HelpMsgWithStability("EPP response processing latency distribution in seconds: the sum of per-chunk handler time for a streamed response, or the interval from response headers to completion for a non-streaming response.", compbasemetrics.ALPHA),
300300
Buckets: []float64{
301-
0.0001, 0.0002, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1,
301+
0.0001, 0.00025, 0.0005, 0.001, 0.0025, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5,
302302
},
303303
},
304304
[]string{},

pkg/epp/metrics/metrics.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -770,14 +770,13 @@ func RecordSchedulerE2ELatency(duration time.Duration) {
770770
}
771771

772772
// RecordRequestProcessingLatency records the EPP request processing latency,
773-
// measured from request receipt through endpoint selection and request
774-
// preparation, excluding admission-control time.
773+
// measured from request receipt until the request body has been handled.
775774
func RecordRequestProcessingLatency(duration time.Duration) {
776775
llmdRequestProcessingLatency.WithLabelValues().Observe(duration.Seconds())
777776
}
778777

779-
// RecordResponseProcessingLatency records the EPP response processing latency,
780-
// accumulated across response handlers for a single request.
778+
// RecordResponseProcessingLatency records the EPP response processing latency
779+
// for a single request.
781780
func RecordResponseProcessingLatency(duration time.Duration) {
782781
llmdResponseProcessingLatency.WithLabelValues().Observe(duration.Seconds())
783782
}
Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
1-
# HELP llm_d_epp_request_processing_duration_seconds [ALPHA] EPP request orchestration latency distribution in seconds, from request receipt through endpoint selection and request preparation, excluding admission-control time.
1+
# HELP llm_d_epp_request_processing_duration_seconds [ALPHA] EPP request processing latency distribution in seconds, from request receipt until the request body has been handled, including admission control.
22
# TYPE llm_d_epp_request_processing_duration_seconds histogram
3-
llm_d_epp_request_processing_duration_seconds_bucket{le="0.0001"} 0
4-
llm_d_epp_request_processing_duration_seconds_bucket{le="0.0002"} 1
53
llm_d_epp_request_processing_duration_seconds_bucket{le="0.0005"} 1
64
llm_d_epp_request_processing_duration_seconds_bucket{le="0.001"} 2
75
llm_d_epp_request_processing_duration_seconds_bucket{le="0.002"} 3
86
llm_d_epp_request_processing_duration_seconds_bucket{le="0.005"} 4
97
llm_d_epp_request_processing_duration_seconds_bucket{le="0.01"} 5
10-
llm_d_epp_request_processing_duration_seconds_bucket{le="0.02"} 6
11-
llm_d_epp_request_processing_duration_seconds_bucket{le="0.05"} 7
8+
llm_d_epp_request_processing_duration_seconds_bucket{le="0.015"} 6
9+
llm_d_epp_request_processing_duration_seconds_bucket{le="0.025"} 6
10+
llm_d_epp_request_processing_duration_seconds_bucket{le="0.04"} 7
11+
llm_d_epp_request_processing_duration_seconds_bucket{le="0.06"} 7
1212
llm_d_epp_request_processing_duration_seconds_bucket{le="0.1"} 8
13+
llm_d_epp_request_processing_duration_seconds_bucket{le="0.25"} 9
14+
llm_d_epp_request_processing_duration_seconds_bucket{le="0.5"} 9
15+
llm_d_epp_request_processing_duration_seconds_bucket{le="1"} 9
16+
llm_d_epp_request_processing_duration_seconds_bucket{le="2.5"} 9
17+
llm_d_epp_request_processing_duration_seconds_bucket{le="5"} 9
18+
llm_d_epp_request_processing_duration_seconds_bucket{le="10"} 9
1319
llm_d_epp_request_processing_duration_seconds_bucket{le="+Inf"} 9
1420
llm_d_epp_request_processing_duration_seconds_sum 0.2835
1521
llm_d_epp_request_processing_duration_seconds_count 9
Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1-
# HELP llm_d_epp_response_processing_duration_seconds [ALPHA] EPP response processing latency distribution in seconds, accumulated across response handlers and excluding model server generation time.
1+
# HELP llm_d_epp_response_processing_duration_seconds [ALPHA] EPP response processing latency distribution in seconds: the sum of per-chunk handler time for a streamed response, or the interval from response headers to completion for a non-streaming response.
22
# TYPE llm_d_epp_response_processing_duration_seconds histogram
33
llm_d_epp_response_processing_duration_seconds_bucket{le="0.0001"} 0
4-
llm_d_epp_response_processing_duration_seconds_bucket{le="0.0002"} 1
4+
llm_d_epp_response_processing_duration_seconds_bucket{le="0.00025"} 1
55
llm_d_epp_response_processing_duration_seconds_bucket{le="0.0005"} 1
66
llm_d_epp_response_processing_duration_seconds_bucket{le="0.001"} 2
7-
llm_d_epp_response_processing_duration_seconds_bucket{le="0.002"} 3
7+
llm_d_epp_response_processing_duration_seconds_bucket{le="0.0025"} 3
88
llm_d_epp_response_processing_duration_seconds_bucket{le="0.005"} 4
99
llm_d_epp_response_processing_duration_seconds_bucket{le="0.01"} 5
10-
llm_d_epp_response_processing_duration_seconds_bucket{le="0.02"} 6
10+
llm_d_epp_response_processing_duration_seconds_bucket{le="0.025"} 6
1111
llm_d_epp_response_processing_duration_seconds_bucket{le="0.05"} 7
1212
llm_d_epp_response_processing_duration_seconds_bucket{le="0.1"} 8
13+
llm_d_epp_response_processing_duration_seconds_bucket{le="0.25"} 9
14+
llm_d_epp_response_processing_duration_seconds_bucket{le="0.5"} 9
15+
llm_d_epp_response_processing_duration_seconds_bucket{le="1"} 9
16+
llm_d_epp_response_processing_duration_seconds_bucket{le="2.5"} 9
17+
llm_d_epp_response_processing_duration_seconds_bucket{le="5"} 9
1318
llm_d_epp_response_processing_duration_seconds_bucket{le="+Inf"} 9
1419
llm_d_epp_response_processing_duration_seconds_sum 0.2835
1520
llm_d_epp_response_processing_duration_seconds_count 9

pkg/epp/requestcontrol/director.go

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -232,15 +232,6 @@ 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 measures EPP's own orchestration cost for a
238-
// request (receipt through endpoint selection and request preparation). Time
239-
// spent in admission control is subtracted: under flow control it is
240-
// dominated by the queue wait, which is load- rather than EPP-driven and is
241-
// tracked separately by flow_control_request_queue_duration_seconds.
242-
defer func() { metrics.RecordRequestProcessingLatency(time.Since(start) - admissionWait) }()
243-
244235
tracer := tracing.Tracer("llm-d-router/pkg/epp/requestcontrol")
245236
ctx, span := tracer.Start(ctx, "gateway.request_orchestration", trace.WithSpanKind(trace.SpanKindServer))
246237
defer func() {
@@ -301,11 +292,8 @@ func (d *Director) HandleRequest(ctx context.Context, reqCtx *handlers.RequestCo
301292
}
302293

303294
// Admit may block until flow control admits the request.
304-
admitStart := time.Now()
305-
admitErr := d.admissionController.Admit(ctx, reqCtx, priority)
306-
admissionWait = time.Since(admitStart)
307-
if admitErr != nil {
308-
return reqCtx, admitErr
295+
if err := d.admissionController.Admit(ctx, reqCtx, priority); err != nil {
296+
return reqCtx, err
309297
}
310298

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

0 commit comments

Comments
 (0)