Skip to content

Commit 20db0a1

Browse files
Merge branch 'main' into fix/per-prompt-mm-features
2 parents 4175bc7 + 6c2dd71 commit 20db0a1

13 files changed

Lines changed: 610 additions & 165 deletions

File tree

pkg/epp/framework/plugins/requestcontrol/dataproducer/preciseprefixcache/producer.go

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"encoding/json"
2222
"errors"
2323
"fmt"
24+
"sort"
2425
"time"
2526

2627
"github.com/jellydator/ttlcache/v3"
@@ -64,7 +65,19 @@ type PluginConfig struct {
6465
SpeculativeTTL string `json:"speculativeTTL"`
6566
}
6667

67-
var _ requestcontrol.DataProducer = &Producer{}
68+
var (
69+
_ requestcontrol.DataProducer = &Producer{}
70+
_ plugin.StateDumper = &Producer{}
71+
)
72+
73+
// subscriberManager is the subset of kvevents.SubscriberManager the producer
74+
// relies on, narrowed so tests can substitute a fake.
75+
type subscriberManager interface {
76+
EnsureSubscriber(ctx context.Context, podIdentifier, endpoint, topicFilter string, remoteSocket bool) error
77+
RemoveSubscriber(ctx context.Context, podIdentifier string)
78+
GetActiveSubscribers() ([]string, []string)
79+
Shutdown(ctx context.Context)
80+
}
6881

6982
// Producer is a DataProducer plugin that maintains a KV-block prefix-cache
7083
// index by subscribing to vLLM KV-events and writes per-endpoint
@@ -78,7 +91,7 @@ type Producer struct {
7891
typedName plugin.TypedName
7992
kvCacheIndexer kvCacheIndexer
8093

81-
subscribersManager *kvevents.SubscriberManager
94+
subscribersManager subscriberManager
8295
kvEventsConfig *kvevents.Config
8396

8497
kvBlockScorer kvcache.KVBlockScorer
@@ -202,6 +215,70 @@ func (p *Producer) TypedName() plugin.TypedName {
202215
return p.typedName
203216
}
204217

218+
// Debug-dump caps keep the payload bounded. A list is partial when its matching
219+
// TotalX exceeds MaxX.
220+
const (
221+
maxDumpSubscribers = 100
222+
maxDumpSpeculativeEntries = 100
223+
)
224+
225+
// precisePrefixState is the snapshot returned by DumpState. The KV-block index
226+
// is keyed by prompt-derived block hashes and is not enumerable, so it is not
227+
// reported; the active subscriber pod identities and the live speculative
228+
// request ids are enumerated (sorted and capped) for debugging.
229+
type precisePrefixState struct {
230+
Subscribers []string `json:"subscribers"`
231+
TotalSubscribers int `json:"totalSubscribers"`
232+
MaxSubscribers int `json:"maxSubscribers"`
233+
SpeculativeIndexing bool `json:"speculativeIndexing"`
234+
SpeculativeEntries []string `json:"speculativeEntries"`
235+
TotalSpeculativeEntries int `json:"totalSpeculativeEntries"`
236+
MaxSpeculativeEntries int `json:"maxSpeculativeEntries"`
237+
BlockSizeTokens int `json:"blockSizeTokens"`
238+
}
239+
240+
// DumpState reports the producer's bounded operational state: the active
241+
// KV-event subscriber pod identities, whether speculative indexing is on and
242+
// the live speculative request ids, and the block size in tokens. Both lists
243+
// are sorted and capped; the prompt-derived block index is not exposed.
244+
func (p *Producer) DumpState() (json.RawMessage, error) {
245+
subscribers := []string{}
246+
var totalSubscribers int
247+
if p.subscribersManager != nil {
248+
ids, _ := p.subscribersManager.GetActiveSubscribers()
249+
totalSubscribers = len(ids)
250+
subscribers = sortedCapped(ids, maxDumpSubscribers)
251+
}
252+
speculativeEntries := []string{}
253+
var totalSpeculativeEntries int
254+
if p.speculativeCache != nil {
255+
keys := p.speculativeCache.Keys()
256+
totalSpeculativeEntries = len(keys)
257+
speculativeEntries = sortedCapped(keys, maxDumpSpeculativeEntries)
258+
}
259+
return json.Marshal(precisePrefixState{
260+
Subscribers: subscribers,
261+
TotalSubscribers: totalSubscribers,
262+
MaxSubscribers: maxDumpSubscribers,
263+
SpeculativeIndexing: p.speculativeEnabled,
264+
SpeculativeEntries: speculativeEntries,
265+
TotalSpeculativeEntries: totalSpeculativeEntries,
266+
MaxSpeculativeEntries: maxDumpSpeculativeEntries,
267+
BlockSizeTokens: p.blockSizeTokens,
268+
})
269+
}
270+
271+
// sortedCapped returns a sorted copy of in (never nil, so empty lists serialize
272+
// as [] not null), truncated to limit entries.
273+
func sortedCapped(in []string, limit int) []string {
274+
out := append([]string{}, in...)
275+
sort.Strings(out)
276+
if len(out) > limit {
277+
out = out[:limit]
278+
}
279+
return out
280+
}
281+
205282
// Produces declares the PrefixCacheMatchInfoDataKey published per endpoint,
206283
// name-bound to this producer instance.
207284
func (p *Producer) Produces() map[plugin.DataKey]any {

pkg/epp/framework/plugins/requestcontrol/dataproducer/preciseprefixcache/producer_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ package preciseprefixcache
1919
import (
2020
"context"
2121
"encoding/json"
22+
"fmt"
2223
"testing"
2324

25+
"github.com/jellydator/ttlcache/v3"
2426
"github.com/llm-d/llm-d-router/pkg/kvcache"
2527
"github.com/llm-d/llm-d-router/pkg/kvcache/kvblock"
2628
"github.com/llm-d/llm-d-router/pkg/kvevents"
@@ -675,3 +677,99 @@ func TestNew_BlockSizeFlowsViaTokenProcessor(t *testing.T) {
675677
})
676678
}
677679
}
680+
681+
type fakeSubscriberManager struct {
682+
ids []string
683+
endpoints []string
684+
}
685+
686+
func (f *fakeSubscriberManager) EnsureSubscriber(_ context.Context, _, _, _ string, _ bool) error {
687+
return nil
688+
}
689+
func (f *fakeSubscriberManager) RemoveSubscriber(_ context.Context, _ string) {}
690+
func (f *fakeSubscriberManager) GetActiveSubscribers() ([]string, []string) {
691+
return f.ids, f.endpoints
692+
}
693+
func (f *fakeSubscriberManager) Shutdown(_ context.Context) {}
694+
695+
func TestDumpState(t *testing.T) {
696+
// Start() is intentionally not called: NoTTL entries never expire, and the
697+
// enumeration helpers work on an unstarted cache.
698+
specCache := ttlcache.New[string, *speculativeEntries]()
699+
specCache.Set("req-2", &speculativeEntries{}, ttlcache.NoTTL)
700+
specCache.Set("req-1", &speculativeEntries{}, ttlcache.NoTTL)
701+
702+
p := &Producer{
703+
typedName: plugin.TypedName{Type: PluginType, Name: "x"},
704+
subscribersManager: &fakeSubscriberManager{ids: []string{"ns/pod-b", "ns/pod-a"}},
705+
speculativeCache: specCache,
706+
speculativeEnabled: true,
707+
blockSizeTokens: 64,
708+
}
709+
710+
payload, err := p.DumpState()
711+
require.NoError(t, err)
712+
// Subscriber pod identities and live request ids are enumerated for debugging.
713+
assert.Contains(t, string(payload), "ns/pod-a")
714+
715+
var state precisePrefixState
716+
require.NoError(t, json.Unmarshal(payload, &state))
717+
assert.Equal(t, precisePrefixState{
718+
Subscribers: []string{"ns/pod-a", "ns/pod-b"},
719+
TotalSubscribers: 2,
720+
MaxSubscribers: maxDumpSubscribers,
721+
SpeculativeIndexing: true,
722+
SpeculativeEntries: []string{"req-1", "req-2"},
723+
TotalSpeculativeEntries: 2,
724+
MaxSpeculativeEntries: maxDumpSpeculativeEntries,
725+
BlockSizeTokens: 64,
726+
}, state)
727+
}
728+
729+
func TestDumpStateEmpty(t *testing.T) {
730+
p := &Producer{}
731+
732+
payload, err := p.DumpState()
733+
require.NoError(t, err)
734+
assert.True(t, json.Valid(payload))
735+
// Empty lists serialize as [] not null, matching the documented response shape.
736+
assert.Contains(t, string(payload), `"subscribers":[]`)
737+
assert.Contains(t, string(payload), `"speculativeEntries":[]`)
738+
739+
var state precisePrefixState
740+
require.NoError(t, json.Unmarshal(payload, &state))
741+
assert.Equal(t, precisePrefixState{
742+
Subscribers: []string{},
743+
SpeculativeEntries: []string{},
744+
MaxSubscribers: maxDumpSubscribers,
745+
MaxSpeculativeEntries: maxDumpSpeculativeEntries,
746+
}, state)
747+
}
748+
749+
func TestDumpStateCaps(t *testing.T) {
750+
specCache := ttlcache.New[string, *speculativeEntries]()
751+
ids := make([]string, 0, maxDumpSubscribers+5)
752+
for i := 0; i < maxDumpSubscribers+5; i++ {
753+
ids = append(ids, fmt.Sprintf("ns/pod-%03d", i))
754+
}
755+
for i := 0; i < maxDumpSpeculativeEntries+7; i++ {
756+
specCache.Set(fmt.Sprintf("req-%04d", i), &speculativeEntries{}, ttlcache.NoTTL)
757+
}
758+
759+
p := &Producer{
760+
subscribersManager: &fakeSubscriberManager{ids: ids},
761+
speculativeCache: specCache,
762+
}
763+
764+
payload, err := p.DumpState()
765+
require.NoError(t, err)
766+
767+
var state precisePrefixState
768+
require.NoError(t, json.Unmarshal(payload, &state))
769+
// Lists are capped, but the totals report the full counts so a consumer can
770+
// tell the dump is partial from totalX > maxX.
771+
assert.Len(t, state.Subscribers, maxDumpSubscribers)
772+
assert.Equal(t, maxDumpSubscribers+5, state.TotalSubscribers)
773+
assert.Len(t, state.SpeculativeEntries, maxDumpSpeculativeEntries)
774+
assert.Equal(t, maxDumpSpeculativeEntries+7, state.TotalSpeculativeEntries)
775+
}

pkg/epp/framework/plugins/scheduling/scorer/tokenload/token_load.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323

2424
"sigs.k8s.io/controller-runtime/pkg/log"
2525

26+
logutil "github.com/llm-d/llm-d-router/pkg/common/observability/logging"
2627
fwkplugin "github.com/llm-d/llm-d-router/pkg/epp/framework/interface/plugin"
2728
fwksched "github.com/llm-d/llm-d-router/pkg/epp/framework/interface/scheduling"
2829
attrconcurrency "github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/datalayer/attribute/concurrency"
@@ -122,7 +123,7 @@ func (s *TokenLoadScorer) Score(ctx context.Context, _ *fwksched.InferenceReques
122123
score = 1.0 - (tokenLoad / s.queueThresholdTokens)
123124
}
124125
scores[endpoint] = score
125-
logger.V(1).Info("TokenLoadScorer scoring", "endpoint", endpointID, "tokenLoad", tokenLoad, "score", score)
126+
logger.V(logutil.DEBUG).Info("TokenLoadScorer scoring", "endpoint", endpointID, "tokenLoad", tokenLoad, "score", score)
126127
}
127128

128129
return scores

0 commit comments

Comments
 (0)