@@ -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+
364572func TestNew (t * testing.T ) {
365573 ctx , cancel := context .WithCancel (context .Background ())
366574 defer cancel ()
0 commit comments