Skip to content

Commit 10e56bd

Browse files
committed
feat(scheduling): add endpoint attribute weight scorer
Refs #2201 Signed-off-by: nicole-lihui <nicole.li@daocloud.io>
1 parent 26df3f1 commit 10e56bd

9 files changed

Lines changed: 878 additions & 0 deletions

File tree

cmd/epp/runner/runner.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import (
6868
attrtopology "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/datalayer/attribute/topology"
6969
discoveryfile "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/datalayer/discovery/file"
7070
extdcgm "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/datalayer/extractor/dcgm"
71+
labelproducer "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/datalayer/extractor/label"
7172
extractormetrics "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/datalayer/extractor/metrics"
7273
extmodels "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/datalayer/extractor/models"
7374
exttopology "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/datalayer/extractor/topology"
@@ -118,6 +119,7 @@ import (
118119
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/scheduling/profilehandler/disagg"
119120
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/scheduling/profilehandler/single"
120121
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/scheduling/scorer/activerequest"
122+
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/scheduling/scorer/attributeweight"
121123
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/scheduling/scorer/contextlengthaware"
122124
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/scheduling/scorer/endpointattribute"
123125
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/scheduling/scorer/kvcacheutilization"
@@ -565,8 +567,10 @@ func (r *Runner) registerInTreePlugins() {
565567
fwkplugin.Register(loadaware.LoadAwareType, loadaware.Factory)
566568
fwkplugin.Register(sessionaffinity.SessionAffinityType, sessionaffinity.Factory)
567569
fwkplugin.Register(contextlengthaware.ContextLengthAwareType, contextlengthaware.Factory)
570+
fwkplugin.Register(attributeweight.EndpointAttributeWeightScorerType, attributeweight.Factory)
568571

569572
// data layer models source/extractor
573+
fwkplugin.Register(labelproducer.LabelProducerType, labelproducer.Factory)
570574
fwkplugin.Register(srcmodels.ModelsDataSourceType, srcmodels.ModelDataSourceFactory)
571575
fwkplugin.Register(attrmodels.ModelsExtractorType, extmodels.ModelServerExtractorFactory)
572576
fwkplugin.Register(attrtopology.TopologyExtractorType, exttopology.Factory)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
Copyright 2026 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package stringattribute declares string-valued endpoint attributes.
18+
package stringattribute
19+
20+
import fwkdl "github.com/llm-d/llm-d-router/pkg/epp/framework/interface/datalayer"
21+
22+
// Value is a string-valued endpoint attribute.
23+
type Value string
24+
25+
// Clone returns the value unchanged.
26+
func (v Value) Clone() fwkdl.Cloneable {
27+
return v
28+
}
29+
30+
// ReadValue retrieves a string value from an endpoint attribute map.
31+
func ReadValue(attrs fwkdl.AttributeMap, key string) (Value, bool) {
32+
return fwkdl.ReadAttribute[Value](attrs, key)
33+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
Copyright 2026 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package label provides an endpoint extractor that copies a Pod label into
18+
// an endpoint attribute.
19+
package label
20+
21+
import (
22+
"context"
23+
"encoding/json"
24+
"fmt"
25+
"strconv"
26+
27+
fwkdl "github.com/llm-d/llm-d-router/pkg/epp/framework/interface/datalayer"
28+
fwkplugin "github.com/llm-d/llm-d-router/pkg/epp/framework/interface/plugin"
29+
attrmetrics "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/datalayer/attribute/metrics"
30+
attrstring "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/datalayer/attribute/string"
31+
sourcenotifications "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/datalayer/source/notifications"
32+
)
33+
34+
const (
35+
// LabelProducerType identifies the label producer plugin.
36+
LabelProducerType = "label-producer"
37+
38+
valueTypeString = "string"
39+
valueTypeNumber = "number"
40+
)
41+
42+
var (
43+
_ fwkplugin.Plugin = (*Producer)(nil)
44+
_ fwkdl.Registrant = (*Producer)(nil)
45+
_ fwkdl.EndpointExtractor = (*Producer)(nil)
46+
)
47+
48+
type parameters struct {
49+
// Label is the Pod label to copy.
50+
Label string `json:"label"`
51+
// AttributeKey is the endpoint attribute key to populate.
52+
AttributeKey string `json:"attributeKey"`
53+
// ValueType controls whether the value is stored as a string or number.
54+
ValueType string `json:"valueType"`
55+
}
56+
57+
// Factory creates a label producer.
58+
func Factory(name string, decoder *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
59+
var params parameters
60+
if decoder != nil {
61+
if err := decoder.Decode(&params); err != nil {
62+
return nil, fmt.Errorf("failed to parse the parameters of the '%s' plugin: %w", LabelProducerType, err)
63+
}
64+
}
65+
return NewProducer(name, params)
66+
}
67+
68+
// NewProducer validates parameters and creates a label producer.
69+
func NewProducer(name string, params parameters) (*Producer, error) {
70+
if name == "" {
71+
name = LabelProducerType
72+
}
73+
if params.Label == "" {
74+
return nil, fmt.Errorf("'%s' requires a non-empty 'label'", LabelProducerType)
75+
}
76+
if params.AttributeKey == "" {
77+
return nil, fmt.Errorf("'%s' requires a non-empty 'attributeKey'", LabelProducerType)
78+
}
79+
if params.ValueType == "" {
80+
params.ValueType = valueTypeString
81+
}
82+
if params.ValueType != valueTypeString && params.ValueType != valueTypeNumber {
83+
return nil, fmt.Errorf("'%s' valueType must be %q or %q, got %q", LabelProducerType, valueTypeString, valueTypeNumber, params.ValueType)
84+
}
85+
86+
return &Producer{
87+
typedName: fwkplugin.TypedName{Type: LabelProducerType, Name: name},
88+
label: params.Label,
89+
attributeKey: params.AttributeKey,
90+
valueType: params.ValueType,
91+
}, nil
92+
}
93+
94+
// Producer copies a Pod label into an endpoint attribute.
95+
type Producer struct {
96+
typedName fwkplugin.TypedName
97+
label string
98+
attributeKey string
99+
valueType string
100+
}
101+
102+
// TypedName returns the type and name of the producer.
103+
func (p *Producer) TypedName() fwkplugin.TypedName {
104+
return p.typedName
105+
}
106+
107+
// RegisterDependencies wires the producer to endpoint lifecycle events.
108+
func (p *Producer) RegisterDependencies(r fwkdl.Registrar) error {
109+
return r.Register(fwkdl.PendingRegistration{
110+
Owner: p.typedName,
111+
SourceType: sourcenotifications.EndpointNotificationSourceType,
112+
Extractor: p,
113+
DefaultSource: sourcenotifications.NewEndpointDataSource(
114+
sourcenotifications.EndpointNotificationSourceType,
115+
sourcenotifications.EndpointNotificationSourceType,
116+
),
117+
})
118+
}
119+
120+
// Extract copies the configured label into the endpoint attribute.
121+
func (p *Producer) Extract(_ context.Context, event fwkdl.EndpointEvent) error {
122+
if event.Type == fwkdl.EventDelete || event.Endpoint == nil {
123+
return nil
124+
}
125+
126+
meta := event.Endpoint.GetMetadata()
127+
if meta == nil {
128+
return nil
129+
}
130+
value := meta.Labels[p.label]
131+
132+
if p.valueType == valueTypeString {
133+
event.Endpoint.GetAttributes().Put(p.attributeKey, attrstring.Value(value))
134+
return nil
135+
}
136+
137+
if value == "" {
138+
event.Endpoint.GetAttributes().Put(p.attributeKey, attrmetrics.ScalarMetricValue(0))
139+
return nil
140+
}
141+
number, err := strconv.ParseFloat(value, 64)
142+
if err != nil {
143+
event.Endpoint.GetAttributes().Put(p.attributeKey, attrmetrics.ScalarMetricValue(0))
144+
return fmt.Errorf("label %q on endpoint %s has non-numeric value %q: %w", p.label, meta.NamespacedName, value, err)
145+
}
146+
event.Endpoint.GetAttributes().Put(p.attributeKey, attrmetrics.ScalarMetricValue(number))
147+
return nil
148+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
Copyright 2026 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package label
18+
19+
import (
20+
"context"
21+
"encoding/json"
22+
"testing"
23+
24+
"github.com/stretchr/testify/assert"
25+
"github.com/stretchr/testify/require"
26+
27+
fwkdl "github.com/llm-d/llm-d-router/pkg/epp/framework/interface/datalayer"
28+
fwkplugin "github.com/llm-d/llm-d-router/pkg/epp/framework/interface/plugin"
29+
attrmetrics "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/datalayer/attribute/metrics"
30+
attrstring "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/datalayer/attribute/string"
31+
)
32+
33+
const testLabel = "nvidia.com/gpu.product"
34+
35+
type captureRegistrar struct {
36+
registration fwkdl.PendingRegistration
37+
}
38+
39+
func (r *captureRegistrar) Register(reg fwkdl.PendingRegistration) error {
40+
r.registration = reg
41+
return nil
42+
}
43+
44+
func newEndpoint(labels map[string]string) fwkdl.Endpoint {
45+
return fwkdl.NewEndpoint(&fwkdl.EndpointMetadata{Labels: labels}, nil)
46+
}
47+
48+
func TestFactory(t *testing.T) {
49+
producer, err := Factory("gpu-product", fwkplugin.StrictDecoder(json.RawMessage(`{
50+
"label": "nvidia.com/gpu.product",
51+
"attributeKey": "gpu.product",
52+
"valueType": "string"
53+
}`)), nil)
54+
55+
require.NoError(t, err)
56+
assert.Equal(t, LabelProducerType, producer.TypedName().Type)
57+
assert.Equal(t, "gpu-product", producer.TypedName().Name)
58+
}
59+
60+
func TestFactory_Validation(t *testing.T) {
61+
tests := []struct {
62+
name string
63+
parameters string
64+
wantErr string
65+
}{
66+
{name: "missing label", parameters: `{"attributeKey":"gpu.product"}`, wantErr: "label"},
67+
{name: "missing attribute key", parameters: `{"label":"gpu"}`, wantErr: "attributeKey"},
68+
{name: "invalid value type", parameters: `{"label":"gpu","attributeKey":"gpu.product","valueType":"bool"}`, wantErr: "valueType"},
69+
}
70+
71+
for _, test := range tests {
72+
t.Run(test.name, func(t *testing.T) {
73+
producer, err := Factory("test", fwkplugin.StrictDecoder(json.RawMessage(test.parameters)), nil)
74+
require.Error(t, err)
75+
assert.Contains(t, err.Error(), test.wantErr)
76+
assert.Nil(t, producer)
77+
})
78+
}
79+
}
80+
81+
func TestExtract_StringValue(t *testing.T) {
82+
producer, err := NewProducer("gpu-product", parameters{
83+
Label: testLabel,
84+
AttributeKey: "gpu.product",
85+
ValueType: valueTypeString,
86+
})
87+
require.NoError(t, err)
88+
89+
endpoint := newEndpoint(map[string]string{testLabel: "H100"})
90+
require.NoError(t, producer.Extract(context.Background(), fwkdl.EndpointEvent{
91+
Type: fwkdl.EventAddOrUpdate,
92+
Endpoint: endpoint,
93+
}))
94+
95+
value, ok := attrstring.ReadValue(endpoint.GetAttributes(), "gpu.product")
96+
require.True(t, ok)
97+
assert.Equal(t, attrstring.Value("H100"), value)
98+
}
99+
100+
func TestExtract_NumberValue(t *testing.T) {
101+
producer, err := NewProducer("gpu-score", parameters{
102+
Label: "gpu.score",
103+
AttributeKey: "gpu.score",
104+
ValueType: valueTypeNumber,
105+
})
106+
require.NoError(t, err)
107+
108+
endpoint := newEndpoint(map[string]string{"gpu.score": "2.5"})
109+
require.NoError(t, producer.Extract(context.Background(), fwkdl.EndpointEvent{
110+
Type: fwkdl.EventAddOrUpdate,
111+
Endpoint: endpoint,
112+
}))
113+
114+
value, ok := attrmetrics.ReadScalarMetricValue(endpoint.GetAttributes(), "gpu.score")
115+
require.True(t, ok)
116+
assert.Equal(t, attrmetrics.ScalarMetricValue(2.5), value)
117+
}
118+
119+
func TestExtract_MissingLabelWritesFallbackValue(t *testing.T) {
120+
producer, err := NewProducer("gpu-product", parameters{
121+
Label: testLabel,
122+
AttributeKey: "gpu.product",
123+
ValueType: valueTypeString,
124+
})
125+
require.NoError(t, err)
126+
127+
endpoint := newEndpoint(nil)
128+
require.NoError(t, producer.Extract(context.Background(), fwkdl.EndpointEvent{
129+
Type: fwkdl.EventAddOrUpdate,
130+
Endpoint: endpoint,
131+
}))
132+
133+
value, ok := attrstring.ReadValue(endpoint.GetAttributes(), "gpu.product")
134+
require.True(t, ok)
135+
assert.Empty(t, value)
136+
}
137+
138+
func TestRegisterDependencies(t *testing.T) {
139+
producer, err := NewProducer("gpu-product", parameters{
140+
Label: testLabel,
141+
AttributeKey: "gpu.product",
142+
})
143+
require.NoError(t, err)
144+
145+
var registrar captureRegistrar
146+
require.NoError(t, producer.RegisterDependencies(&registrar))
147+
assert.Equal(t, "endpoint-notification-source", registrar.registration.SourceType)
148+
assert.Same(t, producer, registrar.registration.Extractor)
149+
}

0 commit comments

Comments
 (0)