Skip to content

Commit cc813e5

Browse files
committed
feat(prefix): auto-tune prefix match cap from model max_model_len
The approximate-prefix-cache producer caps prefix matching at a fixed maxPrefixTokensToMatch (default 128k). On models with a larger context window that under-matches long shared prefixes, and it's one more knob to set per model. When autoTune is on and maxPrefixTokensToMatch is left at its default, derive the cap from each endpoint's max_model_len instead, falling back to the existing default when it isn't known. An explicitly configured cap is always respected. max_model_len rides the existing /v1/models attribute. Rather than poll that endpoint on a timer for information that's fixed for an endpoint's lifetime, a new models-endpoint-extractor fetches /v1/models once when an endpoint is added and stores the model list (now carrying max_model_len) as an attribute. It registers as the default producer of that attribute and self-wires an endpoint-notification source, so the producer only declares the dependency and reads the value. A failed or slow fetch leaves the attribute unset and the cap falls back to the default. Signed-off-by: Mayur Das <mayur.das@neevcloud.com>
1 parent 546ea56 commit cc813e5

13 files changed

Lines changed: 657 additions & 20 deletions

File tree

cmd/epp/runner/runner.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,9 @@ func (r *Runner) registerInTreePlugins() {
499499
// data layer models source/extractor
500500
fwkplugin.Register(srcmodels.ModelsDataSourceType, srcmodels.ModelDataSourceFactory)
501501
fwkplugin.Register(attrmodels.ModelsExtractorType, extmodels.ModelServerExtractorFactory)
502+
// Default producer of the /v1/models attribute: fetches once per endpoint on
503+
// add rather than polling, and self-wires its endpoint-notification source.
504+
fwkplugin.RegisterAsDefaultProducer(extmodels.ModelsEndpointExtractorType, extmodels.ModelEndpointExtractorFactory, attrmodels.ModelsAttributeKey)
502505

503506
fwkplugin.Register(prefix.PrefixCacheScorerPluginType, prefix.PrefixCachePluginFactory)
504507
fwkplugin.Register(maxscore.MaxScorePickerType, maxscore.MaxScorePickerFactory)

pkg/epp/framework/plugins/datalayer/attribute/models/data_types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ type ModelDataCollection []ModelData
3737
type ModelData struct {
3838
ID string `json:"id"`
3939
Parent string `json:"parent,omitempty"`
40+
// MaxModelLen is the model's context window in tokens as reported by the
41+
// model server; zero when the server does not report it.
42+
MaxModelLen int `json:"max_model_len,omitempty"`
4043
}
4144

4245
// String returns a string representation of the model info
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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 models
18+
19+
import (
20+
"testing"
21+
22+
"github.com/stretchr/testify/assert"
23+
)
24+
25+
// TestModelDataCollectionClone verifies Clone copies entries (including
26+
// MaxModelLen) and is independent of the original.
27+
func TestModelDataCollectionClone(t *testing.T) {
28+
assert.Nil(t, ModelDataCollection(nil).Clone())
29+
30+
orig := ModelDataCollection{{ID: "m1", MaxModelLen: 100}, {ID: "m2", MaxModelLen: 200}}
31+
cloned, ok := orig.Clone().(ModelDataCollection)
32+
assert.True(t, ok)
33+
assert.Equal(t, orig, cloned)
34+
35+
// Mutating the clone must not affect the original.
36+
cloned[0].MaxModelLen = 999
37+
assert.Equal(t, 100, orig[0].MaxModelLen)
38+
}

pkg/epp/framework/plugins/datalayer/extractor/models/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,26 @@ modelData, ok := attr.(models.ModelDataCollection)
2727
## Configuration
2828

2929
No configuration parameters.
30+
31+
## Once-per-endpoint variant
32+
33+
**Type:** `models-endpoint-extractor`
34+
35+
`ModelEndpointExtractor` produces the same `ModelsAttributeKey` attribute but is
36+
driven by endpoint lifecycle events instead of a poll loop. On endpoint add it
37+
fetches `/v1/models` once and stores the parsed `ModelDataCollection`; the model
38+
list is fixed for an endpoint's lifetime, so it is not re-fetched on a timer. A
39+
failed fetch is logged and skipped, leaving the attribute unset so consumers fall
40+
back to their default.
41+
42+
It is registered as the default producer of `ModelsAttributeKey` and self-wires
43+
to an `endpoint-notification-source` (auto-created when absent), so any consumer
44+
that declares the attribute as a dependency gets it without extra configuration.
45+
46+
### Configuration
47+
48+
| Field | Default | Description |
49+
|---|---|---|
50+
| `scheme` | `http` | Scheme used to reach the model server. |
51+
| `path` | `/v1/models` | Path fetched on the model server. |
52+
| `insecureSkipVerify` | `true` | Skip TLS verification when `scheme` is `https`. |
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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 models
18+
19+
import (
20+
"context"
21+
"encoding/json"
22+
"fmt"
23+
24+
"sigs.k8s.io/controller-runtime/pkg/log"
25+
26+
"github.com/llm-d/llm-d-router/pkg/common/observability/logging"
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+
attrmodels "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/datalayer/attribute/models"
30+
srchttp "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/datalayer/source/http"
31+
srcnotifications "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/datalayer/source/notifications"
32+
)
33+
34+
// ModelsEndpointExtractorType identifies the extractor that fetches /v1/models
35+
// once per endpoint, as opposed to the polling models-data-extractor.
36+
const ModelsEndpointExtractorType = "models-endpoint-extractor"
37+
38+
// Model-server connection defaults, matching the polling models source so both
39+
// reach the same endpoint with the same TLS handling.
40+
const (
41+
defaultModelsScheme = "http"
42+
defaultModelsPath = "/v1/models"
43+
defaultModelsInsecureSkipVerify = true
44+
)
45+
46+
// Assert the extractor produces an attribute, runs on endpoint lifecycle events,
47+
// and self-wires its source dependency.
48+
var (
49+
_ fwkplugin.ProducerPlugin = (*ModelEndpointExtractor)(nil)
50+
_ fwkdl.EndpointExtractor = (*ModelEndpointExtractor)(nil)
51+
_ fwkdl.Registrant = (*ModelEndpointExtractor)(nil)
52+
)
53+
54+
// modelsEndpointExtractorParams configures the model-server endpoint and TLS
55+
// handling. It mirrors the polling models source so operators can point the
56+
// extractor at an https model server.
57+
type modelsEndpointExtractorParams struct {
58+
Scheme string `json:"scheme"`
59+
Path string `json:"path"`
60+
InsecureSkipVerify bool `json:"insecureSkipVerify"`
61+
}
62+
63+
// ModelEndpointExtractor fetches /v1/models once when an endpoint is added and
64+
// stores the parsed model list (including max_model_len) as an endpoint
65+
// attribute. The model list is fixed for an endpoint's lifetime, so it does not
66+
// re-fetch on a timer the way the polling extractor does.
67+
type ModelEndpointExtractor struct {
68+
typedName fwkplugin.TypedName
69+
dk fwkplugin.DataKey
70+
// fetcher performs the one-shot GET and JSON parse, reusing the HTTP source's
71+
// scheme/TLS handling. Only its Poll method is used; the polling Dispatch loop
72+
// is never driven.
73+
fetcher *srchttp.HTTPDataSource[*ModelResponse]
74+
}
75+
76+
// NewModelEndpointExtractor builds an extractor that fetches from
77+
// scheme://<endpoint>/path, verifying the server certificate unless insecure is set.
78+
func NewModelEndpointExtractor(name, scheme, path string, insecure bool) (*ModelEndpointExtractor, error) {
79+
fetcher, err := srchttp.NewHTTPDataSource(scheme, path, insecure, ModelsEndpointExtractorType, name, ParseModels)
80+
if err != nil {
81+
return nil, fmt.Errorf("failed to create models fetcher: %w", err)
82+
}
83+
return &ModelEndpointExtractor{
84+
typedName: fwkplugin.TypedName{Type: ModelsEndpointExtractorType, Name: name},
85+
dk: attrmodels.ModelsAttributeKey,
86+
fetcher: fetcher,
87+
}, nil
88+
}
89+
90+
// TypedName returns the plugin type and name.
91+
func (me *ModelEndpointExtractor) TypedName() fwkplugin.TypedName { return me.typedName }
92+
93+
// Produces declares the /v1/models attribute this extractor populates so the
94+
// framework can wire it to consumers of that attribute.
95+
func (me *ModelEndpointExtractor) Produces() map[fwkplugin.DataKey]any {
96+
return map[fwkplugin.DataKey]any{me.dk: attrmodels.ModelDataCollection{}}
97+
}
98+
99+
// Extract fetches /v1/models once on endpoint add and records the model list as
100+
// an attribute. Delete events need no work; the attribute leaves with the
101+
// endpoint. A failed fetch is logged and skipped rather than returned, so an
102+
// unreachable or still-starting endpoint leaves the attribute unset and the
103+
// consumer falls back to its default.
104+
func (me *ModelEndpointExtractor) Extract(ctx context.Context, event fwkdl.EndpointEvent) error {
105+
// Only an add/update needs a fetch; deletes carry no model data to extract.
106+
if event.Type != fwkdl.EventAddOrUpdate {
107+
return nil
108+
}
109+
if event.Endpoint == nil || event.Endpoint.GetMetadata() == nil {
110+
return nil
111+
}
112+
resp, err := me.fetcher.Poll(ctx, event.Endpoint)
113+
if err != nil {
114+
log.FromContext(ctx).V(logging.DEBUG).Info("failed to fetch /v1/models",
115+
"endpoint", event.Endpoint.GetMetadata().NamespacedName, "err", err)
116+
return nil
117+
}
118+
event.Endpoint.GetAttributes().Put(me.dk.String(), attrmodels.ModelDataCollection(resp.Data))
119+
return nil
120+
}
121+
122+
// RegisterDependencies binds this extractor to an endpoint-notification-source,
123+
// auto-creating that source when the configuration does not already define one.
124+
// This is what makes the extractor wire itself in as a default source.
125+
func (me *ModelEndpointExtractor) RegisterDependencies(r fwkdl.Registrar) error {
126+
return r.Register(fwkdl.PendingRegistration{
127+
Owner: me.TypedName(),
128+
SourceType: srcnotifications.EndpointNotificationSourceType,
129+
Extractor: me,
130+
DefaultSource: srcnotifications.NewEndpointDataSource(
131+
srcnotifications.EndpointNotificationSourceType, srcnotifications.EndpointNotificationSourceType),
132+
})
133+
}
134+
135+
// ModelEndpointExtractorFactory instantiates the extractor from optional JSON
136+
// parameters, defaulting to an http model server with TLS verification skipped.
137+
func ModelEndpointExtractorFactory(name string, parameters *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
138+
params := modelsEndpointExtractorParams{
139+
Scheme: defaultModelsScheme,
140+
Path: defaultModelsPath,
141+
InsecureSkipVerify: defaultModelsInsecureSkipVerify,
142+
}
143+
// Overlay the defaults with any configured values.
144+
if parameters != nil {
145+
if err := parameters.Decode(&params); err != nil {
146+
return nil, err
147+
}
148+
}
149+
return NewModelEndpointExtractor(name, params.Scheme, params.Path, params.InsecureSkipVerify)
150+
}

0 commit comments

Comments
 (0)