Skip to content

Commit d858f32

Browse files
authored
Revert "test(e2e): add cache-affinity test for multimodal routing (llm-d#1562)" (llm-d#1955)
This reverts commit 8ae9991. Signed-off-by: Sam Batschelet <sbatsche@redhat.com>
1 parent 8ae9991 commit d858f32

5 files changed

Lines changed: 0 additions & 258 deletions

File tree

test/e2e/configs_test.go

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -242,57 +242,6 @@ schedulingProfiles:
242242
weight: 10
243243
`
244244

245-
// EPP configuration for multimodal cache-affinity routing. Pairs the
246-
// mm-embeddings-cache producer+scorer with token-producer at loopback; the sim
247-
// sidecar (swapped in by usesSimRender) provides mm_features.
248-
const mmCacheAffinityConfig = `apiVersion: llm-d.ai/v1alpha1
249-
kind: EndpointPickerConfig
250-
plugins:
251-
- type: token-producer
252-
parameters:
253-
modelName: food-review
254-
vllm:
255-
url: http://localhost:8000
256-
- type: mm-embeddings-cache-producer
257-
parameters:
258-
cacheSizeInMBPerServer: 64
259-
- type: mm-embeddings-cache-scorer
260-
- type: decode-filter
261-
- type: max-score-picker
262-
- type: single-profile-handler
263-
schedulingProfiles:
264-
- name: default
265-
plugins:
266-
- pluginRef: decode-filter
267-
- pluginRef: mm-embeddings-cache-scorer
268-
weight: 10
269-
- pluginRef: max-score-picker
270-
`
271-
272-
// Same as mmCacheAffinityConfig but the token-producer uses the estimate
273-
// backend, which derives mm_features in-EPP and never calls the render endpoint.
274-
const mmCacheAffinityEstimateConfig = `apiVersion: llm-d.ai/v1alpha1
275-
kind: EndpointPickerConfig
276-
plugins:
277-
- type: token-producer
278-
parameters:
279-
estimate: {}
280-
- type: mm-embeddings-cache-producer
281-
parameters:
282-
cacheSizeInMBPerServer: 64
283-
- type: mm-embeddings-cache-scorer
284-
- type: decode-filter
285-
- type: max-score-picker
286-
- type: single-profile-handler
287-
schedulingProfiles:
288-
- name: default
289-
plugins:
290-
- pluginRef: decode-filter
291-
- pluginRef: mm-embeddings-cache-scorer
292-
weight: 10
293-
- pluginRef: max-score-picker
294-
`
295-
296245
// EPP configuration for running scale model server test
297246
const scaleConfig = `apiVersion: llm-d.ai/v1alpha1
298247
kind: EndpointPickerConfig

test/e2e/e2e_test.go

Lines changed: 0 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -770,99 +770,6 @@ var _ = ginkgo.Describe("Run end to end tests", ginkgo.Ordered, func() {
770770
})
771771
})
772772

773-
ginkgo.When("Running multimodal cache-affinity configuration", ginkgo.Label(extendedTestLabel), func() {
774-
ginkgo.It("should route identical multimodal content to the same decode pod", func() {
775-
infPoolObjects = createInferencePool(1, true)
776-
777-
decodeReplicas := 2
778-
modelServers := createModelServersDecode(decodeReplicas)
779-
epp := createEndPointPicker(mmCacheAffinityConfig)
780-
781-
_, decodePods := getModelServerPods(podSelector, prefillSelector, decodeSelector)
782-
gomega.Expect(decodePods).Should(gomega.HaveLen(decodeReplicas))
783-
784-
for _, tc := range []struct {
785-
name string
786-
send func() (string, string)
787-
}{
788-
{"image", func() (string, string) { return runChatCompletionWithImages(testImageURL) }},
789-
{"audio", runChatCompletionWithAudio},
790-
{"video", runChatCompletionWithVideo},
791-
} {
792-
ginkgo.By("modality=" + tc.name)
793-
794-
ns1, pod1 := tc.send()
795-
gomega.Expect(ns1).Should(gomega.Equal(nsName))
796-
gomega.Expect(pod1).Should(gomega.BeElementOf(decodePods))
797-
798-
ns2, pod2 := tc.send()
799-
gomega.Expect(ns2).Should(gomega.Equal(nsName))
800-
gomega.Expect(pod2).Should(gomega.Equal(pod1))
801-
}
802-
803-
// A/B symmetry: each content key independently routes back to its
804-
// first-served pod. Catches "first request wins forever" bugs that
805-
// the same-content loop above can't distinguish.
806-
ginkgo.By("A/B image symmetry")
807-
_, podA := runChatCompletionWithImages(testImageURL)
808-
_, podB := runChatCompletionWithImages(testImageURL2)
809-
_, podARe := runChatCompletionWithImages(testImageURL)
810-
gomega.Expect(podARe).Should(gomega.Equal(podA))
811-
_, podBRe := runChatCompletionWithImages(testImageURL2)
812-
gomega.Expect(podBRe).Should(gomega.Equal(podB))
813-
814-
// Mixed image + audio + video in one request. Probes per-item match
815-
// accounting and the empty-hash short-circuit in ExtractMMItems.
816-
ginkgo.By("mixed image+audio+video cache affinity")
817-
_, podMix := runChatCompletionWithImageAudioVideo(testImageURL, testAudioData, testVideoURL)
818-
_, podMixRe := runChatCompletionWithImageAudioVideo(testImageURL, testAudioData, testVideoURL)
819-
gomega.Expect(podMixRe).Should(gomega.Equal(podMix))
820-
821-
// hits is the matched subset of queries, so hits < queries (never equal).
822-
// PreRequest writes the LRU async (wg.Go) → Eventually.
823-
ginkgo.By("metrics: hits_total + queries_total")
824-
gomega.Eventually(func() float64 {
825-
return getMetricValue("llm_d_router_epp_encoder_cache_queries_total",
826-
map[string]string{"modality": "image"})
827-
}, 5*time.Second, 250*time.Millisecond).Should(gomega.BeNumerically(">=", 1))
828-
gomega.Eventually(func() float64 {
829-
return getMetricValue("llm_d_router_epp_encoder_cache_hits_total",
830-
map[string]string{"modality": "image"})
831-
}, 5*time.Second, 250*time.Millisecond).Should(gomega.BeNumerically(">=", 1))
832-
833-
testutils.DeleteObjects(testConfig, epp)
834-
testutils.DeleteObjects(testConfig, modelServers)
835-
})
836-
837-
// Estimate token-producer: encoder-cache metrics must be labelled per
838-
// modality, not folded into "image" (#1617). Depends on the fix in #1618.
839-
ginkgo.It("should label per-modality metrics with the estimate token-producer", func() {
840-
infPoolObjects = createInferencePool(1, true)
841-
842-
decodeReplicas := 2
843-
modelServers := createModelServersDecode(decodeReplicas)
844-
epp := createEndPointPicker(mmCacheAffinityEstimateConfig)
845-
846-
_, decodePods := getModelServerPods(podSelector, prefillSelector, decodeSelector)
847-
gomega.Expect(decodePods).Should(gomega.HaveLen(decodeReplicas))
848-
849-
// One request per modality so each is recorded by the producer.
850-
runChatCompletionWithImages(testImageURL)
851-
runChatCompletionWithAudio()
852-
runChatCompletionWithVideo()
853-
854-
for _, modality := range []string{"image", "audio", "video"} {
855-
gomega.Eventually(func() float64 {
856-
return getMetricValue("llm_d_router_epp_encoder_cache_queries_total",
857-
map[string]string{"modality": modality})
858-
}, 5*time.Second, 250*time.Millisecond).Should(gomega.BeNumerically(">=", 1), "modality=%s", modality)
859-
}
860-
861-
testutils.DeleteObjects(testConfig, epp)
862-
testutils.DeleteObjects(testConfig, modelServers)
863-
})
864-
})
865-
866773
ginkgo.When("Running simple non-PD KV enabled configuration", ginkgo.Label(extendedTestLabel), func() {
867774
ginkgo.It("should run successfully", func() {
868775
infPoolObjects = createInferencePool(1, true)

test/e2e/requests_test.go

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -216,15 +216,6 @@ func runChatCompletionWithVideo() (string, string) {
216216
return runRawChatCompletion(body)
217217
}
218218

219-
// runChatCompletionWithImageAudioVideo sends one request with image_url,
220-
// input_audio, and video_url blocks to probe per-item accounting across modalities.
221-
func runChatCompletionWithImageAudioVideo(imageURL, audioData, videoURL string) (string, string) {
222-
ginkgo.By("Sending Multimodal Chat Completion Request with image + audio + video")
223-
body := fmt.Sprintf(`{"model":%q,"messages":[{"role":"user","content":[{"type":"image_url","image_url":{"url":%q},"uuid":"image-mixed-0"},{"type":"input_audio","input_audio":{"data":%q,"format":"wav"}},{"type":"video_url","video_url":{"url":%q}},{"type":"text","text":"Describe what you see and hear."}]}],"max_tokens":150}`,
224-
simModelName, imageURL, audioData, videoURL)
225-
return runRawChatCompletion(body)
226-
}
227-
228219
// runChatCompletionWithImageEmbeds sends a chat completion request with an image_embeds content block
229220
// carrying a pre-encoded tensor. image_embeds is not a recognised multimodal type for encode
230221
// disaggregation, so the request routes like a text request (decode-only or prefill-decode).

test/e2e/setup_test.go

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,6 @@ func createEndPointPickerHelper(eppConfig string, replicas int, isLeaderElection
185185
})
186186
if !usesTokenProducer(eppConfig) {
187187
eppYamls = removeRenderSidecar(eppYamls)
188-
} else if usesSimRender(eppConfig) {
189-
eppYamls = swapToSimRenderSidecar(eppYamls, vllmSimImage, simModelName)
190188
}
191189
eppYamls = appendEppArgs(eppYamls, eppExtraArgs)
192190

@@ -207,16 +205,3 @@ func usesTokenProducer(eppConfig string) bool {
207205
}
208206
return false
209207
}
210-
211-
// usesSimRender reports whether the config needs multimodal /render output;
212-
// such configs require the sim sidecar (text-only vLLM emits no mm_features).
213-
func usesSimRender(eppConfig string) bool {
214-
cfg, _, err := configloader.LoadRawConfig([]byte(eppConfig), ginkgo.GinkgoLogr)
215-
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
216-
for _, plugin := range cfg.Plugins {
217-
if plugin.Type == "mm-embeddings-cache-producer" {
218-
return true
219-
}
220-
}
221-
return false
222-
}

test/e2e/utils_test.go

Lines changed: 0 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -29,46 +29,6 @@ const (
2929
deploymentKind = "deployment"
3030
)
3131

32-
// getMetricValue scrapes the EPP /metrics endpoint and returns the sum of all
33-
// series for metricName whose label set matches every entry in filters.
34-
// Returns 0 on scrape error or no match.
35-
func getMetricValue(metricName string, filters map[string]string) float64 {
36-
resp, err := http.Get(fmt.Sprintf("http://localhost:%s/metrics", metricsPort))
37-
if err != nil {
38-
return 0
39-
}
40-
defer resp.Body.Close()
41-
body, err := io.ReadAll(resp.Body)
42-
if err != nil {
43-
return 0
44-
}
45-
var total float64
46-
for _, line := range strings.Split(string(body), "\n") {
47-
if !strings.HasPrefix(line, metricName+"{") && !strings.HasPrefix(line, metricName+" ") {
48-
continue
49-
}
50-
match := true
51-
for k, v := range filters {
52-
if !strings.Contains(line, fmt.Sprintf("%s=%q", k, v)) {
53-
match = false
54-
break
55-
}
56-
}
57-
if !match {
58-
continue
59-
}
60-
parts := strings.Fields(line)
61-
if len(parts) < 2 {
62-
continue
63-
}
64-
f, err := strconv.ParseFloat(parts[len(parts)-1], 64)
65-
if err == nil {
66-
total += f
67-
}
68-
}
69-
return total
70-
}
71-
7232
func scaleDeployment(objects []string, increment int) {
7333
direction := "up"
7434
absIncrement := increment
@@ -287,56 +247,6 @@ func filterDocument(doc string) string {
287247
return strings.TrimRight(string(out), "\n")
288248
}
289249

290-
// swapToSimRenderSidecar rewrites the vllm-render sidecar to run the inference
291-
// sim, which emits mm_features that text-only vLLM does not.
292-
func swapToSimRenderSidecar(eppManifests []string, simImage, modelName string) []string {
293-
outputs := make([]string, len(eppManifests))
294-
for idx, manifest := range eppManifests {
295-
yamlDocs := strings.Split(manifest, "\n---")
296-
rendered := make([]string, 0, len(yamlDocs))
297-
for _, yamlDoc := range yamlDocs {
298-
if strings.TrimSpace(yamlDoc) == "" {
299-
continue
300-
}
301-
rendered = append(rendered, swapRenderSidecarInDoc(yamlDoc, simImage, modelName))
302-
}
303-
outputs[idx] = strings.Join(rendered, "\n---\n")
304-
}
305-
return outputs
306-
}
307-
308-
func swapRenderSidecarInDoc(doc, simImage, modelName string) string {
309-
obj := &unstructured.Unstructured{}
310-
err := yaml.Unmarshal([]byte(doc), &obj.Object)
311-
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
312-
if len(obj.Object) == 0 || obj.GetKind() != "Deployment" {
313-
return doc
314-
}
315-
path := []string{"spec", "template", "spec", "containers"}
316-
containers, found, err := unstructured.NestedSlice(obj.Object, path...)
317-
if err != nil || !found {
318-
return doc
319-
}
320-
for i, raw := range containers {
321-
c, ok := raw.(map[string]any)
322-
if !ok || c["name"] != "vllm-render" {
323-
continue
324-
}
325-
c["image"] = simImage
326-
delete(c, "command")
327-
// --log-http: surface inbound URL/method so CI failures are diagnosable.
328-
c["args"] = []any{"--port=8000", "--model=" + modelName, "--log-http"}
329-
delete(c, "volumeMounts")
330-
containers[i] = c
331-
}
332-
err = unstructured.SetNestedSlice(obj.Object, containers, path...)
333-
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
334-
removePodSpecListItem(obj, "volumes", "model-cache")
335-
out, err := yaml.Marshal(obj.Object)
336-
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
337-
return strings.TrimRight(string(out), "\n")
338-
}
339-
340250
func removePodSpecListItem(obj *unstructured.Unstructured, fieldName, itemName string) {
341251
path := []string{"spec", "template", "spec", fieldName}
342252
items, found, err := unstructured.NestedSlice(obj.Object, path...)

0 commit comments

Comments
 (0)