Skip to content

Commit 61ddbdb

Browse files
committed
epp: generalize endpoint identity and add endpoint type
Rename EndpointMetadata.PodName to Name -- the endpoint model is not Kubernetes-only (file-discovered endpoints have no pod) and NamespacedName is the identity. Name is always set, so classifying an endpoint no longer requires blanking its identity. Add EndpointMetadata.Type (EndpointTypeEngine, EndpointTypeEPP): what an endpoint is, independent of how it was discovered. Engines are discovered from Pod objects or from a file and are still engines; an EPP endpoint publishes aggregates over the servers it schedules rather than engine metrics. The Kubernetes pod path sets engine, and file-discovery parses the entry's type field, defaulting to engine and rejecting unknown values. The type is an attribute set at creation rather than a label, and the metrics extractor is unchanged: EPP metrics warrant their own extractor rather than overloading engine type. Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
1 parent 44828e2 commit 61ddbdb

18 files changed

Lines changed: 167 additions & 40 deletions

File tree

docs/discovery.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,20 @@ type DiscoveryNotifier interface {
115115
| Field | Type | Description |
116116
|---|---|---|
117117
| `NamespacedName` | `types.NamespacedName` | Unique identity of the endpoint. |
118-
| `PodName` | `string` | Logical name (used in metrics). |
118+
| `Name` | `string` | Name of the workload behind the endpoint, shared by all its ranks. `NamespacedName` is the identity. |
119119
| `Address` | `string` | IP address of the inference server. |
120120
| `Port` | `string` | Port as a string (e.g. `"8000"`). |
121121
| `MetricsHost` | `string` | `host:port` for metrics scraping. Defaults to `address:port` if empty. |
122122
| `Labels` | `map[string]string` | Arbitrary labels available to scheduler plugins. |
123+
| `Type` | `EndpointType` | What the endpoint is: `engine` or `epp`. Defaults to `engine` when omitted in the endpoints file. |
124+
125+
### Endpoint types
126+
127+
`engine` is an inference server. Its load signals are that server's own engine metrics,
128+
and the `llm-d.ai/engine-type` label selects which schema to read (`vllm`, `sglang`, ...).
129+
130+
`epp` is an endpoint picker. Its load signals are aggregates over the servers behind it,
131+
so `llm-d.ai/engine-type` does not apply.
123132

124133
### Ordering contract
125134

@@ -197,6 +206,7 @@ endpoints:
197206
port: <string> # required -- integer 1-65535 as a string
198207
labels: # optional -- arbitrary key/value labels
199208
<key>: <value>
209+
type: <engine|epp> # optional -- defaults to engine
200210
```
201211

202212
Example with two vLLM instances:

pkg/epp/controller/pod_reconciler_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ func TestPodReconciler(t *testing.T) {
219219
gotPods := make([]*corev1.Pod, len(podList))
220220
for idx, pm := range podList {
221221
gotPods[idx] = &corev1.Pod{
222-
ObjectMeta: metav1.ObjectMeta{Name: pm.GetMetadata().PodName, Namespace: pm.GetMetadata().NamespacedName.Namespace},
222+
ObjectMeta: metav1.ObjectMeta{Name: pm.GetMetadata().Name, Namespace: pm.GetMetadata().NamespacedName.Namespace},
223223
Status: corev1.PodStatus{PodIP: pm.GetMetadata().GetIPAddress()},
224224
}
225225
}

pkg/epp/datalayer/logger/logger_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,10 @@ func TestLogger(t *testing.T) {
8484

8585
logOutput := b.read()
8686
assert.Contains(t, logOutput, "Refreshing Prometheus Metrics {\"ReadyPods\": 2}")
87-
assert.Contains(t, logOutput, "Current Pods and metrics gathered {\"Fresh metrics\": \"[Metadata: {NamespacedName:default/pod1 PodName: Address:1.2.3.4:5678")
87+
assert.Contains(t, logOutput, "Current Pods and metrics gathered {\"Fresh metrics\": \"[Metadata: {NamespacedName:default/pod1 Name: Address:1.2.3.4:5678")
8888
assert.Contains(t, logOutput, "Metrics: {ActiveModels:map[modelA:1] WaitingModels:map[modelB:2] MaxActiveModels:5")
8989
assert.Contains(t, logOutput, "RunningRequestsSize:3 WaitingQueueSize:7 KVCacheUsagePercent:42.5 KvCacheMaxTokenCapacity:2048")
90-
assert.Contains(t, logOutput, "Metadata: {NamespacedName:default/pod2 PodName: Address:1.2.3.4:5679")
90+
assert.Contains(t, logOutput, "Metadata: {NamespacedName:default/pod2 Name: Address:1.2.3.4:5679")
9191
assert.Contains(t, logOutput, "\"Stale metrics\": \"[]\"")
9292
}
9393

pkg/epp/datastore/datastore.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,12 +336,13 @@ func (ds *datastore) podUpdateOrAddIfNotExist(ctx context.Context, pod *corev1.P
336336
pods = append(pods,
337337
&fwkdl.EndpointMetadata{
338338
NamespacedName: createEndpointNamespacedName(pod, idx),
339-
PodName: pod.Name,
339+
Name: pod.Name,
340340
Address: pod.Status.PodIP,
341341
NodeAddress: pod.Status.HostIP,
342342
Port: strconv.Itoa(port),
343343
MetricsHost: net.JoinHostPort(pod.Status.PodIP, strconv.Itoa(port)),
344344
Labels: labels,
345+
Type: fwkdl.EndpointTypeEngine,
345346
RankIndex: idx,
346347
})
347348
}
@@ -394,7 +395,7 @@ func (ds *datastore) podUpdateOrAddIfNotExist(ctx context.Context, pod *corev1.P
394395
func (ds *datastore) PodDelete(podName string) {
395396
ds.pods.Range(func(k, v any) bool {
396397
ep := v.(fwkdl.Endpoint)
397-
if ep.GetMetadata().PodName == podName {
398+
if ep.GetMetadata().Name == podName {
398399
ds.pods.Delete(k)
399400
ds.epf.ReleaseEndpoint(ep)
400401
}

pkg/epp/datastore/datastore_test.go

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -514,7 +514,7 @@ func TestPods(t *testing.T) {
514514
gotPods := make([]*corev1.Pod, len(podList))
515515
for idx, pm := range podList {
516516
gotPods[idx] = &corev1.Pod{
517-
ObjectMeta: metav1.ObjectMeta{Name: pm.GetMetadata().PodName, Namespace: pm.GetMetadata().NamespacedName.Namespace},
517+
ObjectMeta: metav1.ObjectMeta{Name: pm.GetMetadata().Name, Namespace: pm.GetMetadata().NamespacedName.Namespace},
518518
Status: corev1.PodStatus{
519519
PodIP: pm.GetMetadata().GetIPAddress(),
520520
HostIP: pm.GetMetadata().GetNodeAddress(),
@@ -659,12 +659,13 @@ func TestEndpointMetadata(t *testing.T) {
659659
Namespace: pod1.Namespace,
660660
},
661661

662-
PodName: pod1.Name,
662+
Name: pod1.Name,
663663
Address: pod1.Status.PodIP,
664664
NodeAddress: pod1.Status.HostIP,
665665
Port: inferencePoolTargetPort,
666666
MetricsHost: net.JoinHostPort(pod1.Status.PodIP, inferencePoolTargetPort),
667667
Labels: map[string]string{},
668+
Type: fwkdl.EndpointTypeEngine,
668669
},
669670
},
670671
op: func(ctx context.Context, ds Datastore) {
@@ -682,25 +683,27 @@ func TestEndpointMetadata(t *testing.T) {
682683
Namespace: pod1.Namespace,
683684
},
684685

685-
PodName: pod1.Name,
686+
Name: pod1.Name,
686687
Address: pod1.Status.PodIP,
687688
NodeAddress: pod1.Status.HostIP,
688689
Port: inferencePoolMultiTargetPort0,
689690
MetricsHost: net.JoinHostPort(pod1.Status.PodIP, inferencePoolMultiTargetPort0),
690691
Labels: map[string]string{},
692+
Type: fwkdl.EndpointTypeEngine,
691693
},
692694
{
693695
NamespacedName: types.NamespacedName{
694696
Name: pod1.Name + "-rank-1",
695697
Namespace: pod1.Namespace,
696698
},
697699

698-
PodName: pod1.Name,
700+
Name: pod1.Name,
699701
Address: pod1.Status.PodIP,
700702
NodeAddress: pod1.Status.HostIP,
701703
Port: inferencePoolMultiTargetPort1,
702704
MetricsHost: net.JoinHostPort(pod1.Status.PodIP, inferencePoolMultiTargetPort1),
703705
Labels: map[string]string{},
706+
Type: fwkdl.EndpointTypeEngine,
704707
RankIndex: 1,
705708
},
706709
},
@@ -719,25 +722,27 @@ func TestEndpointMetadata(t *testing.T) {
719722
Namespace: pod1.Namespace,
720723
},
721724

722-
PodName: pod1.Name,
725+
Name: pod1.Name,
723726
Address: pod1.Status.PodIP,
724727
NodeAddress: pod1.Status.HostIP,
725728
Port: inferencePoolMultiTargetPort0,
726729
MetricsHost: net.JoinHostPort(pod1.Status.PodIP, inferencePoolMultiTargetPort0),
727730
Labels: map[string]string{},
731+
Type: fwkdl.EndpointTypeEngine,
728732
},
729733
{
730734
NamespacedName: types.NamespacedName{
731735
Name: pod1.Name + "-rank-1",
732736
Namespace: pod1.Namespace,
733737
},
734738

735-
PodName: pod1.Name,
739+
Name: pod1.Name,
736740
Address: pod1.Status.PodIP,
737741
NodeAddress: pod1.Status.HostIP,
738742
Port: inferencePoolMultiTargetPort1,
739743
MetricsHost: net.JoinHostPort(pod1.Status.PodIP, inferencePoolMultiTargetPort1),
740744
Labels: map[string]string{},
745+
Type: fwkdl.EndpointTypeEngine,
741746
RankIndex: 1,
742747
},
743748
{
@@ -746,25 +751,27 @@ func TestEndpointMetadata(t *testing.T) {
746751
Namespace: pod2.Namespace,
747752
},
748753

749-
PodName: pod2.Name,
754+
Name: pod2.Name,
750755
Address: pod2.Status.PodIP,
751756
NodeAddress: pod2.Status.HostIP,
752757
Port: inferencePoolMultiTargetPort0,
753758
MetricsHost: net.JoinHostPort(pod2.Status.PodIP, inferencePoolMultiTargetPort0),
754759
Labels: map[string]string{},
760+
Type: fwkdl.EndpointTypeEngine,
755761
},
756762
{
757763
NamespacedName: types.NamespacedName{
758764
Name: pod2.Name + "-rank-1",
759765
Namespace: pod2.Namespace,
760766
},
761767

762-
PodName: pod2.Name,
768+
Name: pod2.Name,
763769
Address: pod2.Status.PodIP,
764770
NodeAddress: pod2.Status.HostIP,
765771
Port: inferencePoolMultiTargetPort1,
766772
MetricsHost: net.JoinHostPort(pod2.Status.PodIP, inferencePoolMultiTargetPort1),
767773
Labels: map[string]string{},
774+
Type: fwkdl.EndpointTypeEngine,
768775
RankIndex: 1,
769776
},
770777
},
@@ -783,25 +790,27 @@ func TestEndpointMetadata(t *testing.T) {
783790
Namespace: pod1.Namespace,
784791
},
785792

786-
PodName: pod1.Name,
793+
Name: pod1.Name,
787794
Address: pod1.Status.PodIP,
788795
NodeAddress: pod1.Status.HostIP,
789796
Port: inferencePoolMultiTargetPort0,
790797
MetricsHost: net.JoinHostPort(pod1.Status.PodIP, inferencePoolMultiTargetPort0),
791798
Labels: map[string]string{},
799+
Type: fwkdl.EndpointTypeEngine,
792800
},
793801
{
794802
NamespacedName: types.NamespacedName{
795803
Name: pod1.Name + "-rank-1",
796804
Namespace: pod1.Namespace,
797805
},
798806

799-
PodName: pod1.Name,
807+
Name: pod1.Name,
800808
Address: pod1.Status.PodIP,
801809
NodeAddress: pod1.Status.HostIP,
802810
Port: inferencePoolMultiTargetPort1,
803811
MetricsHost: net.JoinHostPort(pod1.Status.PodIP, inferencePoolMultiTargetPort1),
804812
Labels: map[string]string{},
813+
Type: fwkdl.EndpointTypeEngine,
805814
RankIndex: 1,
806815
},
807816
},

pkg/epp/framework/interface/datalayer/endpoint_metadata.go

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,48 @@ limitations under the License.
1717
package datalayer
1818

1919
import (
20+
"errors"
2021
"fmt"
2122
"maps"
2223

2324
"k8s.io/apimachinery/pkg/types"
2425
)
2526

26-
// EndpointMetadata represents the relevant Kubernetes Pod state of an inference server.
27+
type EndpointType string
28+
29+
const (
30+
EndpointTypeEngine EndpointType = "engine" // an inference server
31+
EndpointTypeEPP EndpointType = "epp" // an endpoint picker over its own servers
32+
)
33+
34+
// ErrUnknownEndpointType is returned by ParseEndpointType for an unrecognized type.
35+
var ErrUnknownEndpointType = errors.New("unknown endpoint type")
36+
37+
// ParseEndpointType returns the endpoint type named by s, defaulting to engine when
38+
// unset, or ErrUnknownEndpointType for any other value.
39+
func ParseEndpointType(s string) (EndpointType, error) {
40+
switch t := EndpointType(s); t {
41+
case "":
42+
return EndpointTypeEngine, nil
43+
case EndpointTypeEngine, EndpointTypeEPP:
44+
return t, nil
45+
default:
46+
return "", fmt.Errorf("%w %q", ErrUnknownEndpointType, s)
47+
}
48+
}
49+
50+
// EndpointMetadata describes an inference endpoint.
2751
type EndpointMetadata struct {
2852
NamespacedName types.NamespacedName
29-
PodName string
53+
Name string
3054
Address string
3155
// NodeAddress is the node IP hosting this pod (pod.Status.HostIP).
3256
// Empty for non-Kubernetes discovery sources (e.g. file discovery).
3357
NodeAddress string
3458
Port string
3559
MetricsHost string
3660
Labels map[string]string
61+
Type EndpointType
3762
// RankIndex is this endpoint's position in the pool's TargetPorts,
3863
// identifying the pod-local rank in multi-port deployments.
3964
RankIndex int
@@ -60,12 +85,13 @@ func (epm *EndpointMetadata) Clone() *EndpointMetadata {
6085
Name: epm.NamespacedName.Name,
6186
Namespace: epm.NamespacedName.Namespace,
6287
},
63-
PodName: epm.PodName,
88+
Name: epm.Name,
6489
Address: epm.Address,
6590
NodeAddress: epm.NodeAddress,
6691
Port: epm.Port,
6792
MetricsHost: epm.MetricsHost,
6893
Labels: clonedLabels,
94+
Type: epm.Type,
6995
RankIndex: epm.RankIndex,
7096
}
7197
}
@@ -77,11 +103,12 @@ func (epm *EndpointMetadata) Equal(other *EndpointMetadata) bool {
77103
return epm == other
78104
}
79105
return epm.NamespacedName == other.NamespacedName &&
80-
epm.PodName == other.PodName &&
106+
epm.Name == other.Name &&
81107
epm.Address == other.Address &&
82108
epm.NodeAddress == other.NodeAddress &&
83109
epm.Port == other.Port &&
84110
epm.MetricsHost == other.MetricsHost &&
111+
epm.Type == other.Type &&
85112
epm.RankIndex == other.RankIndex &&
86113
maps.Equal(epm.Labels, other.Labels)
87114
}

pkg/epp/framework/interface/datalayer/endpoint_metadata_test.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,12 @@ func TestEndpointMetadataClone(t *testing.T) {
6969
func TestEndpointMetadataEqual(t *testing.T) {
7070
base := &EndpointMetadata{
7171
NamespacedName: types.NamespacedName{Name: "pod-a-rank-0", Namespace: "default"},
72-
PodName: "pod-a",
72+
Name: "pod-a",
7373
Address: "10.0.0.1",
7474
Port: "8000",
7575
MetricsHost: "10.0.0.1:9000",
7676
Labels: map[string]string{"app": "vllm"},
77+
Type: EndpointTypeEngine,
7778
RankIndex: 1,
7879
}
7980

@@ -99,9 +100,9 @@ func TestEndpointMetadataEqual(t *testing.T) {
99100
},
100101
},
101102
{
102-
name: "pod name",
103+
name: "endpoint name",
103104
mutate: func(meta *EndpointMetadata) {
104-
meta.PodName = "pod-b"
105+
meta.Name = "pod-b"
105106
},
106107
},
107108
{
@@ -128,6 +129,12 @@ func TestEndpointMetadataEqual(t *testing.T) {
128129
meta.Labels["app"] = "sglang"
129130
},
130131
},
132+
{
133+
name: "type",
134+
mutate: func(meta *EndpointMetadata) {
135+
meta.Type = EndpointTypeEPP
136+
},
137+
},
131138
{
132139
name: "rank index",
133140
mutate: func(meta *EndpointMetadata) {
@@ -151,7 +158,7 @@ func TestEndpointMetadataString(t *testing.T) {
151158
Name: pod.Name,
152159
Namespace: pod.Namespace,
153160
},
154-
PodName: pod.Name,
161+
Name: pod.Name,
155162
Address: pod.Status.PodIP,
156163
Port: "8000",
157164
MetricsHost: "127.0.0.1:8000",

pkg/epp/framework/interface/scheduling/types_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func (s cloneableString) Clone() fwkdl.Cloneable { return s }
3333
func newTestMetadata(name string) *fwkdl.EndpointMetadata {
3434
return &fwkdl.EndpointMetadata{
3535
NamespacedName: types.NamespacedName{Namespace: "ns", Name: name},
36-
PodName: name,
36+
Name: name,
3737
Address: "10.0.0.1",
3838
Port: "8000",
3939
MetricsHost: "10.0.0.1:9100",

pkg/epp/framework/plugins/datalayer/discovery/file/plugin.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ type EndpointEntry struct {
4848
Address string `json:"address" yaml:"address"`
4949
Port string `json:"port" yaml:"port"`
5050
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
51+
Type string `json:"type,omitempty" yaml:"type,omitempty"`
5152
}
5253

5354
// EndpointsFile is the top-level structure of the endpoints YAML/JSON file.
@@ -246,17 +247,23 @@ func (f *FileDiscovery) load(notifier fwkdl.DiscoveryNotifier) error {
246247
errs = append(errs, fmt.Errorf("endpoint %q: invalid port %q", e.Name, e.Port))
247248
continue
248249
}
250+
epType, err := fwkdl.ParseEndpointType(e.Type)
251+
if err != nil {
252+
errs = append(errs, fmt.Errorf("endpoint %q: %w", e.Name, err))
253+
continue
254+
}
249255
ns := e.Namespace
250256
if ns == "" {
251257
ns = "default"
252258
}
253259
meta := &fwkdl.EndpointMetadata{
254260
NamespacedName: types.NamespacedName{Name: e.Name, Namespace: ns},
255-
PodName: e.Name,
261+
Name: e.Name,
256262
Address: e.Address,
257263
Port: e.Port,
258264
MetricsHost: net.JoinHostPort(e.Address, e.Port),
259265
Labels: e.Labels,
266+
Type: epType,
260267
}
261268
incoming[meta.NamespacedName] = struct{}{}
262269
notifier.Upsert(meta)

0 commit comments

Comments
 (0)