Skip to content

Commit 0b9aad9

Browse files
authored
Trace scheduler profile execution by name (#1983)
Create a profile-level span so filter-stage spans can be attributed during multi-profile routing. Refs #1982 Signed-off-by: liuyt <144877719+AutuSnow@users.noreply.github.com>
1 parent de4f661 commit 0b9aad9

2 files changed

Lines changed: 152 additions & 1 deletion

File tree

pkg/epp/scheduling/scheduler.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@ import (
2222
"fmt"
2323
"time"
2424

25+
"go.opentelemetry.io/otel/attribute"
26+
"go.opentelemetry.io/otel/trace"
2527
"sigs.k8s.io/controller-runtime/pkg/log"
2628

2729
logutil "github.com/llm-d/llm-d-router/pkg/common/observability/logging"
30+
"github.com/llm-d/llm-d-router/pkg/common/observability/tracing"
2831
fwksched "github.com/llm-d/llm-d-router/pkg/epp/framework/interface/scheduling"
2932
"github.com/llm-d/llm-d-router/pkg/epp/metrics"
3033
)
@@ -75,7 +78,7 @@ func (s *Scheduler) Schedule(ctx context.Context, request *fwksched.InferenceReq
7578
for name, profile := range profiles {
7679
loggerVerbose.Info("Running scheduler profile", "profile", name)
7780
// run the selected profiles and collect results (current code runs all profiles)
78-
profileRunResult, err := profile.Run(ctx, request, candidateEndpoints)
81+
profileRunResult, err := runSchedulerProfile(ctx, name, profile, request, candidateEndpoints)
7982
if err != nil {
8083
loggerVerbose.Info("failed to run scheduler profile", "profile", name, "error", err.Error())
8184
} else {
@@ -99,3 +102,15 @@ func (s *Scheduler) Schedule(ctx context.Context, request *fwksched.InferenceReq
99102

100103
return result, err
101104
}
105+
106+
func runSchedulerProfile(ctx context.Context, name string, profile fwksched.SchedulerProfile,
107+
request *fwksched.InferenceRequest, candidateEndpoints []fwksched.Endpoint,
108+
) (*fwksched.ProfileRunResult, error) {
109+
profileCtx, span := tracing.Tracer(TracerScope).Start(ctx, "run_scheduler_profile",
110+
trace.WithSpanKind(trace.SpanKindInternal),
111+
trace.WithAttributes(attribute.String("llm_d.epp.scheduling.profile.name", name)),
112+
)
113+
defer span.End()
114+
115+
return profile.Run(profileCtx, request, candidateEndpoints)
116+
}

pkg/epp/scheduling/scheduler_profile_tracing_test.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,142 @@ func newTestEndpoints(names ...string) []fwksched.Endpoint {
7171
return endpoints
7272
}
7373

74+
type allProfilesTraceHandler struct{}
75+
76+
func (*allProfilesTraceHandler) TypedName() fwkplugin.TypedName {
77+
return fwkplugin.TypedName{Type: "all-profiles-trace-handler"}
78+
}
79+
80+
func (*allProfilesTraceHandler) Pick(_ context.Context, _ *fwksched.InferenceRequest,
81+
profiles map[string]fwksched.SchedulerProfile,
82+
profileResults map[string]*fwksched.ProfileRunResult,
83+
) map[string]fwksched.SchedulerProfile {
84+
if len(profileResults) == 0 {
85+
return profiles
86+
}
87+
return nil
88+
}
89+
90+
func (*allProfilesTraceHandler) ProcessResults(_ context.Context, _ *fwksched.InferenceRequest,
91+
profileResults map[string]*fwksched.ProfileRunResult,
92+
) (*fwksched.SchedulingResult, error) {
93+
return &fwksched.SchedulingResult{
94+
ProfileResults: profileResults,
95+
PrimaryProfileName: "decode",
96+
}, nil
97+
}
98+
99+
type panicTraceFilter struct{}
100+
101+
func (*panicTraceFilter) TypedName() fwkplugin.TypedName {
102+
return fwkplugin.TypedName{Type: "panic-trace-filter"}
103+
}
104+
105+
func (*panicTraceFilter) Filter(_ context.Context, _ *fwksched.InferenceRequest,
106+
_ []fwksched.Endpoint,
107+
) []fwksched.Endpoint {
108+
panic("filter panic")
109+
}
110+
111+
func TestScheduleNestsFilterSpansUnderNamedProfiles(t *testing.T) {
112+
recorder := setupSpanRecorder(t)
113+
endpoints := newTestEndpoints("pod1")
114+
115+
newProfile := func(name string) *SchedulerProfile {
116+
plugin := &testPlugin{
117+
typedName: fwkplugin.TypedName{Type: "test-plugin", Name: name},
118+
FilterRes: []k8stypes.NamespacedName{{Name: "pod1"}},
119+
PickRes: k8stypes.NamespacedName{Name: "pod1"},
120+
}
121+
return NewSchedulerProfile().WithFilters(plugin).WithPicker(plugin)
122+
}
123+
124+
scheduler := NewSchedulerWithConfig(NewSchedulerConfig(
125+
&allProfilesTraceHandler{},
126+
map[string]fwksched.SchedulerProfile{
127+
"decode": newProfile("decode"),
128+
"prefill": newProfile("prefill"),
129+
},
130+
))
131+
132+
ctx, root := otel.Tracer("test").Start(context.Background(), "root")
133+
_, err := scheduler.Schedule(ctx, &fwksched.InferenceRequest{TargetModel: "m1", RequestID: "r1"}, endpoints)
134+
root.End()
135+
if err != nil {
136+
t.Fatalf("Schedule returned error: %v", err)
137+
}
138+
139+
profileSpans := findSpans(recorder.Ended(), "run_scheduler_profile")
140+
if len(profileSpans) != 2 {
141+
t.Fatalf("got %d run_scheduler_profile spans, want 2", len(profileSpans))
142+
}
143+
profileNameBySpanID := make(map[trace.SpanID]string, len(profileSpans))
144+
for _, profileSpan := range profileSpans {
145+
profileName := spanAttributes(profileSpan)["llm_d.epp.scheduling.profile.name"].AsString()
146+
profileNameBySpanID[profileSpan.SpanContext().SpanID()] = profileName
147+
}
148+
149+
filterSpans := findSpans(recorder.Ended(), "filter_endpoints")
150+
if len(filterSpans) != 2 {
151+
t.Fatalf("got %d filter_endpoints spans, want 2", len(filterSpans))
152+
}
153+
seenProfiles := make(map[string]bool, len(filterSpans))
154+
for _, filterSpan := range filterSpans {
155+
profileName, ok := profileNameBySpanID[filterSpan.Parent().SpanID()]
156+
if !ok {
157+
t.Errorf("filter_endpoints parent %v is not a run_scheduler_profile span", filterSpan.Parent().SpanID())
158+
continue
159+
}
160+
seenProfiles[profileName] = true
161+
}
162+
for _, profileName := range []string{"decode", "prefill"} {
163+
if !seenProfiles[profileName] {
164+
t.Errorf("no filter_endpoints span attributed to profile %q", profileName)
165+
}
166+
}
167+
}
168+
169+
func TestScheduleEndsProfileSpanWhenFilterPanics(t *testing.T) {
170+
recorder := setupSpanRecorder(t)
171+
endpoints := newTestEndpoints("pod1")
172+
picker := &testPlugin{
173+
typedName: fwkplugin.TypedName{Type: "test-picker"},
174+
PickRes: k8stypes.NamespacedName{Name: "pod1"},
175+
}
176+
profile := NewSchedulerProfile().WithFilters(&panicTraceFilter{}).WithPicker(picker)
177+
scheduler := NewSchedulerWithConfig(NewSchedulerConfig(
178+
&allProfilesTraceHandler{},
179+
map[string]fwksched.SchedulerProfile{"decode": profile},
180+
))
181+
182+
ctx, root := otel.Tracer("test").Start(context.Background(), "root")
183+
recovered := func() (value any) {
184+
defer func() { value = recover() }()
185+
_, _ = scheduler.Schedule(ctx, &fwksched.InferenceRequest{TargetModel: "m1", RequestID: "r1"}, endpoints)
186+
return nil
187+
}()
188+
root.End()
189+
if recovered != "filter panic" {
190+
t.Fatalf("recovered panic = %v, want filter panic", recovered)
191+
}
192+
193+
profileSpans := findSpans(recorder.Ended(), "run_scheduler_profile")
194+
if len(profileSpans) != 1 {
195+
t.Fatalf("got %d run_scheduler_profile spans after panic, want 1", len(profileSpans))
196+
}
197+
filterSpans := findSpans(recorder.Ended(), "filter_endpoints")
198+
if len(filterSpans) != 1 {
199+
t.Fatalf("got %d filter_endpoints spans after panic, want 1", len(filterSpans))
200+
}
201+
if filterSpans[0].Parent().SpanID() != profileSpans[0].SpanContext().SpanID() {
202+
t.Errorf("filter_endpoints parent = %v, want profile span %v",
203+
filterSpans[0].Parent().SpanID(), profileSpans[0].SpanContext().SpanID())
204+
}
205+
if got := spanAttributes(profileSpans[0])["llm_d.epp.scheduling.profile.name"].AsString(); got != "decode" {
206+
t.Errorf("profile name = %q, want decode", got)
207+
}
208+
}
209+
74210
func TestRunFilterPluginsSingleSpan(t *testing.T) {
75211
recorder := setupSpanRecorder(t)
76212

0 commit comments

Comments
 (0)