Files
f956207415 deps(llmisvc): upgrade llm-d components to v0.7.0 release (#5596)
Signed-off-by: Vivek Karunai Kiri Ragavan <[email protected]>
Signed-off-by: Pierangelo Di Pilato <[email protected]>
Co-authored-by: Pierangelo Di Pilato <[email protected]>
2026-06-19 15:11:27 -03:00

2005 lines
63 KiB
Go

/*
Copyright 2026 The KServe Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package llmisvc
import (
"context"
"fmt"
"testing"
. "github.com/onsi/gomega"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/yaml"
"github.com/kserve/kserve/pkg/apis/serving/v1alpha2"
)
func TestSchedulerConfigTextLoRA(t *testing.T) {
loraAdapters := []v1alpha2.LLMModelSpec{{}}
tests := []struct {
name string
llmSvc *v1alpha2.LLMInferenceService
wantLoRA bool
}{
{
name: "no LoRA - standard default config",
llmSvc: &v1alpha2.LLMInferenceService{},
wantLoRA: false,
},
{
name: "LoRA nil pointer - no scorer",
llmSvc: &v1alpha2.LLMInferenceService{
Spec: v1alpha2.LLMInferenceServiceSpec{
Model: v1alpha2.LLMModelSpec{LoRA: nil},
},
},
wantLoRA: false,
},
{
name: "LoRA spec with empty adapters - no scorer",
llmSvc: &v1alpha2.LLMInferenceService{
Spec: v1alpha2.LLMInferenceServiceSpec{
Model: v1alpha2.LLMModelSpec{LoRA: &v1alpha2.LoRASpec{}},
},
},
wantLoRA: false,
},
{
name: "LoRA adapters present - scorer included",
llmSvc: &v1alpha2.LLMInferenceService{
Spec: v1alpha2.LLMInferenceServiceSpec{
Model: v1alpha2.LLMModelSpec{
LoRA: &v1alpha2.LoRASpec{Adapters: loraAdapters},
},
},
},
wantLoRA: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewGomegaWithT(t)
text := schedulerConfigText(tt.llmSvc)
var obj map[string]interface{}
g.Expect(yaml.Unmarshal([]byte(text), &obj)).To(Succeed())
plugins := obj["plugins"].([]interface{})
types := make([]string, 0, len(plugins))
for _, p := range plugins {
types = append(types, p.(map[string]interface{})["type"].(string))
}
profiles := obj["schedulingProfiles"].([]interface{})
defaultProfile := profiles[0].(map[string]interface{})
refs := defaultProfile["plugins"].([]interface{})
refNames := make([]string, 0, len(refs))
for _, r := range refs {
refNames = append(refNames, r.(map[string]interface{})["pluginRef"].(string))
}
if tt.wantLoRA {
g.Expect(types).To(ContainElement(loraAffinityScorerPlugin))
g.Expect(refNames[0]).To(Equal(loraAffinityScorerPlugin))
g.Expect(refs[0].(map[string]interface{})["weight"]).To(BeNumerically("==", 4))
} else {
g.Expect(types).NotTo(ContainElement(loraAffinityScorerPlugin))
g.Expect(refNames).NotTo(ContainElement(loraAffinityScorerPlugin))
}
})
}
}
func TestSchedulerConfigTextPDLoRA(t *testing.T) {
loraAdapters := []v1alpha2.LLMModelSpec{{}}
tests := []struct {
name string
llmSvc *v1alpha2.LLMInferenceService
wantLoRA bool
}{
{
name: "P/D without LoRA - no scorer",
llmSvc: &v1alpha2.LLMInferenceService{
Spec: v1alpha2.LLMInferenceServiceSpec{
Prefill: &v1alpha2.WorkloadSpec{},
},
},
wantLoRA: false,
},
{
name: "P/D with LoRA adapters - scorer in both profiles",
llmSvc: &v1alpha2.LLMInferenceService{
Spec: v1alpha2.LLMInferenceServiceSpec{
Prefill: &v1alpha2.WorkloadSpec{},
Model: v1alpha2.LLMModelSpec{
LoRA: &v1alpha2.LoRASpec{Adapters: loraAdapters},
},
},
},
wantLoRA: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewGomegaWithT(t)
text := schedulerConfigText(tt.llmSvc)
var obj map[string]interface{}
g.Expect(yaml.Unmarshal([]byte(text), &obj)).To(Succeed())
plugins := obj["plugins"].([]interface{})
types := make([]string, 0, len(plugins))
for _, p := range plugins {
types = append(types, p.(map[string]interface{})["type"].(string))
}
profiles := obj["schedulingProfiles"].([]interface{})
g.Expect(profiles).To(HaveLen(2))
for _, profile := range profiles {
refs := profile.(map[string]interface{})["plugins"].([]interface{})
refNames := make([]string, 0, len(refs))
for _, r := range refs {
refNames = append(refNames, r.(map[string]interface{})["pluginRef"].(string))
}
if tt.wantLoRA {
g.Expect(types).To(ContainElement(loraAffinityScorerPlugin))
// scorer is second (after the profile's filter plugin)
g.Expect(refNames[1]).To(Equal(loraAffinityScorerPlugin))
g.Expect(refs[1].(map[string]interface{})["weight"]).To(BeNumerically("==", 4))
} else {
g.Expect(refNames).NotTo(ContainElement(loraAffinityScorerPlugin))
}
}
if !tt.wantLoRA {
g.Expect(types).NotTo(ContainElement(loraAffinityScorerPlugin))
}
})
}
}
func TestPreserveSchedulerConfig(t *testing.T) {
defaultSvc := &v1alpha2.LLMInferenceService{}
inlineSvc := &v1alpha2.LLMInferenceService{
Spec: v1alpha2.LLMInferenceServiceSpec{
Router: &v1alpha2.RouterSpec{
Scheduler: &v1alpha2.SchedulerSpec{
Config: &v1alpha2.SchedulerConfigSpec{
Inline: &runtime.RawExtension{Raw: []byte("updated-inline-config")},
},
},
},
},
}
tests := []struct {
name string
llmSvc *v1alpha2.LLMInferenceService
curr *appsv1.Deployment
expected []string
}{
{
name: "no current deployment - generates fresh config",
llmSvc: defaultSvc,
curr: &appsv1.Deployment{},
expected: []string{"--config-text", schedulerConfigText(defaultSvc)},
},
{
name: "current deployment with --config-text - preserves it",
llmSvc: defaultSvc,
curr: &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "main",
Args: []string{"--config-text", "existing-config-yaml"},
},
},
},
},
},
},
expected: []string{"--config-text", "existing-config-yaml"},
},
{
name: "current deployment with -config-text - preserves it",
llmSvc: defaultSvc,
curr: &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "main",
Args: []string{"-config-text", "old-config"},
},
},
},
},
},
},
expected: []string{"-config-text", "old-config"},
},
{
name: "current deployment with --config-file - preserves it",
llmSvc: defaultSvc,
curr: &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "main",
Args: []string{"--config-file", "/etc/scheduler/config.yaml"},
},
},
},
},
},
},
expected: []string{"--config-file", "/etc/scheduler/config.yaml"},
},
{
name: "current deployment with non-main container - ignored",
llmSvc: defaultSvc,
curr: &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "sidecar",
Args: []string{"--config-text", "sidecar-config"},
},
},
},
},
},
},
expected: []string{"--config-text", schedulerConfigText(defaultSvc)},
},
{
name: "inline config overrides existing deployment config",
llmSvc: inlineSvc,
curr: &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "main",
Args: []string{"--config-text", "stale-config"},
},
},
},
},
},
},
expected: []string{"--config-text", "updated-inline-config"},
},
{
name: "inline config used when no existing deployment",
llmSvc: inlineSvc,
curr: &appsv1.Deployment{},
expected: []string{"--config-text", "updated-inline-config"},
},
{
name: "template already has --config-text - returns nil to avoid duplication",
llmSvc: &v1alpha2.LLMInferenceService{
Spec: v1alpha2.LLMInferenceServiceSpec{
Router: &v1alpha2.RouterSpec{
Scheduler: &v1alpha2.SchedulerSpec{
Template: &corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "main",
Args: []string{"--config-text", "template-config", "--poolName", "test"},
},
},
},
},
},
},
},
curr: &appsv1.Deployment{},
expected: nil,
},
{
name: "inline config overrides template config args",
llmSvc: &v1alpha2.LLMInferenceService{
Spec: v1alpha2.LLMInferenceServiceSpec{
Router: &v1alpha2.RouterSpec{
Scheduler: &v1alpha2.SchedulerSpec{
Config: &v1alpha2.SchedulerConfigSpec{
Inline: &runtime.RawExtension{Raw: []byte("inline-override")},
},
Template: &corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "main",
Args: []string{"--config-text", "template-config"},
},
},
},
},
},
},
},
curr: &appsv1.Deployment{},
expected: []string{"--config-text", "inline-override"},
},
{
name: "config flag as last arg without value - generates fresh config",
llmSvc: defaultSvc,
curr: &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "main",
Args: []string{"--config-text"},
},
},
},
},
},
},
expected: []string{"--config-text", schedulerConfigText(defaultSvc)},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewGomegaWithT(t)
result := preserveSchedulerConfig(tt.llmSvc, tt.curr)
g.Expect(result).To(Equal(tt.expected))
})
}
}
func TestFilterArgs(t *testing.T) {
tests := []struct {
name string
args []string
names map[string]bool
expectedFiltered []string
expectedExtracted map[string]string
}{
{
name: "no matching args",
args: []string{"--poolName", "test-pool", "--grpc-port", "9002"},
names: map[string]bool{"kv-cache-usage-percentage-metric": true},
expectedFiltered: []string{"--poolName", "test-pool", "--grpc-port", "9002"},
expectedExtracted: map[string]string{},
},
{
name: "remove flag with separate value",
args: []string{"--poolName", "test-pool", "--kv-cache-usage-percentage-metric", "vllm:kv_cache_usage_perc", "--grpc-port", "9002"},
names: map[string]bool{"kv-cache-usage-percentage-metric": true},
expectedFiltered: []string{"--poolName", "test-pool", "--grpc-port", "9002"},
expectedExtracted: map[string]string{"kv-cache-usage-percentage-metric": "vllm:kv_cache_usage_perc"},
},
{
name: "remove flag with equals value",
args: []string{"--poolName", "test-pool", "--kv-cache-usage-percentage-metric=vllm:kv_cache_usage_perc", "--grpc-port", "9002"},
names: map[string]bool{"kv-cache-usage-percentage-metric": true},
expectedFiltered: []string{"--poolName", "test-pool", "--grpc-port", "9002"},
expectedExtracted: map[string]string{"kv-cache-usage-percentage-metric": "vllm:kv_cache_usage_perc"},
},
{
name: "remove multiple flags",
args: []string{
"--poolName", "test-pool",
"--total-queued-requests-metric", "vllm:num_requests_waiting",
"--total-running-requests-metric", "vllm:num_requests_running",
"--grpc-port", "9002",
},
names: map[string]bool{
"total-queued-requests-metric": true,
"total-running-requests-metric": true,
},
expectedFiltered: []string{"--poolName", "test-pool", "--grpc-port", "9002"},
expectedExtracted: map[string]string{
"total-queued-requests-metric": "vllm:num_requests_waiting",
"total-running-requests-metric": "vllm:num_requests_running",
},
},
{
name: "flag at end with no value",
args: []string{"--poolName", "test-pool", "--lora-info-metric"},
names: map[string]bool{"lora-info-metric": true},
expectedFiltered: []string{"--poolName", "test-pool"},
expectedExtracted: map[string]string{"lora-info-metric": ""},
},
{
name: "flag followed by another flag",
args: []string{"--lora-info-metric", "--grpc-port", "9002"},
names: map[string]bool{"lora-info-metric": true},
expectedFiltered: []string{"--grpc-port", "9002"},
expectedExtracted: map[string]string{"lora-info-metric": ""},
},
{
name: "empty args",
args: []string{},
names: map[string]bool{"kv-cache-usage-percentage-metric": true},
expectedFiltered: nil,
expectedExtracted: map[string]string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewGomegaWithT(t)
filtered, extracted := filterArgs(tt.args, tt.names)
g.Expect(filtered).To(Equal(tt.expectedFiltered))
g.Expect(extracted).To(Equal(tt.expectedExtracted))
})
}
}
func TestWithRenamePlugin(t *testing.T) {
tests := []struct {
name string
configYAML string
oldType string
newType string
validate func(g Gomega, obj map[string]interface{})
}{
{
name: "renames matching plugin type",
configYAML: `
plugins:
- type: prefill-header-handler
- type: queue-scorer
`,
oldType: "prefill-header-handler",
newType: "disagg-headers-handler",
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
g.Expect(plugins[0].(map[string]interface{})["type"]).To(Equal("disagg-headers-handler"))
g.Expect(plugins[1].(map[string]interface{})["type"]).To(Equal("queue-scorer"))
},
},
{
name: "no match - no change",
configYAML: `
plugins:
- type: queue-scorer
`,
oldType: "prefill-header-handler",
newType: "disagg-headers-handler",
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
g.Expect(plugins[0].(map[string]interface{})["type"]).To(Equal("queue-scorer"))
},
},
{
name: "already renamed - idempotent",
configYAML: `
plugins:
- type: disagg-headers-handler
`,
oldType: "prefill-header-handler",
newType: "disagg-headers-handler",
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
g.Expect(plugins).To(HaveLen(1))
g.Expect(plugins[0].(map[string]interface{})["type"]).To(Equal("disagg-headers-handler"))
},
},
{
name: "renames pluginRef in schedulingProfiles",
configYAML: `
plugins:
- type: pd-profile-handler
schedulingProfiles:
- name: default
plugins:
- pluginRef: pd-profile-handler
- pluginRef: queue-scorer
`,
oldType: "pd-profile-handler",
newType: "disagg-profile-handler",
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
g.Expect(plugins[0].(map[string]interface{})["type"]).To(Equal("disagg-profile-handler"))
profiles := obj["schedulingProfiles"].([]interface{})
profilePlugins := profiles[0].(map[string]interface{})["plugins"].([]interface{})
g.Expect(profilePlugins[0].(map[string]interface{})["pluginRef"]).To(Equal("disagg-profile-handler"))
g.Expect(profilePlugins[1].(map[string]interface{})["pluginRef"]).To(Equal("queue-scorer"))
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewGomegaWithT(t)
var obj map[string]interface{}
g.Expect(yaml.Unmarshal([]byte(tt.configYAML), &obj)).To(Succeed())
u := unstructured.Unstructured{Object: obj}
fn := WithRenamePlugin(tt.oldType, tt.newType)
g.Expect(fn(context.Background(), &u)).To(Succeed())
tt.validate(g, u.Object)
})
}
}
// validateDeciderOrder checks the GIE loader ordering invariant: every plugin
// referenced in a handler's "deciders" map must appear earlier in the plugins
// list. The GIE loader registers plugins in list order, so a handler that
// references a decider declared later will fail with "plugin not found".
func validateDeciderOrder(g Gomega, obj map[string]interface{}) {
val, ok := obj["plugins"]
if !ok {
return
}
plugins, ok := val.([]interface{})
if !ok {
return
}
typeIndex := map[string]int{}
for i, p := range plugins {
pm, ok := p.(map[string]interface{})
if !ok {
continue
}
if t, ok := pm["type"].(string); ok {
typeIndex[t] = i
}
}
for i, p := range plugins {
pm, ok := p.(map[string]interface{})
if !ok {
continue
}
pluginType, _ := pm["type"].(string)
params, ok := pm["parameters"].(map[string]interface{})
if !ok {
continue
}
deciders, ok := params["deciders"].(map[string]interface{})
if !ok {
continue
}
for role, ref := range deciders {
refName, ok := ref.(string)
if !ok {
continue
}
refIdx, exists := typeIndex[refName]
if !exists {
// Decider not in the plugins list — may be externally
// declared (e.g. Path A where the user manages it).
continue
}
g.Expect(refIdx).To(BeNumerically("<", i),
fmt.Sprintf("%s at index %d references decider %q (role %s) at index %d — decider must appear before handler",
pluginType, i, refName, role, refIdx))
}
}
}
// validateDeciderOrderFromYAML is a convenience wrapper that unmarshals a
// config-text YAML string and then runs validateDeciderOrder on the result.
func validateDeciderOrderFromYAML(g Gomega, configText string) {
var obj map[string]interface{}
g.Expect(yaml.Unmarshal([]byte(configText), &obj)).To(Succeed())
validateDeciderOrder(g, obj)
}
func TestWithMigrateDisaggProfileParams(t *testing.T) {
tests := []struct {
name string
configYAML string
validate func(g Gomega, obj map[string]interface{})
}{
{
name: "migrates deciderPluginName to deciders map",
configYAML: `
plugins:
- type: disagg-profile-handler
parameters:
deciderPluginName: always-disagg-pd-decider
`,
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
params := plugins[0].(map[string]interface{})["parameters"].(map[string]interface{})
g.Expect(params).NotTo(HaveKey("deciderPluginName"))
deciders := params["deciders"].(map[string]interface{})
g.Expect(deciders).To(HaveKeyWithValue("prefill", "always-disagg-pd-decider"))
},
},
{
name: "already has deciders map - idempotent",
configYAML: `
plugins:
- type: disagg-profile-handler
parameters:
deciders:
prefill: always-disagg-pd-decider
`,
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
params := plugins[0].(map[string]interface{})["parameters"].(map[string]interface{})
deciders := params["deciders"].(map[string]interface{})
g.Expect(deciders).To(HaveKeyWithValue("prefill", "always-disagg-pd-decider"))
},
},
{
name: "no matching plugin - no-op",
configYAML: `
plugins:
- type: queue-scorer
`,
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
g.Expect(plugins).To(HaveLen(1))
g.Expect(plugins[0].(map[string]interface{})["type"]).To(Equal("queue-scorer"))
},
},
{
name: "works with old pd-profile-handler type name",
configYAML: `
plugins:
- type: pd-profile-handler
parameters:
deciderPluginName: always-disagg-pd-decider
`,
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
params := plugins[0].(map[string]interface{})["parameters"].(map[string]interface{})
g.Expect(params).NotTo(HaveKey("deciderPluginName"))
deciders := params["deciders"].(map[string]interface{})
g.Expect(deciders).To(HaveKeyWithValue("prefill", "always-disagg-pd-decider"))
},
},
{
name: "migrates threshold 0 to always-disagg-pd-decider",
configYAML: `
plugins:
- type: pd-profile-handler
parameters:
threshold: 0
- type: prefill-filter
`,
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
g.Expect(plugins).To(HaveLen(3))
g.Expect(plugins[0].(map[string]interface{})["type"]).To(Equal("always-disagg-pd-decider"))
params := plugins[1].(map[string]interface{})["parameters"].(map[string]interface{})
g.Expect(params).NotTo(HaveKey("threshold"))
deciders := params["deciders"].(map[string]interface{})
g.Expect(deciders).To(HaveKeyWithValue("prefill", "always-disagg-pd-decider"))
},
},
{
name: "migrates threshold 0 when decider plugin already exists",
configYAML: `
plugins:
- type: always-disagg-pd-decider
- type: pd-profile-handler
parameters:
threshold: 0
`,
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
g.Expect(plugins).To(HaveLen(2))
g.Expect(plugins[0].(map[string]interface{})["type"]).To(Equal("always-disagg-pd-decider"))
params := plugins[1].(map[string]interface{})["parameters"].(map[string]interface{})
g.Expect(params).NotTo(HaveKey("threshold"))
deciders := params["deciders"].(map[string]interface{})
g.Expect(deciders).To(HaveKeyWithValue("prefill", "always-disagg-pd-decider"))
},
},
{
name: "strips threshold when deciderPluginName also present",
configYAML: `
plugins:
- type: pd-profile-handler
parameters:
deciderPluginName: prefix-based-pd-decider
threshold: 0
`,
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
params := plugins[0].(map[string]interface{})["parameters"].(map[string]interface{})
g.Expect(params).NotTo(HaveKey("deciderPluginName"))
g.Expect(params).NotTo(HaveKey("threshold"))
deciders := params["deciders"].(map[string]interface{})
g.Expect(deciders).To(HaveKeyWithValue("prefill", "prefix-based-pd-decider"))
},
},
{
name: "migrates non-zero threshold 100 to prefix-based-pd-decider",
configYAML: `
plugins:
- type: pd-profile-handler
parameters:
threshold: 100
- type: prefill-filter
`,
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
g.Expect(plugins).To(HaveLen(3))
deciderPlugin := plugins[0].(map[string]interface{})
g.Expect(deciderPlugin["type"]).To(Equal("prefix-based-pd-decider"))
deciderParams := deciderPlugin["parameters"].(map[string]interface{})
g.Expect(deciderParams["nonCachedTokens"]).To(Equal(int64(25)))
params := plugins[1].(map[string]interface{})["parameters"].(map[string]interface{})
g.Expect(params).NotTo(HaveKey("threshold"))
deciders := params["deciders"].(map[string]interface{})
g.Expect(deciders).To(HaveKeyWithValue("prefill", "prefix-based-pd-decider"))
},
},
{
name: "migrates non-zero threshold 5 to prefix-based-pd-decider with ceil",
configYAML: `
plugins:
- type: pd-profile-handler
parameters:
threshold: 5
`,
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
deciderPlugin := plugins[0].(map[string]interface{})
g.Expect(deciderPlugin["type"]).To(Equal("prefix-based-pd-decider"))
deciderParams := deciderPlugin["parameters"].(map[string]interface{})
g.Expect(deciderParams["nonCachedTokens"]).To(Equal(int64(2)))
params := plugins[1].(map[string]interface{})["parameters"].(map[string]interface{})
g.Expect(params).NotTo(HaveKey("threshold"))
deciders := params["deciders"].(map[string]interface{})
g.Expect(deciders).To(HaveKeyWithValue("prefill", "prefix-based-pd-decider"))
},
},
{
name: "migrates non-zero threshold 1 to prefix-based-pd-decider minimum 1 token",
configYAML: `
plugins:
- type: pd-profile-handler
parameters:
threshold: 1
`,
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
deciderPlugin := plugins[0].(map[string]interface{})
g.Expect(deciderPlugin["type"]).To(Equal("prefix-based-pd-decider"))
deciderParams := deciderPlugin["parameters"].(map[string]interface{})
g.Expect(deciderParams["nonCachedTokens"]).To(Equal(int64(1)))
params := plugins[1].(map[string]interface{})["parameters"].(map[string]interface{})
g.Expect(params).NotTo(HaveKey("threshold"))
deciders := params["deciders"].(map[string]interface{})
g.Expect(deciders).To(HaveKeyWithValue("prefill", "prefix-based-pd-decider"))
},
},
{
name: "non-zero threshold idempotent when prefix-based-pd-decider already exists",
configYAML: `
plugins:
- type: prefix-based-pd-decider
parameters:
nonCachedTokens: 50
- type: pd-profile-handler
parameters:
threshold: 100
`,
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
g.Expect(plugins).To(HaveLen(2))
g.Expect(plugins[0].(map[string]interface{})["type"]).To(Equal("prefix-based-pd-decider"))
params := plugins[1].(map[string]interface{})["parameters"].(map[string]interface{})
g.Expect(params).NotTo(HaveKey("threshold"))
deciders := params["deciders"].(map[string]interface{})
g.Expect(deciders).To(HaveKeyWithValue("prefill", "prefix-based-pd-decider"))
},
},
{
name: "decider inserted before handler not after - load order matters",
configYAML: `
plugins:
- type: queue-scorer
- type: pd-profile-handler
parameters:
threshold: 100
- type: prefill-filter
`,
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
g.Expect(plugins).To(HaveLen(4))
g.Expect(plugins[0].(map[string]interface{})["type"]).To(Equal("queue-scorer"))
deciderPlugin := plugins[1].(map[string]interface{})
g.Expect(deciderPlugin["type"]).To(Equal("prefix-based-pd-decider"))
handlerPlugin := plugins[2].(map[string]interface{})
g.Expect(handlerPlugin["type"]).To(Equal("pd-profile-handler"))
g.Expect(plugins[3].(map[string]interface{})["type"]).To(Equal("prefill-filter"))
},
},
{
name: "always-disagg decider inserted before handler not after",
configYAML: `
plugins:
- type: queue-scorer
- type: pd-profile-handler
parameters:
threshold: 0
- type: prefill-filter
`,
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
g.Expect(plugins).To(HaveLen(4))
g.Expect(plugins[0].(map[string]interface{})["type"]).To(Equal("queue-scorer"))
g.Expect(plugins[1].(map[string]interface{})["type"]).To(Equal("always-disagg-pd-decider"))
handlerPlugin := plugins[2].(map[string]interface{})
g.Expect(handlerPlugin["type"]).To(Equal("pd-profile-handler"))
g.Expect(plugins[3].(map[string]interface{})["type"]).To(Equal("prefill-filter"))
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewGomegaWithT(t)
var obj map[string]interface{}
g.Expect(yaml.Unmarshal([]byte(tt.configYAML), &obj)).To(Succeed())
u := unstructured.Unstructured{Object: obj}
g.Expect(WithMigrateDisaggProfileParams(context.Background(), &u)).To(Succeed())
tt.validate(g, u.Object)
validateDeciderOrder(g, u.Object)
})
}
}
func TestThresholdToNonCachedTokens(t *testing.T) {
tests := []struct {
name string
val interface{}
expected int64
}{
{name: "int64 100", val: int64(100), expected: 25},
{name: "int64 5", val: int64(5), expected: 2},
{name: "int64 1", val: int64(1), expected: 1},
{name: "int64 3", val: int64(3), expected: 1},
{name: "float64 100.0", val: float64(100.0), expected: 25},
{name: "float64 0.5", val: float64(0.5), expected: 1},
{name: "float64 7.0", val: float64(7.0), expected: 2},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewGomegaWithT(t)
g.Expect(thresholdToNonCachedTokens(tt.val)).To(Equal(tt.expected))
})
}
}
func TestWithMigrateDisaggProfileHandlerThreshold(t *testing.T) {
tests := []struct {
name string
configYAML string
validate func(g Gomega, obj map[string]interface{})
}{
{
name: "migrates non-zero threshold with rename and prefix-based-pd-decider",
configYAML: `
plugins:
- type: pd-profile-handler
parameters:
threshold: 0.5
`,
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
g.Expect(plugins).To(HaveLen(2))
deciderPlugin := plugins[0].(map[string]interface{})
g.Expect(deciderPlugin["type"]).To(Equal("prefix-based-pd-decider"))
deciderParams := deciderPlugin["parameters"].(map[string]interface{})
g.Expect(deciderParams["nonCachedTokens"]).To(Equal(int64(1)))
pluginMap := plugins[1].(map[string]interface{})
g.Expect(pluginMap["type"]).To(Equal("disagg-profile-handler"))
params := pluginMap["parameters"].(map[string]interface{})
g.Expect(params).NotTo(HaveKey("threshold"))
deciders := params["deciders"].(map[string]interface{})
g.Expect(deciders).To(HaveKeyWithValue("prefill", "prefix-based-pd-decider"))
},
},
{
name: "handles threshold 0 without deciderPluginName",
configYAML: `
plugins:
- type: pd-profile-handler
parameters:
threshold: 0
- type: prefill-filter
`,
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
g.Expect(plugins).To(HaveLen(3))
g.Expect(plugins[0].(map[string]interface{})["type"]).To(Equal("always-disagg-pd-decider"))
pluginMap := plugins[1].(map[string]interface{})
g.Expect(pluginMap["type"]).To(Equal("disagg-profile-handler"))
params := pluginMap["parameters"].(map[string]interface{})
g.Expect(params).NotTo(HaveKey("threshold"))
deciders := params["deciders"].(map[string]interface{})
g.Expect(deciders).To(HaveKeyWithValue("prefill", "always-disagg-pd-decider"))
},
},
{
name: "handles threshold 0 when decider plugin already present",
configYAML: `
plugins:
- type: always-disagg-pd-decider
- type: pd-profile-handler
parameters:
threshold: 0
`,
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
g.Expect(plugins).To(HaveLen(2))
g.Expect(plugins[0].(map[string]interface{})["type"]).To(Equal("always-disagg-pd-decider"))
pluginMap := plugins[1].(map[string]interface{})
g.Expect(pluginMap["type"]).To(Equal("disagg-profile-handler"))
params := pluginMap["parameters"].(map[string]interface{})
g.Expect(params).NotTo(HaveKey("threshold"))
deciders := params["deciders"].(map[string]interface{})
g.Expect(deciders).To(HaveKeyWithValue("prefill", "always-disagg-pd-decider"))
},
},
{
name: "migrates non-zero threshold with deciderPluginName - uses specified decider",
configYAML: `
plugins:
- type: pd-profile-handler
parameters:
deciderPluginName: prefix-based-pd-decider
threshold: 0.5
`,
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
pluginMap := plugins[0].(map[string]interface{})
g.Expect(pluginMap["type"]).To(Equal("disagg-profile-handler"))
params := pluginMap["parameters"].(map[string]interface{})
g.Expect(params).NotTo(HaveKey("deciderPluginName"))
g.Expect(params).NotTo(HaveKey("threshold"))
deciders := params["deciders"].(map[string]interface{})
g.Expect(deciders).To(HaveKeyWithValue("prefill", "prefix-based-pd-decider"))
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewGomegaWithT(t)
var obj map[string]interface{}
g.Expect(yaml.Unmarshal([]byte(tt.configYAML), &obj)).To(Succeed())
u := unstructured.Unstructured{Object: obj}
g.Expect(withMigrateDisaggProfileHandler(context.Background(), &u)).To(Succeed())
tt.validate(g, u.Object)
validateDeciderOrder(g, u.Object)
})
}
}
func TestWithRemoveHashBlockSize(t *testing.T) {
tests := []struct {
name string
configYAML string
validate func(g Gomega, obj map[string]interface{})
}{
{
name: "removes hashBlockSize",
configYAML: `
plugins:
- type: prefix-cache-scorer
parameters:
hashBlockSize: 64
blockSizeTokens: 16
`,
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
params := plugins[0].(map[string]interface{})["parameters"].(map[string]interface{})
g.Expect(params).NotTo(HaveKey("hashBlockSize"))
g.Expect(params).To(HaveKey("blockSizeTokens"))
},
},
{
name: "no hashBlockSize - no-op",
configYAML: `
plugins:
- type: prefix-cache-scorer
parameters:
blockSizeTokens: 16
`,
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
params := plugins[0].(map[string]interface{})["parameters"].(map[string]interface{})
g.Expect(params).To(HaveKey("blockSizeTokens"))
},
},
{
name: "removes from multiple plugins",
configYAML: `
plugins:
- type: prefix-cache-scorer
parameters:
hashBlockSize: 64
- type: precise-prefix-cache-scorer
parameters:
hashBlockSize: 32
`,
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
for _, p := range plugins {
params := p.(map[string]interface{})["parameters"].(map[string]interface{})
g.Expect(params).NotTo(HaveKey("hashBlockSize"))
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewGomegaWithT(t)
var obj map[string]interface{}
g.Expect(yaml.Unmarshal([]byte(tt.configYAML), &obj)).To(Succeed())
u := unstructured.Unstructured{Object: obj}
g.Expect(WithRemoveHashBlockSize(context.Background(), &u)).To(Succeed())
tt.validate(g, u.Object)
})
}
}
func TestWithRemovePrefixCacheScorerParametersV09(t *testing.T) {
tests := []struct {
name string
configYAML string
validate func(g Gomega, obj map[string]interface{})
}{
{
name: "removes all parameters except prefixMatchInfoProducerName",
configYAML: `
plugins:
- type: prefix-cache-scorer
parameters:
blockSizeTokens: 16
hashBlockSize: 64
prefixMatchInfoProducerName: my-producer
`,
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
params := plugins[0].(map[string]interface{})["parameters"].(map[string]interface{})
g.Expect(params).To(HaveKey("prefixMatchInfoProducerName"))
g.Expect(params).NotTo(HaveKey("blockSizeTokens"))
g.Expect(params).NotTo(HaveKey("hashBlockSize"))
},
},
{
name: "removes parameters entirely when no prefixMatchInfoProducerName",
configYAML: `
plugins:
- type: prefix-cache-scorer
parameters:
blockSizeTokens: 16
`,
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
g.Expect(plugins[0].(map[string]interface{})).NotTo(HaveKey("parameters"))
},
},
{
name: "no parameters - no-op",
configYAML: `
plugins:
- type: prefix-cache-scorer
`,
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
g.Expect(plugins[0].(map[string]interface{})).NotTo(HaveKey("parameters"))
},
},
{
name: "does not affect other plugins",
configYAML: `
plugins:
- type: prefix-cache-scorer
parameters:
blockSizeTokens: 16
- type: queue-scorer
parameters:
someParam: value
`,
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
g.Expect(plugins[0].(map[string]interface{})).NotTo(HaveKey("parameters"))
queueParams := plugins[1].(map[string]interface{})["parameters"].(map[string]interface{})
g.Expect(queueParams).To(HaveKeyWithValue("someParam", "value"))
},
},
{
name: "no plugins - no-op",
configYAML: `
schedulingProfiles:
- name: default
`,
validate: func(g Gomega, obj map[string]interface{}) {
g.Expect(obj).NotTo(HaveKey("plugins"))
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewGomegaWithT(t)
var obj map[string]interface{}
g.Expect(yaml.Unmarshal([]byte(tt.configYAML), &obj)).To(Succeed())
u := unstructured.Unstructured{Object: obj}
g.Expect(withRemovePrefixCacheScorerParametersV09(context.Background(), &u)).To(Succeed())
tt.validate(g, u.Object)
})
}
}
func TestWithCoreMetricsExtractorPlugin(t *testing.T) {
tests := []struct {
name string
configYAML string
extracted map[string]string
validate func(g Gomega, obj map[string]interface{})
}{
{
name: "injects plugin with extracted values",
configYAML: `
plugins:
- type: queue-scorer
`,
extracted: map[string]string{
"total-queued-requests-metric": "vllm:num_requests_waiting",
"total-running-requests-metric": "vllm:num_requests_running",
"kv-cache-usage-percentage-metric": "vllm:kv_cache_usage_perc",
"lora-info-metric": "vllm:lora_requests_info",
"cache-info-metric": "vllm:cache_config_info",
},
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
g.Expect(plugins).To(HaveLen(2))
pluginMap := plugins[1].(map[string]interface{})
g.Expect(pluginMap["type"]).To(Equal(coreMetricsExtractorPlugin))
params := pluginMap["parameters"].(map[string]interface{})
g.Expect(params["defaultEngine"]).To(Equal("vllm"))
engineConfigs := params["engineConfigs"].([]interface{})
engine := engineConfigs[0].(map[string]interface{})
g.Expect(engine["queuedRequestsSpec"]).To(Equal("vllm:num_requests_waiting"))
g.Expect(engine["runningRequestsSpec"]).To(Equal("vllm:num_requests_running"))
g.Expect(engine["kvUsageSpec"]).To(Equal("vllm:kv_cache_usage_perc"))
},
},
{
name: "skips when plugin already exists",
configYAML: `
plugins:
- type: model-server-protocol-metrics
parameters:
defaultEngine: vllm
`,
extracted: map[string]string{
"kv-cache-usage-percentage-metric": "vllm:kv_cache_usage_perc",
},
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
g.Expect(plugins).To(HaveLen(1))
},
},
{
name: "skips when renamed plugin already exists",
configYAML: `
plugins:
- type: core-metrics-extractor
parameters:
defaultEngine: vllm
`,
extracted: map[string]string{
"kv-cache-usage-percentage-metric": "vllm:kv_cache_usage_perc",
},
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
g.Expect(plugins).To(HaveLen(1))
},
},
{
name: "skips when no values extracted",
configYAML: `
plugins:
- type: queue-scorer
`,
extracted: map[string]string{},
validate: func(g Gomega, obj map[string]interface{}) {
plugins := obj["plugins"].([]interface{})
g.Expect(plugins).To(HaveLen(1))
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewGomegaWithT(t)
var obj map[string]interface{}
g.Expect(yaml.Unmarshal([]byte(tt.configYAML), &obj)).To(Succeed())
u := unstructured.Unstructured{Object: obj}
fn := withCoreMetricsExtractorPlugin(tt.extracted)
g.Expect(fn(context.Background(), &u)).To(Succeed())
tt.validate(g, u.Object)
})
}
}
func TestSchedulerTransform(t *testing.T) {
oldConfigYAML := `apiVersion: inference.networking.x-k8s.io/v1alpha1
kind: EndpointPickerConfig
plugins:
- type: prefill-header-handler
- type: pd-profile-handler
parameters:
deciderPluginName: always-disagg-pd-decider
- type: prefix-cache-scorer
parameters:
hashBlockSize: 64
blockSizeTokens: 16
`
tests := []struct {
name string
version string
validateConfig func(g Gomega, configText string)
}{
{
name: "applies all migrations for v0.7.0",
version: "0.7.0",
validateConfig: func(g Gomega, configText string) {
g.Expect(configText).To(ContainSubstring("disagg-headers-handler"))
g.Expect(configText).NotTo(ContainSubstring("prefill-header-handler"))
g.Expect(configText).To(ContainSubstring("disagg-profile-handler"))
g.Expect(configText).NotTo(ContainSubstring("pd-profile-handler"))
g.Expect(configText).To(ContainSubstring("prefill: always-disagg-pd-decider"))
g.Expect(configText).NotTo(ContainSubstring("deciderPluginName"))
g.Expect(configText).NotTo(ContainSubstring("hashBlockSize"))
g.Expect(configText).To(ContainSubstring("blockSizeTokens"))
},
},
{
name: "skips all migrations for v0.6.0",
version: "0.6.0",
validateConfig: func(g Gomega, configText string) {
g.Expect(configText).To(ContainSubstring("prefill-header-handler"))
g.Expect(configText).NotTo(ContainSubstring("disagg-headers-handler"))
g.Expect(configText).To(ContainSubstring("pd-profile-handler"))
g.Expect(configText).NotTo(ContainSubstring("disagg-profile-handler"))
g.Expect(configText).To(ContainSubstring("deciderPluginName"))
g.Expect(configText).To(ContainSubstring("hashBlockSize"))
},
},
{
name: "skips all migrations when no version annotation",
version: "",
validateConfig: func(g Gomega, configText string) {
g.Expect(configText).To(ContainSubstring("prefill-header-handler"))
g.Expect(configText).To(ContainSubstring("pd-profile-handler"))
g.Expect(configText).To(ContainSubstring("deciderPluginName"))
g.Expect(configText).To(ContainSubstring("hashBlockSize"))
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewGomegaWithT(t)
d := &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"app.kubernetes.io/version": tt.version,
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "main", Args: []string{"--config-text", oldConfigYAML}},
},
},
},
},
}
g.Expect(schedulerTransform(context.Background(), d)).To(Succeed())
configText := d.Spec.Template.Spec.Containers[0].Args[1]
tt.validateConfig(g, configText)
})
}
}
func TestSchedulerTransformThreshold(t *testing.T) {
tests := []struct {
name string
configYAML string
version string
validateConfig func(g Gomega, configText string)
}{
{
name: "migrates non-zero threshold in full transform",
configYAML: `apiVersion: inference.networking.x-k8s.io/v1alpha1
kind: EndpointPickerConfig
plugins:
- type: prefill-header-handler
- type: pd-profile-handler
parameters:
threshold: 0.5
- type: prefix-cache-scorer
parameters:
hashBlockSize: 64
blockSizeTokens: 16
`,
version: "0.7.0",
validateConfig: func(g Gomega, configText string) {
g.Expect(configText).To(ContainSubstring("disagg-headers-handler"))
g.Expect(configText).NotTo(ContainSubstring("prefill-header-handler"))
g.Expect(configText).To(ContainSubstring("disagg-profile-handler"))
g.Expect(configText).NotTo(ContainSubstring("pd-profile-handler"))
g.Expect(configText).To(ContainSubstring("prefix-based-pd-decider"))
g.Expect(configText).To(ContainSubstring("nonCachedTokens"))
g.Expect(configText).NotTo(ContainSubstring("threshold"))
g.Expect(configText).NotTo(ContainSubstring("hashBlockSize"))
g.Expect(configText).To(ContainSubstring("blockSizeTokens"))
},
},
{
name: "migrates non-zero threshold with deciderPluginName in full transform",
configYAML: `apiVersion: inference.networking.x-k8s.io/v1alpha1
kind: EndpointPickerConfig
plugins:
- type: prefill-header-handler
- type: pd-profile-handler
parameters:
deciderPluginName: prefix-based-pd-decider
threshold: 0.5
- type: prefix-cache-scorer
parameters:
hashBlockSize: 64
`,
version: "0.7.0",
validateConfig: func(g Gomega, configText string) {
g.Expect(configText).To(ContainSubstring("disagg-headers-handler"))
g.Expect(configText).NotTo(ContainSubstring("prefill-header-handler"))
g.Expect(configText).To(ContainSubstring("disagg-profile-handler"))
g.Expect(configText).NotTo(ContainSubstring("pd-profile-handler"))
g.Expect(configText).NotTo(ContainSubstring("deciderPluginName"))
g.Expect(configText).NotTo(ContainSubstring("threshold"))
g.Expect(configText).To(ContainSubstring("prefill: prefix-based-pd-decider"))
g.Expect(configText).NotTo(ContainSubstring("hashBlockSize"))
},
},
{
name: "migrates threshold 0 in full transform",
configYAML: `apiVersion: inference.networking.x-k8s.io/v1alpha1
kind: EndpointPickerConfig
plugins:
- type: prefill-header-handler
- type: pd-profile-handler
parameters:
threshold: 0
- type: prefix-cache-scorer
parameters:
hashBlockSize: 64
blockSizeTokens: 16
`,
version: "0.7.0",
validateConfig: func(g Gomega, configText string) {
g.Expect(configText).To(ContainSubstring("disagg-headers-handler"))
g.Expect(configText).NotTo(ContainSubstring("prefill-header-handler"))
g.Expect(configText).To(ContainSubstring("disagg-profile-handler"))
g.Expect(configText).NotTo(ContainSubstring("pd-profile-handler"))
g.Expect(configText).To(ContainSubstring("prefill: always-disagg-pd-decider"))
g.Expect(configText).NotTo(ContainSubstring("threshold"))
g.Expect(configText).To(ContainSubstring("always-disagg-pd-decider"))
g.Expect(configText).NotTo(ContainSubstring("hashBlockSize"))
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewGomegaWithT(t)
d := &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"app.kubernetes.io/version": tt.version,
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "main", Args: []string{"--config-text", tt.configYAML}},
},
},
},
},
}
g.Expect(schedulerTransform(context.Background(), d)).To(Succeed())
configText := d.Spec.Template.Spec.Containers[0].Args[1]
tt.validateConfig(g, configText)
validateDeciderOrderFromYAML(g, configText)
})
}
}
func TestFullMigrationPipeline(t *testing.T) {
// Realistic old v0.6 config with all deprecated features.
oldConfigYAML := `apiVersion: inference.networking.x-k8s.io/v1alpha1
kind: EndpointPickerConfig
plugins:
- type: prefill-header-handler
- type: prefill-filter
- type: decode-filter
- type: queue-scorer
- type: prefix-cache-scorer
parameters:
hashBlockSize: 64
blockSizeTokens: 16
indexerConfig:
tokenProcessorConfig:
batchSize: 1024
- type: max-score-picker
- type: always-disagg-pd-decider
- type: pd-profile-handler
parameters:
deciderPluginName: always-disagg-pd-decider
schedulingProfiles:
- name: prefill
plugins:
- pluginRef: prefill-filter
- pluginRef: queue-scorer
- pluginRef: max-score-picker
- name: decode
plugins:
- pluginRef: decode-filter
- pluginRef: prefix-cache-scorer
- pluginRef: queue-scorer
- pluginRef: max-score-picker
`
tests := []struct {
name string
version string
extraArgs []string
validateConfig func(g Gomega, configText string)
validateArgs func(g Gomega, args []string)
}{
{
name: "full migration of old v0.6 config to v0.7",
version: "0.7.0",
extraArgs: []string{
"--total-queued-requests-metric", "vllm:num_requests_waiting",
"--total-running-requests-metric", "vllm:num_requests_running",
"--kv-cache-usage-percentage-metric", "vllm:kv_cache_usage_perc",
"--grpc-port", "9002",
},
validateConfig: func(g Gomega, configText string) {
// Plugin renames applied
g.Expect(configText).To(ContainSubstring("disagg-headers-handler"))
g.Expect(configText).NotTo(ContainSubstring("prefill-header-handler"))
g.Expect(configText).To(ContainSubstring("disagg-profile-handler"))
g.Expect(configText).NotTo(ContainSubstring("pd-profile-handler"))
// Parameter restructuring applied
g.Expect(configText).To(ContainSubstring("prefill: always-disagg-pd-decider"))
g.Expect(configText).NotTo(ContainSubstring("deciderPluginName"))
// Deprecated field removed
g.Expect(configText).NotTo(ContainSubstring("hashBlockSize"))
// Pre-existing migrations applied
g.Expect(configText).To(ContainSubstring("blockSizeTokens"))
// CLI flag values moved to model-server-protocol-metrics plugin
g.Expect(configText).To(ContainSubstring("model-server-protocol-metrics"))
g.Expect(configText).To(ContainSubstring("vllm:num_requests_waiting"))
g.Expect(configText).To(ContainSubstring("vllm:num_requests_running"))
g.Expect(configText).To(ContainSubstring("vllm:kv_cache_usage_perc"))
// Unchanged plugins preserved
g.Expect(configText).To(ContainSubstring("prefill-filter"))
g.Expect(configText).To(ContainSubstring("decode-filter"))
g.Expect(configText).To(ContainSubstring("queue-scorer"))
g.Expect(configText).To(ContainSubstring("max-score-picker"))
},
validateArgs: func(g Gomega, args []string) {
// Metric flags removed
for _, a := range args {
g.Expect(a).NotTo(ContainSubstring("total-queued-requests-metric"))
g.Expect(a).NotTo(ContainSubstring("total-running-requests-metric"))
g.Expect(a).NotTo(ContainSubstring("kv-cache-usage-percentage-metric"))
}
// Non-metric flags preserved
g.Expect(args).To(ContainElement("--grpc-port"))
g.Expect(args).To(ContainElement("9002"))
},
},
{
name: "v0.8.0 renames model-server-protocol-metrics to core-metrics-extractor",
version: "0.8.0",
extraArgs: []string{
"--total-queued-requests-metric", "vllm:num_requests_waiting",
"--total-running-requests-metric", "vllm:num_requests_running",
"--kv-cache-usage-percentage-metric", "vllm:kv_cache_usage_perc",
"--grpc-port", "9002",
},
validateConfig: func(g Gomega, configText string) {
// v0.8.0 plugin name used
g.Expect(configText).To(ContainSubstring("core-metrics-extractor"))
g.Expect(configText).NotTo(ContainSubstring("model-server-protocol-metrics"))
// Metric values present
g.Expect(configText).To(ContainSubstring("vllm:num_requests_waiting"))
g.Expect(configText).To(ContainSubstring("vllm:num_requests_running"))
g.Expect(configText).To(ContainSubstring("vllm:kv_cache_usage_perc"))
// v0.7 renames also applied
g.Expect(configText).To(ContainSubstring("disagg-headers-handler"))
g.Expect(configText).NotTo(ContainSubstring("prefill-header-handler"))
},
validateArgs: func(g Gomega, args []string) {
for _, a := range args {
g.Expect(a).NotTo(ContainSubstring("total-queued-requests-metric"))
g.Expect(a).NotTo(ContainSubstring("total-running-requests-metric"))
g.Expect(a).NotTo(ContainSubstring("kv-cache-usage-percentage-metric"))
}
g.Expect(args).To(ContainElement("--grpc-port"))
g.Expect(args).To(ContainElement("9002"))
},
},
{
name: "old config left untouched for v0.6.0",
version: "0.6.0",
extraArgs: []string{
"--total-queued-requests-metric", "vllm:num_requests_waiting",
},
validateConfig: func(g Gomega, configText string) {
// v0.7 renames NOT applied
g.Expect(configText).To(ContainSubstring("prefill-header-handler"))
g.Expect(configText).To(ContainSubstring("pd-profile-handler"))
g.Expect(configText).To(ContainSubstring("deciderPluginName"))
g.Expect(configText).To(ContainSubstring("hashBlockSize"))
// No model-server-protocol-metrics injected
g.Expect(configText).NotTo(ContainSubstring("model-server-protocol-metrics"))
// Pre-existing migrations still applied (unconditional)
g.Expect(configText).To(ContainSubstring("blockSizeTokens"))
},
validateArgs: func(g Gomega, args []string) {
// CLI flags preserved for v0.6 binary
g.Expect(args).To(ContainElement("--total-queued-requests-metric"))
g.Expect(args).To(ContainElement("vllm:num_requests_waiting"))
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewGomegaWithT(t)
args := append([]string{"--config-text", oldConfigYAML}, tt.extraArgs...)
d := &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"app.kubernetes.io/version": tt.version,
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "main", Args: args},
},
},
},
},
}
ctx := context.Background()
// Stage 1: unconditional pre-v0.7 migrations
g.Expect(mutateSchedulerConfig(ctx, d,
WithMigrateTokenProcessorConfig,
WithMigrateBlockSizeToBlockSizeTokens,
)).To(Succeed())
// Stage 2: version-gated v0.7 migrations (single pass)
g.Expect(schedulerTransform(ctx, d)).To(Succeed())
resultArgs := d.Spec.Template.Spec.Containers[0].Args
for i, a := range resultArgs {
if a == "--config-text" && i+1 < len(resultArgs) {
tt.validateConfig(g, resultArgs[i+1])
validateDeciderOrderFromYAML(g, resultArgs[i+1])
}
}
tt.validateArgs(g, resultArgs)
})
}
}
func TestFullMigrationPipelineNonZeroThreshold(t *testing.T) {
oldConfigYAML := `apiVersion: inference.networking.x-k8s.io/v1alpha1
kind: EndpointPickerConfig
plugins:
- type: prefill-header-handler
- type: prefill-filter
- type: decode-filter
- type: queue-scorer
- type: prefix-cache-scorer
parameters:
hashBlockSize: 64
blockSizeTokens: 16
- type: max-score-picker
- type: pd-profile-handler
parameters:
threshold: 100
schedulingProfiles:
- name: prefill
plugins:
- pluginRef: prefill-filter
- pluginRef: queue-scorer
- pluginRef: max-score-picker
- name: decode
plugins:
- pluginRef: decode-filter
- pluginRef: prefix-cache-scorer
- pluginRef: queue-scorer
- pluginRef: max-score-picker
`
g := NewGomegaWithT(t)
d := &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"app.kubernetes.io/version": "0.7.0",
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "main", Args: []string{"--config-text", oldConfigYAML}},
},
},
},
},
}
ctx := context.Background()
g.Expect(schedulerTransform(ctx, d)).To(Succeed())
configText := d.Spec.Template.Spec.Containers[0].Args[1]
// Plugin renames applied
g.Expect(configText).To(ContainSubstring("disagg-headers-handler"))
g.Expect(configText).NotTo(ContainSubstring("prefill-header-handler"))
g.Expect(configText).To(ContainSubstring("disagg-profile-handler"))
g.Expect(configText).NotTo(ContainSubstring("pd-profile-handler"))
// Non-zero threshold migrated to prefix-based-pd-decider
g.Expect(configText).NotTo(ContainSubstring("threshold"))
g.Expect(configText).To(ContainSubstring("prefix-based-pd-decider"))
g.Expect(configText).To(ContainSubstring("nonCachedTokens"))
g.Expect(configText).To(ContainSubstring("prefill: prefix-based-pd-decider"))
// Deprecated field removed
g.Expect(configText).NotTo(ContainSubstring("hashBlockSize"))
g.Expect(configText).To(ContainSubstring("blockSizeTokens"))
// Unchanged plugins preserved
g.Expect(configText).To(ContainSubstring("prefill-filter"))
g.Expect(configText).To(ContainSubstring("decode-filter"))
g.Expect(configText).To(ContainSubstring("queue-scorer"))
g.Expect(configText).To(ContainSubstring("max-score-picker"))
// Decider ordering invariant
validateDeciderOrderFromYAML(g, configText)
}
func TestExtractDeprecatedMetricFlags(t *testing.T) {
tests := []struct {
name string
args []string
expectedFiltered []string
expectedExtracted map[string]string
}{
{
name: "extracts all metric flags",
args: []string{
"--config-text", "someyaml",
"--total-queued-requests-metric", "vllm:num_requests_waiting",
"--kv-cache-usage-percentage-metric", "vllm:kv_cache_usage_perc",
"--grpc-port", "9002",
},
expectedFiltered: []string{"--config-text", "someyaml", "--grpc-port", "9002"},
expectedExtracted: map[string]string{
"total-queued-requests-metric": "vllm:num_requests_waiting",
"kv-cache-usage-percentage-metric": "vllm:kv_cache_usage_perc",
},
},
{
name: "no metric flags - returns nil",
args: []string{"--config-text", "someyaml", "--grpc-port", "9002"},
expectedFiltered: []string{"--config-text", "someyaml", "--grpc-port", "9002"},
expectedExtracted: nil,
},
{
name: "extracts all five deprecated flags",
args: []string{
"--total-queued-requests-metric", "vllm:num_requests_waiting",
"--total-running-requests-metric", "vllm:num_requests_running",
"--kv-cache-usage-percentage-metric", "vllm:kv_cache_usage_perc",
"--lora-info-metric", "vllm:lora_requests_info",
"--cache-info-metric", "vllm:cache_config_info",
},
expectedFiltered: nil,
expectedExtracted: map[string]string{
"total-queued-requests-metric": "vllm:num_requests_waiting",
"total-running-requests-metric": "vllm:num_requests_running",
"kv-cache-usage-percentage-metric": "vllm:kv_cache_usage_perc",
"lora-info-metric": "vllm:lora_requests_info",
"cache-info-metric": "vllm:cache_config_info",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewGomegaWithT(t)
d := &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "main", Args: tt.args},
},
},
},
},
}
extracted := extractDeprecatedMetricFlags(d)
g.Expect(d.Spec.Template.Spec.Containers[0].Args).To(Equal(tt.expectedFiltered))
g.Expect(extracted).To(Equal(tt.expectedExtracted))
})
}
}
func TestSchedulerTransformGatesMetricFlagExtraction(t *testing.T) {
configYAML := `apiVersion: inference.networking.x-k8s.io/v1alpha1
kind: EndpointPickerConfig
plugins:
- type: queue-scorer
`
tests := []struct {
name string
args []string
expectErr bool
errSubstring string
validateArgs func(g Gomega, args []string)
validateConfig func(g Gomega, args []string)
}{
{
name: "config-file with deprecated flags returns error",
args: []string{
"--config-file", "/etc/scheduler/config.yaml",
"--total-queued-requests-metric", "vllm:num_requests_waiting",
"--kv-cache-usage-percentage-metric", "vllm:kv_cache_usage_perc",
},
expectErr: true,
errSubstring: "no inline --config-text",
},
{
name: "no config flag with deprecated flags returns error",
args: []string{
"--grpc-port", "9002",
"--total-queued-requests-metric", "vllm:num_requests_waiting",
},
expectErr: true,
errSubstring: "no inline --config-text",
},
{
name: "config-file without deprecated flags succeeds",
args: []string{
"--config-file", "/etc/scheduler/config.yaml",
"--grpc-port", "9002",
},
expectErr: false,
validateArgs: func(g Gomega, args []string) {
g.Expect(args).To(ContainElement("--config-file"))
g.Expect(args).To(ContainElement("/etc/scheduler/config.yaml"))
g.Expect(args).To(ContainElement("--grpc-port"))
},
},
{
name: "configText camelCase with deprecated flags succeeds",
args: []string{
"--configText", configYAML,
"--total-queued-requests-metric", "vllm:num_requests_waiting",
"--kv-cache-usage-percentage-metric", "vllm:kv_cache_usage_perc",
"--grpc-port", "9002",
},
expectErr: false,
validateArgs: func(g Gomega, args []string) {
for _, a := range args {
g.Expect(a).NotTo(ContainSubstring("total-queued-requests-metric"))
g.Expect(a).NotTo(ContainSubstring("kv-cache-usage-percentage-metric"))
}
g.Expect(args).To(ContainElement("--grpc-port"))
},
validateConfig: func(g Gomega, args []string) {
for i, a := range args {
if a == "--configText" && i+1 < len(args) {
g.Expect(args[i+1]).To(ContainSubstring("model-server-protocol-metrics"))
g.Expect(args[i+1]).To(ContainSubstring("vllm:num_requests_waiting"))
g.Expect(args[i+1]).To(ContainSubstring("vllm:kv_cache_usage_perc"))
return
}
}
g.Expect(true).To(BeFalse(), "expected --configText arg not found")
},
},
{
name: "config-text with deprecated flags succeeds (existing behavior)",
args: []string{
"--config-text", configYAML,
"--total-queued-requests-metric", "vllm:num_requests_waiting",
"--grpc-port", "9002",
},
expectErr: false,
validateArgs: func(g Gomega, args []string) {
for _, a := range args {
g.Expect(a).NotTo(ContainSubstring("total-queued-requests-metric"))
}
g.Expect(args).To(ContainElement("--grpc-port"))
},
validateConfig: func(g Gomega, args []string) {
for i, a := range args {
if a == "--config-text" && i+1 < len(args) {
g.Expect(args[i+1]).To(ContainSubstring("model-server-protocol-metrics"))
g.Expect(args[i+1]).To(ContainSubstring("vllm:num_requests_waiting"))
return
}
}
g.Expect(true).To(BeFalse(), "expected --config-text arg not found")
},
},
{
name: "single-dash -config-text with deprecated flags succeeds",
args: []string{
"-config-text", configYAML,
"--total-queued-requests-metric", "vllm:num_requests_waiting",
"--grpc-port", "9002",
},
expectErr: false,
validateArgs: func(g Gomega, args []string) {
for _, a := range args {
g.Expect(a).NotTo(ContainSubstring("total-queued-requests-metric"))
}
g.Expect(args).To(ContainElement("--grpc-port"))
},
validateConfig: func(g Gomega, args []string) {
for i, a := range args {
if a == "-config-text" && i+1 < len(args) {
g.Expect(args[i+1]).To(ContainSubstring("model-server-protocol-metrics"))
g.Expect(args[i+1]).To(ContainSubstring("vllm:num_requests_waiting"))
return
}
}
g.Expect(true).To(BeFalse(), "expected -config-text arg not found")
},
},
{
name: "single-dash -configText with deprecated flags succeeds",
args: []string{
"-configText", configYAML,
"--total-queued-requests-metric", "vllm:num_requests_waiting",
"--grpc-port", "9002",
},
expectErr: false,
validateArgs: func(g Gomega, args []string) {
for _, a := range args {
g.Expect(a).NotTo(ContainSubstring("total-queued-requests-metric"))
}
g.Expect(args).To(ContainElement("--grpc-port"))
},
validateConfig: func(g Gomega, args []string) {
for i, a := range args {
if a == "-configText" && i+1 < len(args) {
g.Expect(args[i+1]).To(ContainSubstring("model-server-protocol-metrics"))
g.Expect(args[i+1]).To(ContainSubstring("vllm:num_requests_waiting"))
return
}
}
g.Expect(true).To(BeFalse(), "expected -configText arg not found")
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewGomegaWithT(t)
d := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-scheduler",
Namespace: "test-ns",
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"app.kubernetes.io/version": "0.7.0",
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "main", Args: tt.args},
},
},
},
},
}
err := schedulerTransform(context.Background(), d)
if tt.expectErr {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(tt.errSubstring))
return
}
g.Expect(err).NotTo(HaveOccurred())
resultArgs := d.Spec.Template.Spec.Containers[0].Args
if tt.validateArgs != nil {
tt.validateArgs(g, resultArgs)
}
if tt.validateConfig != nil {
tt.validateConfig(g, resultArgs)
}
})
}
}