Skip to content

Commit 5b3cdd7

Browse files
committed
Add GPU utilization filter and scorer plugins
Scheduling plugins that route requests away from heavily loaded GPUs. The filter drops endpoints above a configurable utilization threshold (default 0.90) with graceful fallback; the scorer ranks endpoints inversely to their utilization (category: Distribution). Both consume GPUUtilization produced by the DCGM extractor from PR1. Part of #1706 Signed-off-by: noalimoy <nlimoy@redhat.com>
1 parent 6181fd9 commit 5b3cdd7

8 files changed

Lines changed: 636 additions & 0 deletions

File tree

cmd/epp/runner/runner.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ import (
108108
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/requesthandling/parsers/vllmhttp"
109109
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/scheduling/filter/bylabel"
110110
endpointattributefilter "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/scheduling/filter/endpointattribute"
111+
filtergpuutil "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/scheduling/filter/gpuutilization"
111112
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/scheduling/filter/prefixcacheaffinity"
112113
sessionaffinityfilter "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/scheduling/filter/sessionaffinity"
113114
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/scheduling/filter/sloheadroomtier"
@@ -120,6 +121,7 @@ import (
120121
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/scheduling/scorer/activerequest"
121122
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/scheduling/scorer/contextlengthaware"
122123
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/scheduling/scorer/endpointattribute"
124+
scorergpuutil "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/scheduling/scorer/gpuutilization"
123125
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/scheduling/scorer/kvcacheutilization"
124126
latencyscorer "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/scheduling/scorer/latency"
125127
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/scheduling/scorer/loadaware"
@@ -575,6 +577,10 @@ func (r *Runner) registerInTreePlugins() {
575577
fwkplugin.Register(srcdcgm.DCGMDataSourceType, srcdcgm.DCGMDataSourceFactory)
576578
fwkplugin.Register(attrgpu.DCGMExtractorType, extdcgm.DCGMExtractorFactory)
577579

580+
// GPU utilization filter/scorer
581+
fwkplugin.Register(filtergpuutil.PluginType, filtergpuutil.Factory)
582+
fwkplugin.Register(scorergpuutil.PluginType, scorergpuutil.Factory)
583+
578584
fwkplugin.Register(prefix.PrefixCacheScorerPluginType, prefix.PrefixCachePluginFactory)
579585
fwkplugin.Register(maxscore.MaxScorePickerType, maxscore.MaxScorePickerFactory)
580586
fwkplugin.Register(random.RandomPickerType, random.RandomPickerFactory)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Sample EPP configuration: GPU utilization-aware routing
2+
# Routes requests to pods with lower GPU utilization, filtering out overloaded endpoints.
3+
apiVersion: llm-d.ai/v1alpha1
4+
kind: EndpointPickerConfig
5+
plugins:
6+
- type: dcgm-data-source
7+
name: dcgm-source
8+
parameters:
9+
port: 9400
10+
- type: dcgm-extractor
11+
name: dcgm-extractor
12+
- type: gpu-utilization-filter
13+
parameters:
14+
threshold: 0.90
15+
- type: gpu-utilization-scorer
16+
- type: kv-cache-utilization-scorer
17+
- type: max-score-picker
18+
- type: single-profile-handler
19+
- type: decode-filter
20+
dataLayer:
21+
sources:
22+
- pluginRef: dcgm-source
23+
extractors:
24+
- pluginRef: dcgm-extractor
25+
schedulingProfiles:
26+
- name: default
27+
plugins:
28+
- pluginRef: decode-filter
29+
- pluginRef: gpu-utilization-filter
30+
- pluginRef: gpu-utilization-scorer
31+
weight: 2.0
32+
- pluginRef: kv-cache-utilization-scorer
33+
weight: 1.0
34+
- pluginRef: max-score-picker
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# GPU Utilization Filter
2+
3+
**Type:** `gpu-utilization-filter`
4+
5+
Filters out endpoints whose GPU compute utilization exceeds a configurable threshold. Endpoints without GPU data pass through unconditionally.
6+
7+
## Behavior
8+
9+
- Endpoints with GPU utilization **at or below** the threshold pass.
10+
- Endpoints **without GPU data** pass (conservative: DCGM may not have scraped yet).
11+
- If **all** endpoints with data exceed the threshold, all original endpoints are returned (graceful fallback).
12+
13+
## Configuration
14+
15+
- `threshold` (float64, optional, default: `0.90`): Maximum GPU utilization in [0.0, 1.0].
16+
17+
```yaml
18+
- type: gpu-utilization-filter
19+
name: gpu-filter
20+
parameters:
21+
threshold: 0.85
22+
```
23+
24+
## Data dependency
25+
26+
Declares a **Required** `Consumes` dependency on `GPUUtilization` (`GPUUtilization/dcgm-extractor`). The framework validates at init that a matching producer is configured.
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
Copyright 2026 The llm-d 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 gpuutilization
18+
19+
import (
20+
"context"
21+
"encoding/json"
22+
"fmt"
23+
24+
"sigs.k8s.io/controller-runtime/pkg/log"
25+
26+
logutil "github.com/llm-d/llm-d-router/pkg/common/observability/logging"
27+
fwkplugin "github.com/llm-d/llm-d-router/pkg/epp/framework/interface/plugin"
28+
fwksched "github.com/llm-d/llm-d-router/pkg/epp/framework/interface/scheduling"
29+
attrgpu "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/datalayer/attribute/gpu"
30+
)
31+
32+
const (
33+
PluginType = "gpu-utilization-filter"
34+
DefaultThreshold = 0.90
35+
)
36+
37+
var _ fwksched.Filter = &Plugin{}
38+
39+
// Config holds configurable parameters for the GPU utilization filter.
40+
type Config struct {
41+
// Threshold is the maximum GPU utilization (0.0-1.0) an endpoint may
42+
// have before it is filtered out. Default: 0.90 (90%).
43+
Threshold float64 `json:"threshold,omitempty"`
44+
}
45+
46+
// Plugin filters endpoints whose GPU utilization exceeds a configurable
47+
// threshold. If all endpoints exceed the threshold, all are kept (graceful
48+
// fallback). Endpoints without GPU data pass through unconditionally.
49+
type Plugin struct {
50+
typedName fwkplugin.TypedName
51+
config Config
52+
gpuUtilDataKey fwkplugin.DataKey
53+
}
54+
55+
// Factory creates a gpu-utilization-filter plugin from configuration.
56+
func Factory(name string, rawParameters *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
57+
config := Config{Threshold: DefaultThreshold}
58+
if rawParameters != nil {
59+
if err := rawParameters.Decode(&config); err != nil {
60+
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
61+
}
62+
}
63+
if config.Threshold < 0 || config.Threshold > 1.0 {
64+
return nil, fmt.Errorf("threshold must be in [0, 1], got %f", config.Threshold)
65+
}
66+
return &Plugin{
67+
typedName: fwkplugin.TypedName{Type: PluginType, Name: name},
68+
config: config,
69+
gpuUtilDataKey: attrgpu.GPUUtilizationDataKey,
70+
}, nil
71+
}
72+
73+
func (p *Plugin) TypedName() fwkplugin.TypedName {
74+
return p.typedName
75+
}
76+
77+
// Filter keeps endpoints whose GPU utilization is at or below the configured
78+
// threshold. Endpoints without GPU utilization data pass through (the DCGM
79+
// poller may not have scraped yet). If every endpoint with data exceeds the
80+
// threshold, all original endpoints are returned as a graceful fallback.
81+
func (p *Plugin) Filter(ctx context.Context, _ *fwksched.InferenceRequest, endpoints []fwksched.Endpoint) []fwksched.Endpoint {
82+
logger := log.FromContext(ctx)
83+
84+
if len(endpoints) <= 1 {
85+
return endpoints
86+
}
87+
88+
var passed []fwksched.Endpoint
89+
for _, ep := range endpoints {
90+
raw, ok := ep.Get(p.gpuUtilDataKey.String())
91+
if !ok {
92+
passed = append(passed, ep)
93+
continue
94+
}
95+
util := raw.(attrgpu.GPUUtilization)
96+
if float64(util) <= p.config.Threshold {
97+
passed = append(passed, ep)
98+
}
99+
}
100+
101+
if len(passed) == 0 {
102+
logger.V(logutil.DEBUG).Info("GPUUtilizationFilter: all endpoints above threshold, keeping all",
103+
"threshold", p.config.Threshold, "total", len(endpoints))
104+
return endpoints
105+
}
106+
107+
logger.V(logutil.DEBUG).Info("GPUUtilizationFilter: filtered endpoints",
108+
"threshold", p.config.Threshold, "passed", len(passed), "total", len(endpoints))
109+
return passed
110+
}
111+
112+
// Consumes declares a Required dependency on GPUUtilization so the framework
113+
// validates at init that a producer (dcgm-extractor) is configured.
114+
func (p *Plugin) Consumes() fwkplugin.DataDependencies {
115+
return fwkplugin.DataDependencies{
116+
Required: map[fwkplugin.DataKey]any{p.gpuUtilDataKey: attrgpu.GPUUtilization(0)},
117+
}
118+
}

0 commit comments

Comments
 (0)