Skip to content

Commit 27ddc50

Browse files
committed
Merge upstream/main into epp-hotpath-log-guards
Signed-off-by: Luke Van Drie <lukevandrie@google.com>
2 parents e6dc2db + bc5d0be commit 27ddc50

18 files changed

Lines changed: 241 additions & 46 deletions

File tree

pkg/common/error/error.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import (
2626
)
2727

2828
// RequestDroppedReasonHeaderKey is the HTTP response header that communicates the specific
29-
// reason a request was dropped by flow control.
29+
// reason the EPP dropped a request.
3030
const RequestDroppedReasonHeaderKey = "x-llm-d-request-dropped-reason"
3131

3232
// RequestDroppedReason is the reason a request was rejected before dispatch or evicted after dispatch.

pkg/epp/framework/plugins/datalayer/extractor/metrics/extractor.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,20 @@ func (ext *Extractor) TypedName() fwkplugin.TypedName {
8787
var _ fwkplugin.ProducerPlugin = &Extractor{}
8888

8989
// Produces declares the custom scalar metric attributes, whose names come from
90-
// the per-engine mappings in configuration. Core metrics land on the Metrics
91-
// struct rather than the attribute map and so are not declared here.
90+
// the per-engine mappings in configuration, plus the per-pod metric fields this
91+
// extractor writes into the endpoint's Metrics struct that a scorer or filter
92+
// declares as a Consumes() dependency. Declaring both lets the data-attribute
93+
// registry and the DAG builder validate the scorers and filters that read them.
94+
// The Metrics field types mirror the fwkdl.Metrics struct (float64, int, map
95+
// of model name to count).
9296
func (ext *Extractor) Produces() map[fwkplugin.DataKey]any {
93-
produced := map[fwkplugin.DataKey]any{}
97+
produced := map[fwkplugin.DataKey]any{
98+
fwkplugin.NewDataKey(KVCacheUsagePercentKey, MetricsExtractorType): float64(0),
99+
fwkplugin.NewDataKey(WaitingQueueSizeKey, MetricsExtractorType): int(0),
100+
fwkplugin.NewDataKey(RunningRequestsSizeKey, MetricsExtractorType): int(0),
101+
fwkplugin.NewDataKey(ActiveModelsKey, MetricsExtractorType): map[string]int{},
102+
fwkplugin.NewDataKey(WaitingModelsKey, MetricsExtractorType): map[string]int{},
103+
}
94104
for _, mapping := range ext.registry.Mappings() {
95105
for _, custom := range mapping.CustomMetrics {
96106
produced[attrmetrics.ScalarMetricDataKey(custom.AttributeKey)] = attrmetrics.ScalarMetricValue(0)

pkg/epp/framework/plugins/scheduling/scorer/endpointattribute/endpointattribute.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ const (
3636
)
3737

3838
// compile-time type assertion
39-
var _ fwksched.Scorer = &EndpointAttributeScorer{}
39+
var (
40+
_ fwksched.Scorer = &EndpointAttributeScorer{}
41+
_ fwkplugin.ConsumerPlugin = &EndpointAttributeScorer{}
42+
)
4043

4144
// fixedRangeParameters normalizes the attribute value against a fixed
4245
// [min, max] range (e.g. kv-cache utilization, which is always in [0, 1]).

pkg/epp/framework/plugins/scheduling/scorer/kvcacheutilization/kvcache_utilization.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ const (
3030
)
3131

3232
// compile-time type assertion
33-
var _ fwksched.Scorer = &KVCacheUtilizationScorer{}
33+
var (
34+
_ fwksched.Scorer = &KVCacheUtilizationScorer{}
35+
_ fwkplugin.ConsumerPlugin = &KVCacheUtilizationScorer{}
36+
)
3437

3538
// KvCacheUtilizationScorerFactory defines the factory function for KVCacheUtilizationScorer.
3639
func KvCacheUtilizationScorerFactory(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
@@ -59,10 +62,15 @@ func (s *KVCacheUtilizationScorer) Category() fwksched.ScorerCategory {
5962
return fwksched.Distribution
6063
}
6164

62-
// Consumes returns the list of data that is consumed by the plugin.
63-
func (s *KVCacheUtilizationScorer) Consumes() map[string]any {
64-
return map[string]any{
65-
metrics.KVCacheUsagePercentKey: float64(0),
65+
// Consumes declares that the scorer reads the KV-cache utilization field
66+
// from the endpoint's Metrics struct. The DataKey names the field the
67+
// core-metrics-extractor publishes; the registry validates the consumer's
68+
// declared type against the producer's declaration.
69+
func (s *KVCacheUtilizationScorer) Consumes() fwkplugin.DataDependencies {
70+
return fwkplugin.DataDependencies{
71+
Required: map[fwkplugin.DataKey]any{
72+
fwkplugin.NewDataKey(metrics.KVCacheUsagePercentKey, metrics.MetricsExtractorType): float64(0),
73+
},
6674
}
6775
}
6876

pkg/epp/framework/plugins/scheduling/scorer/loraaffinity/lora_affinity.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ const (
3030
)
3131

3232
// compile-time type assertion
33-
var _ fwksched.Scorer = &LoraAffinityScorer{}
33+
var (
34+
_ fwksched.Scorer = &LoraAffinityScorer{}
35+
_ fwkplugin.ConsumerPlugin = &LoraAffinityScorer{}
36+
)
3437

3538
// LoraAffinityScorerFactory defines the factory function for LoraAffinityScorer.
3639
func LoraAffinityScorerFactory(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
@@ -59,11 +62,15 @@ func (s *LoraAffinityScorer) Category() fwksched.ScorerCategory {
5962
return fwksched.Affinity
6063
}
6164

62-
// Consumes returns the list of data that is consumed by the plugin.
63-
func (s *LoraAffinityScorer) Consumes() map[string]any {
64-
return map[string]any{
65-
metrics.ActiveModelsKey: map[string]int{},
66-
metrics.WaitingModelsKey: map[string]int{},
65+
// Consumes declares the scorer reads the per-pod active and waiting model
66+
// sets from the endpoint's Metrics struct, published by the core-metrics-
67+
// extractor.
68+
func (s *LoraAffinityScorer) Consumes() fwkplugin.DataDependencies {
69+
return fwkplugin.DataDependencies{
70+
Required: map[fwkplugin.DataKey]any{
71+
fwkplugin.NewDataKey(metrics.ActiveModelsKey, metrics.MetricsExtractorType): map[string]int{},
72+
fwkplugin.NewDataKey(metrics.WaitingModelsKey, metrics.MetricsExtractorType): map[string]int{},
73+
},
6774
}
6875
}
6976

pkg/epp/framework/plugins/scheduling/scorer/queuedepth/queue.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ const (
3131
)
3232

3333
// compile-time type assertion
34-
var _ fwksched.Scorer = &QueueScorer{}
34+
var (
35+
_ fwksched.Scorer = &QueueScorer{}
36+
_ fwkplugin.ConsumerPlugin = &QueueScorer{}
37+
)
3538

3639
// QueueScorerFactory defines the factory function for QueueScorer.
3740
func QueueScorerFactory(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
@@ -61,10 +64,13 @@ func (s *QueueScorer) Category() fwksched.ScorerCategory {
6164
return fwksched.Distribution
6265
}
6366

64-
// Consumes returns the list of data that is consumed by the plugin.
65-
func (s *QueueScorer) Consumes() map[string]any {
66-
return map[string]any{
67-
metrics.WaitingQueueSizeKey: int(0),
67+
// Consumes declares the scorer reads the waiting queue size from the
68+
// endpoint's Metrics struct, published by the core-metrics-extractor.
69+
func (s *QueueScorer) Consumes() fwkplugin.DataDependencies {
70+
return fwkplugin.DataDependencies{
71+
Required: map[fwkplugin.DataKey]any{
72+
fwkplugin.NewDataKey(metrics.WaitingQueueSizeKey, metrics.MetricsExtractorType): int(0),
73+
},
6874
}
6975
}
7076

pkg/epp/framework/plugins/scheduling/scorer/runningrequests/runningrequest.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ const (
3131
)
3232

3333
// compile-time type assertion
34-
var _ fwksched.Scorer = &RunningRequestsSizeScorer{}
34+
var (
35+
_ fwksched.Scorer = &RunningRequestsSizeScorer{}
36+
_ fwkplugin.ConsumerPlugin = &RunningRequestsSizeScorer{}
37+
)
3538

3639
// RunningRequestsSizeScorerFactory defines the factory function for RunningRequestsSizeScorer.
3740
func RunningRequestsSizeScorerFactory(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
@@ -61,10 +64,13 @@ func (s *RunningRequestsSizeScorer) Category() fwksched.ScorerCategory {
6164
return fwksched.Distribution
6265
}
6366

64-
// Consumes returns the list of data that is consumed by the plugin.
65-
func (s *RunningRequestsSizeScorer) Consumes() map[string]any {
66-
return map[string]any{
67-
metrics.RunningRequestsSizeKey: int(0),
67+
// Consumes declares the scorer reads the running requests size from the
68+
// endpoint's Metrics struct, published by the core-metrics-extractor.
69+
func (s *RunningRequestsSizeScorer) Consumes() fwkplugin.DataDependencies {
70+
return fwkplugin.DataDependencies{
71+
Required: map[fwkplugin.DataKey]any{
72+
fwkplugin.NewDataKey(metrics.RunningRequestsSizeKey, metrics.MetricsExtractorType): int(0),
73+
},
6874
}
6975
}
7076

pkg/epp/requestcontrol/director.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -304,17 +304,19 @@ func (d *Director) HandleRequest(ctx context.Context, reqCtx *handlers.RequestCo
304304
endpointCandidates := d.endpointCandidates.Locate(ctx, reqCtx.Request.Metadata)
305305
if len(endpointCandidates) == 0 {
306306
return reqCtx, errcommon.Error{
307-
Code: errcommon.ServiceUnavailable,
308-
Msg: "failed to find endpoint candidates for serving the request",
307+
Code: errcommon.ServiceUnavailable,
308+
Msg: "failed to find endpoint candidates for serving the request",
309+
Headers: map[string]string{errcommon.RequestDroppedReasonHeaderKey: string(errcommon.RequestDroppedReasonNoEndpoints)},
309310
}
310311
}
311312

312313
snapshotOfCandidatePods := d.toSchedulerEndpoints(endpointCandidates)
313314
snapshotOfCandidatePods = d.runScreeners(ctx, reqCtx.SchedulingRequest, snapshotOfCandidatePods)
314315
if len(snapshotOfCandidatePods) == 0 {
315316
return reqCtx, errcommon.Error{
316-
Code: errcommon.ServiceUnavailable,
317-
Msg: "screeners eliminated all endpoint candidates",
317+
Code: errcommon.ServiceUnavailable,
318+
Msg: "screeners eliminated all endpoint candidates",
319+
Headers: map[string]string{errcommon.RequestDroppedReasonHeaderKey: string(errcommon.RequestDroppedReasonNoEndpoints)},
318320
}
319321
}
320322
// Prepare per request data by running DataProducer plugins.

pkg/epp/requestcontrol/director_test.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,11 +414,13 @@ func TestDirector_HandleRequest(t *testing.T) {
414414
initialTargetModelName string // Initial target model in the reqCtx.
415415
parser fwkrh.Parser
416416
wantErrCode string // Expected errcommon code string
417+
wantDroppedReason string // If non-empty, expected x-llm-d-request-dropped-reason header on the error
417418
wantReqCtx *handlers.RequestContext // Fields to check in the returned RequestContext
418419
targetModelName string // Expected model name after target model resolution
419420
admitRequestDenialError error // Expected denial error from admission plugin
420421
dataProducerPlugin *mockDataProducerPlugin
421422
screener *mockScreener
423+
emptyEndpoints bool // If true, the director locates no endpoint candidates.
422424
preRequestPlugins []*mockPreRequestPlugin
423425
requestHeaderPlugin *mockRequestHeaderPlugin
424426
wantMutatedBody map[string]any
@@ -995,7 +997,21 @@ func TestDirector_HandleRequest(t *testing.T) {
995997
screener: &mockScreener{name: "eliminate-all", screen: func([]fwksched.Endpoint) []fwksched.Endpoint {
996998
return nil
997999
}},
998-
wantErrCode: errcommon.ServiceUnavailable,
1000+
wantErrCode: errcommon.ServiceUnavailable,
1001+
wantDroppedReason: string(errcommon.RequestDroppedReasonNoEndpoints),
1002+
},
1003+
{
1004+
name: "no endpoint candidates located",
1005+
reqBodyMap: map[string]any{
1006+
"model": model,
1007+
"prompt": "critical prompt",
1008+
},
1009+
mockAdmissionController: &mockAdmissionController{admitErr: nil},
1010+
initialTargetModelName: model,
1011+
inferenceObjectiveName: objectiveName,
1012+
emptyEndpoints: true,
1013+
wantErrCode: errcommon.ServiceUnavailable,
1014+
wantDroppedReason: string(errcommon.RequestDroppedReasonNoEndpoints),
9991015
},
10001016
{
10011017
name: "scheduler returns error",
@@ -1010,6 +1026,30 @@ func TestDirector_HandleRequest(t *testing.T) {
10101026
wantErrCode: errcommon.ResourceExhausted,
10111027
inferenceObjectiveName: objectiveName,
10121028
},
1029+
{
1030+
// The typed error inside a joined scheduler error, including its
1031+
// drop-reason header, must reach the caller instead of the
1032+
// untyped-error fallback.
1033+
name: "scheduler returns joined error with typed capacity rejection",
1034+
reqBodyMap: map[string]any{
1035+
"model": model,
1036+
"prompt": "prompt that causes scheduling drain",
1037+
},
1038+
mockAdmissionController: &mockAdmissionController{admitErr: nil},
1039+
schedulerMockSetup: func(m *mockScheduler) {
1040+
m.scheduleErr = errors.Join(
1041+
errors.New("failed to run scheduler profile 'default'"),
1042+
fmt.Errorf("profile %q: %w", "default", errcommon.Error{
1043+
Code: errcommon.ResourceExhausted,
1044+
Msg: "no endpoints available for the given request",
1045+
Headers: map[string]string{errcommon.RequestDroppedReasonHeaderKey: string(errcommon.RequestDroppedReasonSaturated)},
1046+
}),
1047+
)
1048+
},
1049+
wantErrCode: errcommon.ResourceExhausted,
1050+
wantDroppedReason: string(errcommon.RequestDroppedReasonSaturated),
1051+
inferenceObjectiveName: objectiveName,
1052+
},
10131053
{
10141054
name: "scheduler returns nil result and nil error",
10151055
reqBodyMap: map[string]any{
@@ -1090,6 +1130,9 @@ func TestDirector_HandleRequest(t *testing.T) {
10901130

10911131
endpointCandidates := NewCachedEndpointCandidates(context.Background(), NewDatastoreEndpointCandidates(ds), time.Minute)
10921132
director := NewDirectorWithConfig(ds, mockSched, test.mockAdmissionController, endpointCandidates, config)
1133+
if test.emptyEndpoints {
1134+
director.endpointCandidates = &mockEndpointCandidates{}
1135+
}
10931136
if len(test.rewrites) > 0 {
10941137
mockDs := &mockDatastore{
10951138
pods: ds.PodList(datastore.AllPodsPredicate),
@@ -1140,6 +1183,9 @@ func TestDirector_HandleRequest(t *testing.T) {
11401183
var e errcommon.Error
11411184
if assert.ErrorAs(t, err, &e, "Error should be of type errcommon.Error") {
11421185
assert.Equal(t, test.wantErrCode, e.Code, "Error code mismatch")
1186+
if test.wantDroppedReason != "" {
1187+
assert.Equal(t, test.wantDroppedReason, e.Headers[errcommon.RequestDroppedReasonHeaderKey], "drop-reason header mismatch")
1188+
}
11431189
}
11441190
return
11451191
}

pkg/epp/scheduling/scheduler.go

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ package scheduling
1919

2020
import (
2121
"context"
22+
"errors"
2223
"fmt"
24+
"maps"
25+
"slices"
2326
"time"
2427

2528
"go.opentelemetry.io/otel/attribute"
@@ -66,6 +69,11 @@ func (s *Scheduler) Schedule(ctx context.Context, request *fwksched.InferenceReq
6669
}()
6770

6871
profileRunResults := map[string]*fwksched.ProfileRunResult{}
72+
// Keyed like profileRunResults so a profile that fails in one iteration and
73+
// succeeds in a later one leaves no stale error behind. Nil until a profile
74+
// fails: delete and len are no-ops on a nil map, and the happy path skips
75+
// the allocation.
76+
var profileRunErrors map[string]error
6977

7078
for { // get the next set of profiles to run iteratively based on the request and the previous execution results
7179
if verboseEnabled {
@@ -87,12 +95,19 @@ func (s *Scheduler) Schedule(ctx context.Context, request *fwksched.InferenceReq
8795
}
8896
// run the selected profiles and collect results (current code runs all profiles)
8997
profileRunResult, err := runSchedulerProfile(ctx, name, profile, request, candidateEndpoints)
90-
if verboseEnabled {
91-
if err != nil {
98+
if err != nil {
99+
if verboseEnabled {
92100
loggerVerbose.Info("failed to run scheduler profile", "profile", name, "error", err.Error())
93-
} else {
101+
}
102+
if profileRunErrors == nil {
103+
profileRunErrors = map[string]error{}
104+
}
105+
profileRunErrors[name] = fmt.Errorf("profile %q: %w", name, err)
106+
} else {
107+
if verboseEnabled {
94108
loggerVerbose.Info("Completed running scheduler profile successfully", "profile", name)
95109
}
110+
delete(profileRunErrors, name)
96111
}
97112

98113
profileRunResults[name] = profileRunResult // if profile failed to run, the run result is nil
@@ -114,6 +129,21 @@ func (s *Scheduler) Schedule(ctx context.Context, request *fwksched.InferenceReq
114129
loggerVerbose.Info("Completed running profile handler ProcessResults successfully", "plugin", handlerName)
115130
}
116131

132+
// Profile handlers see failed profiles only as nil results and report them
133+
// with fresh untyped errors. Join the retained profile errors so a typed
134+
// errcommon.Error raised inside a profile run (e.g. filters draining the
135+
// candidate set) stays reachable via errors.As in the caller.
136+
if err != nil && len(profileRunErrors) > 0 {
137+
errs := make([]error, 0, len(profileRunErrors)+1)
138+
errs = append(errs, err)
139+
// Sorted so error composition, and therefore errors.As selection when
140+
// profiles fail with different typed codes, is deterministic.
141+
for _, name := range slices.Sorted(maps.Keys(profileRunErrors)) {
142+
errs = append(errs, profileRunErrors[name])
143+
}
144+
err = errors.Join(errs...)
145+
}
146+
117147
return result, err
118148
}
119149

0 commit comments

Comments
 (0)