Skip to content

Commit 34a239d

Browse files
committed
fix review comments
Signed-off-by: roytman <roytman@il.ibm.com>
1 parent a0c4afc commit 34a239d

4 files changed

Lines changed: 70 additions & 24 deletions

File tree

docs/coordinator_architecture.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@ multimodal item in parallel. The number and shape of these requests depend on th
131131
steps are enabled and how they are parameterized); text-only requests need no media
132132
download or encode at all. Across phases the coordinator sequences the round-trips
133133
(prefill and decode are one each), threading state from each response into the next
134-
request. Each Gateway call routes it by the `EPP-Phase` header to
135-
the matching per-phase EPP and then to a pod in that phase's pool.
134+
request. Each Gateway call routes it by the `EPP-Phase` header to the EPP, which runs
135+
the matching scheduling profile and picks a pod from that phase's pool.
136136

137137
Steps are skipped at runtime when they do not apply (for example, `encode` is a no-op
138138
when the request has no multimodal entries, and `render` short-circuits when the
@@ -362,7 +362,7 @@ service in front of the Inference Gateway:
362362
| Pipeline versatility | Fixed E/P/D orchestration baked into the sidecar | Configurable pipeline of independent, reorderable plugin steps; new stages added without touching existing ones |
363363
| EPP scheduling | One cycle selects all phases (`disagg-profile-handler`) | One EPP call per phase, coordinator drives the cascade |
364364
| vLLM pod selection | All phase pods chosen up front in one scheduling cycle | Deferred per phase: each pod is selected only when that phase's call is made, at the point its destination becomes relevant |
365-
| Phase selection signal | EPP request headers `x-prefiller-host-port`, `x-encoder-hosts-ports` read by the sidecar | `EPP-Phase` header per call; per-phase EPP picks the pod |
365+
| Phase selection signal | EPP request headers `x-prefiller-host-port`, `x-encoder-hosts-ports` read by the sidecar | `EPP-Phase` header per call; the EPP runs the matching profile and picks the pod |
366366
| Tokenization | On the workers | Once, in the coordinator's render step; token IDs reused downstream (experimental path) |
367367
| Cross-phase state | Held by the sidecar | Held on the coordinator `RequestContext` |
368368

pkg/epp/framework/plugins/scheduling/profilehandler/headerphase/README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ Reads the configured header from the incoming request and looks up the
1313
`schedulingProfiles` entry with that exact name:
1414

1515
- If a matching profile hasn't run yet, it runs that profile alone.
16-
- If the header is missing or names a profile that isn't configured, no profile runs; the
17-
request fails with the scheduler's generic "failed to run any scheduler profile" error,
18-
and the EPP logs the specific reason (missing header vs. unconfigured value).
16+
- If the header is missing or names a profile that isn't configured, no profile runs. The
17+
request fails with the scheduler's own generic "no profile ran" error (a 429 to the
18+
client, since the scheduler doesn't distinguish a malformed request from exhausted
19+
capacity); the EPP logs the specific reason (missing header vs. unconfigured value) for
20+
operators.
1921

2022
This differs from the [disagg profile handler](../disagg/README.md), which decides which
2123
profiles to run via decider plugins rather than a header.

pkg/epp/framework/plugins/scheduling/profilehandler/headerphase/header_phase_profile_handler.go

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2026 The Kubernetes Authors.
2+
Copyright 2026 The llm-d Authors.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -42,7 +42,7 @@ var _ fwksched.ProfileHandler = &HeaderPhaseProfileHandler{}
4242
// parameters configures the HeaderPhaseProfileHandler.
4343
type parameters struct {
4444
// HeaderName is the request header whose value names the scheduling profile to run.
45-
// Defaults to "EPP-Phase" when empty.
45+
// Defaults to defaultHeaderName when empty.
4646
HeaderName string `json:"headerName"`
4747
}
4848

@@ -55,22 +55,24 @@ func Factory(name string, rawParameters *json.Decoder, _ fwkplugin.Handle) (fwkp
5555
}
5656
}
5757

58-
headerName := strings.TrimSpace(params.HeaderName)
59-
if headerName == "" {
60-
headerName = defaultHeaderName
61-
}
62-
63-
return NewHeaderPhaseProfileHandler(headerName).WithName(name), nil
58+
return NewHeaderPhaseProfileHandler(params.HeaderName).WithName(name), nil
6459
}
6560

6661
// NewHeaderPhaseProfileHandler initializes a new HeaderPhaseProfileHandler and returns
67-
// its pointer. headerName is lowercased and trimmed: the EPP's request handler lowercases
68-
// every incoming header name at ingestion (pkg/epp/handlers/request.go), so the configured
69-
// name must be normalized the same way to match.
62+
// its pointer. headerName is lowercased and trimmed, falling back to defaultHeaderName
63+
// when that leaves it empty: the EPP's request handler lowercases every incoming header
64+
// name at ingestion (pkg/epp/handlers/request.go), so the configured name must be
65+
// normalized the same way to match, and an empty header key would never match any
66+
// request.
7067
func NewHeaderPhaseProfileHandler(headerName string) *HeaderPhaseProfileHandler {
68+
headerName = strings.ToLower(strings.TrimSpace(headerName))
69+
if headerName == "" {
70+
headerName = strings.ToLower(defaultHeaderName)
71+
}
72+
7173
return &HeaderPhaseProfileHandler{
7274
typedName: fwkplugin.TypedName{Type: HeaderPhaseProfileHandlerType, Name: HeaderPhaseProfileHandlerType},
73-
headerName: strings.ToLower(strings.TrimSpace(headerName)),
75+
headerName: headerName,
7476
}
7577
}
7678

@@ -117,9 +119,15 @@ func (h *HeaderPhaseProfileHandler) noMatchError(phase string) error {
117119
// Pick selects the single SchedulingProfile named by the request's phase header. It
118120
// returns an empty map once that profile has run, or when the header is missing or
119121
// names a profile that isn't configured. In the latter case the scheduler's run loop
120-
// (pkg/epp/scheduling.Scheduler.Schedule) stops without ever calling ProcessResults,
121-
// since no profile ever ran, so the specific reason is logged here rather than
122-
// returned from ProcessResults, where it would be unreachable in practice.
122+
// (pkg/epp/scheduling.Scheduler.Schedule) stops without ever calling ProcessResults, so
123+
// the specific reason is logged here rather than returned from ProcessResults, where it
124+
// would be unreachable. The client never sees that reason: it only gets the scheduler's
125+
// generic "failed to run any scheduler profile" error, which
126+
// pkg/epp/requestcontrol/director.go maps to a 429 ResourceExhausted response -
127+
// misleading, since a malformed or missing header is a client error, not a capacity
128+
// problem. Surfacing the real reason to the client needs a scheduler/ProfileHandler
129+
// contract change and is out of scope here; the log is a diagnostic aid for operators,
130+
// not an equivalent substitute for what the caller receives.
123131
func (h *HeaderPhaseProfileHandler) Pick(ctx context.Context, request *fwksched.InferenceRequest, profiles map[string]fwksched.SchedulerProfile,
124132
profileResults map[string]*fwksched.ProfileRunResult) map[string]fwksched.SchedulerProfile {
125133
// TODO(#2135): single profile per request; extend to loop over an ordered phase

pkg/epp/framework/plugins/scheduling/profilehandler/headerphase/header_phase_profile_handler_test.go

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2026 The Kubernetes Authors.
2+
Copyright 2026 The llm-d Authors.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -52,11 +52,22 @@ func TestNewHeaderPhaseProfileHandler(t *testing.T) {
5252
}
5353
}
5454

55+
func TestNewHeaderPhaseProfileHandlerEmptyFallsBackToDefault(t *testing.T) {
56+
// The constructor itself must uphold the "never empty" invariant, independent of
57+
// Factory: an empty headerName must not produce a handler that can never match any
58+
// request (request.Headers[""] is always empty).
59+
handler := NewHeaderPhaseProfileHandler("")
60+
if handler.headerName != ingestedHeaderKey {
61+
t.Errorf("Expected headerName %q, got %q", ingestedHeaderKey, handler.headerName)
62+
}
63+
}
64+
5565
func TestHeaderPhaseProfileHandlerFactory(t *testing.T) {
5666
tests := []struct {
5767
name string
5868
rawParameters string
5969
wantHeaderName string
70+
wantErr bool
6071
}{
6172
{
6273
name: "no parameters, uses default header, normalized to lowercase",
@@ -78,6 +89,11 @@ func TestHeaderPhaseProfileHandlerFactory(t *testing.T) {
7889
rawParameters: `{"headerName": " "}`,
7990
wantHeaderName: ingestedHeaderKey,
8091
},
92+
{
93+
name: "malformed json returns an error",
94+
rawParameters: `{invalid}`,
95+
wantErr: true,
96+
},
8197
}
8298

8399
for _, tt := range tests {
@@ -87,6 +103,12 @@ func TestHeaderPhaseProfileHandlerFactory(t *testing.T) {
87103
decoder := fwkplugin.StrictDecoder(json.RawMessage(tt.rawParameters))
88104

89105
plugin, err := Factory("custom-name", decoder, nil)
106+
if tt.wantErr {
107+
if err == nil {
108+
t.Fatalf("Factory() expected error, got nil")
109+
}
110+
return
111+
}
90112
if err != nil {
91113
t.Fatalf("Factory() returned unexpected error: %v", err)
92114
}
@@ -213,6 +235,18 @@ func TestHeaderPhasePick(t *testing.T) {
213235
profileResults: map[string]*fwksched.ProfileRunResult{},
214236
wantProfiles: map[string]fwksched.SchedulerProfile{},
215237
},
238+
{
239+
// Unlike the header name, the header value is matched case-sensitively
240+
// against the configured profile names: only the name gets normalized to
241+
// match how the EPP's request handler stores it, since that's a fixed key
242+
// this plugin controls, but the value is caller-supplied and compared
243+
// verbatim against schedulingProfiles names.
244+
name: "header value case does not match the configured profile name",
245+
request: &fwksched.InferenceRequest{Headers: map[string]string{ingestedHeaderKey: "Encode"}},
246+
profiles: profiles,
247+
profileResults: map[string]*fwksched.ProfileRunResult{},
248+
wantProfiles: map[string]fwksched.SchedulerProfile{},
249+
},
216250
}
217251

218252
handler := NewHeaderPhaseProfileHandler(defaultHeaderName)
@@ -283,14 +317,16 @@ func TestHeaderPhaseProcessResults(t *testing.T) {
283317
"encode": successResult,
284318
"decode": successResult,
285319
},
286-
wantErr: true,
320+
wantErr: true,
321+
wantErrContains: "is intended to run a single profile per request, got 2",
287322
},
288323
{
289324
name: "nil result (profile execution failure) returns error",
290325
profileResults: map[string]*fwksched.ProfileRunResult{
291326
"encode": nil,
292327
},
293-
wantErr: true,
328+
wantErr: true,
329+
wantErrContains: "failed to run scheduler profile 'encode'",
294330
},
295331
}
296332

0 commit comments

Comments
 (0)