Skip to content

Commit 15d8aa3

Browse files
authored
Add score tests for resource fungibility plugin (#23)
1 parent ed9168d commit 15d8aa3

File tree

1 file changed

+208
-0
lines changed

1 file changed

+208
-0
lines changed

pkg/plugins/resource_fungibility/resource_fungibility_test.go

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"k8s.io/client-go/informers"
3131
clientsetfake "k8s.io/client-go/kubernetes/fake"
3232
restclient "k8s.io/client-go/rest"
33+
"k8s.io/kubernetes/pkg/scheduler/backend/cache"
3334
"k8s.io/kubernetes/pkg/scheduler/framework"
3435
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/defaultbinder"
3536
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/queuesort"
@@ -361,6 +362,213 @@ func TestResourceFungibility_Filter(t *testing.T) {
361362
}
362363
}
363364

365+
func TestResourceFungibility_Score(t *testing.T) {
366+
tests := []struct {
367+
name string
368+
pod *v1.Pod
369+
model *llmazcoreapi.OpenModel
370+
nodes []*v1.Node
371+
expectedList framework.NodeScoreList
372+
wantPreScoreStatus *framework.Status
373+
}{
374+
{
375+
name: "pod without model label",
376+
pod: &v1.Pod{},
377+
wantPreScoreStatus: framework.NewStatus(framework.Skip),
378+
},
379+
{
380+
name: "model without inference config",
381+
pod: &v1.Pod{
382+
ObjectMeta: metav1.ObjectMeta{
383+
Labels: map[string]string{modelNameLabelKey: "test-model"},
384+
},
385+
},
386+
model: &llmazcoreapi.OpenModel{
387+
TypeMeta: metav1.TypeMeta{
388+
APIVersion: "llmaz.io/v1alpha1",
389+
Kind: "OpenModel",
390+
},
391+
ObjectMeta: metav1.ObjectMeta{
392+
Name: "test-model",
393+
},
394+
},
395+
wantPreScoreStatus: framework.NewStatus(framework.Skip),
396+
},
397+
{
398+
name: "model has flavors but at least one flavor has empty nodeSelector",
399+
pod: &v1.Pod{
400+
ObjectMeta: metav1.ObjectMeta{
401+
Labels: map[string]string{modelNameLabelKey: "test-model"},
402+
},
403+
},
404+
model: &llmazcoreapi.OpenModel{
405+
TypeMeta: metav1.TypeMeta{
406+
APIVersion: "llmaz.io/v1alpha1",
407+
Kind: "OpenModel",
408+
},
409+
ObjectMeta: metav1.ObjectMeta{
410+
Name: "test-model",
411+
},
412+
Spec: llmazcoreapi.ModelSpec{
413+
InferenceConfig: &llmazcoreapi.InferenceConfig{
414+
Flavors: []llmazcoreapi.Flavor{
415+
{
416+
Name: "none",
417+
},
418+
{
419+
Name: "empty",
420+
NodeSelector: map[string]string{},
421+
},
422+
{
423+
Name: "t4",
424+
NodeSelector: map[string]string{"karpenter.k8s.aws/instance-gpu-name": "t4"},
425+
},
426+
},
427+
},
428+
},
429+
},
430+
wantPreScoreStatus: framework.NewStatus(framework.Skip),
431+
},
432+
{
433+
name: "model has flavors but at least one flavor has empty nodeSelector",
434+
pod: &v1.Pod{
435+
ObjectMeta: metav1.ObjectMeta{
436+
Labels: map[string]string{modelNameLabelKey: "test-model"},
437+
},
438+
},
439+
model: &llmazcoreapi.OpenModel{
440+
TypeMeta: metav1.TypeMeta{
441+
APIVersion: "llmaz.io/v1alpha1",
442+
Kind: "OpenModel",
443+
},
444+
ObjectMeta: metav1.ObjectMeta{
445+
Name: "test-model",
446+
},
447+
Spec: llmazcoreapi.ModelSpec{
448+
InferenceConfig: &llmazcoreapi.InferenceConfig{
449+
Flavors: []llmazcoreapi.Flavor{
450+
{
451+
Name: "none",
452+
},
453+
{
454+
Name: "empty",
455+
NodeSelector: map[string]string{},
456+
},
457+
{
458+
Name: "t4",
459+
NodeSelector: map[string]string{"karpenter.k8s.aws/instance-gpu-name": "t4"},
460+
},
461+
},
462+
},
463+
},
464+
},
465+
wantPreScoreStatus: framework.NewStatus(framework.Skip),
466+
},
467+
{
468+
name: "2 flavors, 2 nodes",
469+
pod: &v1.Pod{
470+
ObjectMeta: metav1.ObjectMeta{
471+
Labels: map[string]string{modelNameLabelKey: "test-model"},
472+
},
473+
},
474+
model: &llmazcoreapi.OpenModel{
475+
TypeMeta: metav1.TypeMeta{
476+
APIVersion: "llmaz.io/v1alpha1",
477+
Kind: "OpenModel",
478+
},
479+
ObjectMeta: metav1.ObjectMeta{
480+
Name: "test-model",
481+
},
482+
Spec: llmazcoreapi.ModelSpec{
483+
InferenceConfig: &llmazcoreapi.InferenceConfig{
484+
Flavors: []llmazcoreapi.Flavor{
485+
{
486+
Name: "t4",
487+
NodeSelector: map[string]string{"karpenter.k8s.aws/instance-gpu-name": "t4"},
488+
},
489+
{
490+
Name: "a100",
491+
NodeSelector: map[string]string{"karpenter.k8s.aws/instance-gpu-name": "a100"},
492+
},
493+
},
494+
},
495+
},
496+
},
497+
nodes: []*v1.Node{
498+
{
499+
ObjectMeta: metav1.ObjectMeta{Name: "node1", Labels: map[string]string{"karpenter.k8s.aws/instance-gpu-name": "t4"}}},
500+
{
501+
ObjectMeta: metav1.ObjectMeta{Name: "node2", Labels: map[string]string{"karpenter.k8s.aws/instance-gpu-name": "a100"}},
502+
},
503+
},
504+
expectedList: framework.NodeScoreList{
505+
{
506+
Name: "node1",
507+
Score: 39,
508+
},
509+
{
510+
Name: "node2",
511+
Score: 24,
512+
},
513+
},
514+
},
515+
}
516+
517+
for _, tc := range tests {
518+
t.Run(tc.name, func(t *testing.T) {
519+
ctx, cancel := context.WithCancel(context.Background())
520+
defer cancel()
521+
522+
fr, err := tf.NewFramework(ctx, registeredPlugins, Name, frameworkruntime.WithSnapshotSharedLister(cache.NewSnapshot(nil, tc.nodes)))
523+
if err != nil {
524+
t.Fatalf("failed to create framework: %v", err)
525+
}
526+
527+
scheme := runtime.NewScheme()
528+
dynClient := dynamicfake.NewSimpleDynamicClient(scheme)
529+
if tc.model != nil {
530+
dynClient = dynamicfake.NewSimpleDynamicClient(scheme, tc.model)
531+
}
532+
533+
p := &ResourceFungibility{
534+
handle: fr,
535+
dynClient: dynClient,
536+
}
537+
538+
// PreScore needs to read the state which is written by PreFilter.
539+
state := framework.NewCycleState()
540+
p.PreFilter(ctx, state, tc.pod)
541+
542+
status := p.PreScore(ctx, state, tc.pod, tf.BuildNodeInfos(tc.nodes))
543+
if status.Code() != tc.wantPreScoreStatus.Code() {
544+
t.Errorf("unexpected status code from PreScore: want: %v got: %v", tc.wantPreScoreStatus.Code().String(), status.Code().String())
545+
}
546+
if status.Message() != tc.wantPreScoreStatus.Message() {
547+
t.Errorf("unexpected status message from PreScore: want: %v got: %v", tc.wantPreScoreStatus.Message(), status.Message())
548+
}
549+
if !status.IsSuccess() {
550+
// no need to proceed.
551+
return
552+
}
553+
554+
var gotList framework.NodeScoreList
555+
nodeInfos := tf.BuildNodeInfos(tc.nodes)
556+
for _, nodeInfo := range nodeInfos {
557+
nodeName := nodeInfo.Node().Name
558+
score, status := p.Score(ctx, state, tc.pod, nodeInfo)
559+
if !status.IsSuccess() {
560+
t.Errorf("unexpected error: %v", status)
561+
}
562+
gotList = append(gotList, framework.NodeScore{Name: nodeName, Score: score})
563+
}
564+
565+
if diff := cmp.Diff(tc.expectedList, gotList); diff != "" {
566+
t.Errorf("obtained scores (-want,+got):\n%s", diff)
567+
}
568+
})
569+
}
570+
}
571+
364572
func TestNew(t *testing.T) {
365573
ctx, cancel := context.WithCancel(context.Background())
366574
defer cancel()

0 commit comments

Comments
 (0)