Skip to content

Commit 28d17d2

Browse files
committed
Make multimodal cache weights use optional token data
Use TokenizedPrompt as an optional data dependency so multimodal cache affinity can use tokenized placeholder lengths when token-producer is configured, while preserving the lightweight unit-weight fallback when it is absent. Signed-off-by: Guy Girmonsky <guygir@gmail.com>
1 parent c298aa7 commit 28d17d2

7 files changed

Lines changed: 337 additions & 63 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Sample EPP configuration extending the default scoring baseline with
2+
# tokenized multimodal embeddings cache affinity.
3+
apiVersion: llm-d.ai/v1alpha1
4+
kind: EndpointPickerConfig
5+
plugins:
6+
- type: token-producer
7+
parameters:
8+
modelName: hf-repo/model-name # set to the model used in the vLLM deployment
9+
vllm:
10+
url: http://localhost:8000
11+
- type: endpoint-notification-source
12+
- type: metrics-data-source
13+
- type: core-metrics-extractor
14+
- type: mm-embeddings-cache-producer
15+
parameters:
16+
cacheSizeInMBPerServer: 2048
17+
- type: mm-embeddings-cache-scorer
18+
- type: precise-prefix-cache-scorer
19+
parameters:
20+
tokenProcessorConfig:
21+
blockSize: 64
22+
indexerConfig:
23+
kvBlockIndexConfig:
24+
enableMetrics: true
25+
- type: queue-scorer
26+
- type: kv-cache-utilization-scorer
27+
- type: max-score-picker
28+
- type: single-profile-handler
29+
- type: decode-filter
30+
dataLayer:
31+
sources:
32+
- pluginRef: metrics-data-source
33+
extractors:
34+
- pluginRef: core-metrics-extractor
35+
- pluginRef: endpoint-notification-source
36+
extractors:
37+
- pluginRef: mm-embeddings-cache-producer
38+
- pluginRef: precise-prefix-cache-scorer
39+
schedulingProfiles:
40+
- name: default
41+
plugins:
42+
- pluginRef: decode-filter
43+
- pluginRef: queue-scorer
44+
weight: 2
45+
- pluginRef: kv-cache-utilization-scorer
46+
weight: 2
47+
- pluginRef: precise-prefix-cache-scorer
48+
weight: 3
49+
- pluginRef: mm-embeddings-cache-scorer
50+
weight: 1
51+
- pluginRef: max-score-picker

pkg/epp/datalayer/data_graph.go

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -222,21 +222,33 @@ func buildDAG(producers map[string]plugin.ProducerPlugin, consumers map[string]p
222222
continue
223223
}
224224
dependencies := consumer.Consumes()
225-
if producer.Produces() != nil && dependencies.Required != nil {
226-
for producedKey, producedData := range producer.Produces() {
227-
if consumedData, ok := dependencies.Required[producedKey]; ok {
228-
// Check types are same.
229-
if reflect.TypeOf(producedData) != reflect.TypeOf(consumedData) {
230-
return nil, errors.New("data type mismatch between produced and consumed data for key: " + producedKey.String())
231-
}
232-
if pluginToLayerExecutionOrder(producer) > pluginToLayerExecutionOrder(consumer) {
233-
return nil, errors.New("invalid plugin layer execution order: producer " + pName + " needs to be executed before consumer " + cName)
234-
}
235-
// Consumer depends on producer, so add an edge from consumer to producer.
236-
dag[cName] = append(dag[cName], pName)
237-
break
225+
if producer.Produces() == nil {
226+
continue
227+
}
228+
for producedKey, producedData := range producer.Produces() {
229+
consumedData, ok := dependencies.Required[producedKey]
230+
optional := false
231+
if !ok {
232+
consumedData, ok = dependencies.Optional[producedKey]
233+
optional = ok
234+
}
235+
if !ok {
236+
continue
237+
}
238+
// Check types are same.
239+
if reflect.TypeOf(producedData) != reflect.TypeOf(consumedData) {
240+
if optional {
241+
return nil, errors.New("data type mismatch between produced and optionally consumed data for key: " + producedKey.String())
238242
}
243+
return nil, errors.New("data type mismatch between produced and consumed data for key: " + producedKey.String())
244+
}
245+
if pluginToLayerExecutionOrder(producer) > pluginToLayerExecutionOrder(consumer) {
246+
return nil, errors.New("invalid plugin layer execution order: producer " + pName + " needs to be executed before consumer " + cName)
239247
}
248+
// Consumer depends on producer; optional dependencies only add
249+
// an edge when the producer is already configured.
250+
dag[cName] = append(dag[cName], pName)
251+
break
240252
}
241253
}
242254
}

pkg/epp/datalayer/data_graph_test.go

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,10 @@ import (
3737
const mockProducedDataKey = "mockProducedData"
3838

3939
type mockDataProducerP struct {
40-
name string
41-
produces map[fwkplugin.DataKey]any
42-
consumes map[fwkplugin.DataKey]any
40+
name string
41+
produces map[fwkplugin.DataKey]any
42+
consumes map[fwkplugin.DataKey]any
43+
optionalConsumes map[fwkplugin.DataKey]any
4344
}
4445

4546
type mockProducedDataType struct {
@@ -59,7 +60,7 @@ func (m *mockDataProducerP) Produces() map[fwkplugin.DataKey]any {
5960
}
6061

6162
func (m *mockDataProducerP) Consumes() fwkplugin.DataDependencies {
62-
return fwkplugin.DataDependencies{Required: m.consumes}
63+
return fwkplugin.DataDependencies{Required: m.consumes, Optional: m.optionalConsumes}
6364
}
6465

6566
func (m *mockDataProducerP) Produce(ctx context.Context, request *fwksched.InferenceRequest, endpoints []fwksched.Endpoint) error {
@@ -165,6 +166,7 @@ func TestDAGAndTopologicalOrder(t *testing.T) {
165166
pluginC := &mockDataProducerP{name: "C", consumes: map[fwkplugin.DataKey]any{dkB: nil}}
166167
pluginD := &mockDataProducerP{name: "D", consumes: map[fwkplugin.DataKey]any{dkA: nil}}
167168
pluginE := &mockDataProducerP{name: "E"} // No dependencies
169+
pluginF := &mockDataProducerP{name: "F", optionalConsumes: map[fwkplugin.DataKey]any{dkA: nil}}
168170

169171
// Cycle plugins
170172
pluginX := &mockDataProducerP{name: "X", produces: map[fwkplugin.DataKey]any{dkX: nil}, consumes: map[fwkplugin.DataKey]any{dkY: nil}}
@@ -220,6 +222,15 @@ func TestDAGAndTopologicalOrder(t *testing.T) {
220222
},
221223
expectedErr: "",
222224
},
225+
{
226+
name: "Optional dependency orders when producer is configured",
227+
plugins: []fwkrc.DataProducer{pluginA, pluginF},
228+
expectedDAG: map[string][]string{
229+
"A/mock": {},
230+
"F/mock": {"A/mock"},
231+
},
232+
expectedErr: "",
233+
},
223234
{
224235
name: "Graph with a cycle (X -> Y, Y -> X)",
225236
plugins: []fwkrc.DataProducer{pluginX, pluginY},
@@ -345,6 +356,15 @@ func TestCreateMissingDataProducers(t *testing.T) {
345356
factoryRegistry: map[string]fwkplugin.FactoryFunc{producerTypeA: producerAFactory},
346357
wantTypes: nil,
347358
},
359+
{
360+
name: "optional consumes does not create missing producer",
361+
existingPlugins: []fwkplugin.Plugin{
362+
&mockDataProducerP{name: "optional-consumer", optionalConsumes: map[fwkplugin.DataKey]any{keyA: nil}},
363+
},
364+
defaultProducerRegistry: map[string]string{keyA.String(): producerTypeA},
365+
factoryRegistry: map[string]fwkplugin.FactoryFunc{producerTypeA: producerAFactory},
366+
wantTypes: nil,
367+
},
348368
{
349369
name: "producer already present by type - not duplicated",
350370
existingPlugins: []fwkplugin.Plugin{
@@ -479,7 +499,7 @@ func (m *mockMayConsumerPlugin) Consumes() fwkplugin.DataDependencies {
479499
return fwkplugin.DataDependencies{Optional: m.optionalConsumes}
480500
}
481501

482-
// mockMixedConsumerPlugin is a plugin that has both required Consumes and optional OptionalConsumes.
502+
// mockMixedConsumerPlugin is a plugin that has both required and optional data dependencies.
483503
// This models a real plugin like prefix cache scorer — requires prefix-match data,
484504
// but optionally uses tokenized input and falls back to raw text if unavailable.
485505
type mockMixedConsumerPlugin struct {
@@ -511,7 +531,7 @@ func TestCreateMissingDataProducers_MayConsume(t *testing.T) {
511531
wantErr bool
512532
}{
513533
{
514-
name: "OptionalConsumes key with no producer — warning only, no error",
534+
name: "optional key with no producer — warning only, no error",
515535
existingPlugins: []fwkplugin.Plugin{
516536
&mockMayConsumerPlugin{
517537
name: "optional-consumer",
@@ -522,7 +542,7 @@ func TestCreateMissingDataProducers_MayConsume(t *testing.T) {
522542
wantErr: false, // must NOT error
523543
},
524544
{
525-
name: "OptionalConsumes key with a producer present — no warning, no error",
545+
name: "optional key with a producer present — no warning, no error",
526546
existingPlugins: []fwkplugin.Plugin{
527547
&mockDataProducerP{name: "producer", produces: map[fwkplugin.DataKey]any{keyA: nil}},
528548
&mockMayConsumerPlugin{
@@ -535,10 +555,10 @@ func TestCreateMissingDataProducers_MayConsume(t *testing.T) {
535555
},
536556
{
537557
// Models the real prefix cache scorer — it requires prefix-match data (Consumes)
538-
// but optionally uses tokenized input (OptionalConsumes), falling back to raw text.
558+
// but optionally uses tokenized input, falling back to raw text.
539559
// The required key has a producer. The optional key does not.
540560
// Result: no error. Warning logged for the missing optional key.
541-
name: "plugin with both Consumes and OptionalConsumes — required key has producer, optional does not",
561+
name: "plugin with both required and optional dependencies — required key has producer, optional does not",
542562
existingPlugins: []fwkplugin.Plugin{
543563
&mockDataProducerP{name: "required-producer", produces: map[fwkplugin.DataKey]any{keyA: nil}},
544564
&mockMixedConsumerPlugin{

pkg/epp/framework/plugins/requestcontrol/dataproducer/multimodal/README.md

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ Produces multimodal embeddings cache match data for downstream scheduling plugin
88

99
For each request, the producer extracts stable multimodal item hashes from:
1010

11-
- `TokenizedPrompt.MultiModalFeatures`, when a `token-producer` is configured
11+
- `TokenizedPrompt.MultiModalFeatures`, when tokenized multimodal metadata is
12+
available on the request
1213
- typed OpenAI chat-completions structured media blocks, as a lightweight fallback
14+
- `Generate.Features.MMHashes`, when present
1315

1416
It keeps an in-memory LRU map from multimodal hash to the set of pods that recently
1517
handled that item. During scheduling, it attaches `EncoderCacheMatchInfo` to each
@@ -18,15 +20,24 @@ same image, video, or audio input.
1820

1921
Repeated references to the same multimodal hash within one request count once.
2022

21-
## Inputs Consumed
23+
## Item Weights
24+
25+
Each matched multimodal item contributes to encoder-cache affinity. The scorer
26+
computes `matchedWeight / totalWeight`; this producer defines the per-item
27+
weight in that ratio.
2228

23-
This plugin declares:
29+
- When tokenized multimodal metadata is available, each item weight is
30+
`MultiModalFeature.Length` (falling back to `1` when length is zero).
31+
- Without tokenized multimodal metadata, each unique multimodal hash has item
32+
weight `1`.
2433

25-
- `TokenizedPrompt`
34+
## Inputs Consumed
2635

27-
When `token-producer` is present, this orders tokenization before multimodal match
28-
data production. If tokenized prompt data is absent at runtime, the producer falls
29-
back to typed structured chat-completions media blocks.
36+
This producer declares `TokenizedPrompt` from `token-producer` as an optional
37+
dependency. If `token-producer` is configured, this producer runs after it and
38+
uses `TokenizedPrompt.MultiModalFeatures` placeholder lengths. If tokenized
39+
prompt data is absent at runtime, the producer falls back to typed structured
40+
chat-completions media blocks with item weight `1` per hash.
3041

3142
## Data Produced
3243

@@ -41,7 +52,7 @@ The producer supports the following runtime parameters:
4152
- `cacheSizeInMBPerServer` (integer, default: `2048`, 2 GiB): per-endpoint memory budget in
4253
mebibytes (MiB) for the best-effort pod-affinity LRU.
4354

44-
**Configuration Examples:**
55+
**Lightweight configuration example (tokenizer-free chat media):**
4556

4657
```yaml
4758
plugins:
@@ -60,13 +71,15 @@ schedulingProfiles:
6071
weight: 2
6172
```
6273
74+
**Tokenized multimodal weight example:**
75+
6376
```yaml
6477
plugins:
6578
- type: token-producer
6679
parameters:
6780
modelName: Qwen/Qwen2.5-1.5B-Instruct
6881
vllm:
69-
http: http://localhost:8000
82+
url: http://localhost:8000
7083
- type: mm-embeddings-cache-producer
7184
parameters:
7285
cacheSizeInMBPerServer: 2048
@@ -81,6 +94,9 @@ schedulingProfiles:
8194
## Operational Notes
8295
8396
- The cache is a best-effort routing signal, not a correctness dependency.
84-
- The producer remains tokenizer-free for request shapes where typed media blocks are
85-
sufficient; `token-producer` is only required when relying on upstream multimodal
86-
metadata.
97+
- `cacheSizeInMBPerServer` bounds the EPP routing LRU only; it is not a model-server
98+
encoder cache capacity knob.
99+
- Structured chat media blocks are enough for lightweight cache affinity;
100+
`token-producer` is not required.
101+
- Configure `token-producer` when multimodal placeholder lengths should
102+
influence affinity scores.

0 commit comments

Comments
 (0)