github.com/kubewharf/katalyst-core@v0.5.3/pkg/scheduler/plugins/noderesourcetopology/score.go (about)

     1  /*
     2  Copyright 2022 The Katalyst Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package noderesourcetopology
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  
    23  	"gonum.org/v1/gonum/stat"
    24  	v1 "k8s.io/api/core/v1"
    25  	"k8s.io/apimachinery/pkg/api/resource"
    26  	"k8s.io/apimachinery/pkg/util/sets"
    27  	"k8s.io/klog/v2"
    28  	"k8s.io/kubernetes/pkg/apis/core/v1/helper/qos"
    29  	"k8s.io/kubernetes/pkg/kubelet/cm/topologymanager/bitmask"
    30  	"k8s.io/kubernetes/pkg/scheduler/framework"
    31  
    32  	"github.com/kubewharf/katalyst-api/pkg/apis/node/v1alpha1"
    33  	"github.com/kubewharf/katalyst-api/pkg/consts"
    34  	"github.com/kubewharf/katalyst-core/pkg/scheduler/cache"
    35  	"github.com/kubewharf/katalyst-core/pkg/scheduler/util"
    36  )
    37  
    38  const (
    39  	defaultWeight = int64(1)
    40  )
    41  
    42  type scoreStrategyFn func(v1.ResourceList, v1.ResourceList, resourceToWeightMap, sets.String) int64
    43  
    44  // resourceToWeightMap contains resource name and weight.
    45  type resourceToWeightMap map[v1.ResourceName]int64
    46  
    47  // weight return the weight of the resource and defaultWeight if weight not specified
    48  func (rw resourceToWeightMap) weight(r v1.ResourceName) int64 {
    49  	w, ok := (rw)[r]
    50  	if !ok {
    51  		return defaultWeight
    52  	}
    53  
    54  	if w < 1 {
    55  		return defaultWeight
    56  	}
    57  
    58  	return w
    59  }
    60  
    61  func (tm *TopologyMatch) Score(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string) (int64, *framework.Status) {
    62  	if !tm.topologyMatchSupport(pod) {
    63  		klog.V(5).Infof("pod %v not support topology match", pod.Name)
    64  		return framework.MaxNodeScore, nil
    65  	}
    66  
    67  	var (
    68  		nodeInfo          *framework.NodeInfo
    69  		err               error
    70  		nodeResourceCache *cache.ResourceTopology
    71  	)
    72  	nodeInfo, err = tm.sharedLister.NodeInfos().Get(nodeName)
    73  	if err != nil {
    74  		klog.Errorf("get node %v from snapshot fail: %v", nodeName, err)
    75  		return 0, framework.AsStatus(err)
    76  	}
    77  	if consts.ResourcePluginPolicyNameDynamic == tm.resourcePolicy {
    78  		// only dedicated pods will participate in the calculation
    79  		nodeResourceCache = cache.GetCache().GetNodeResourceTopology(nodeName, tm.dedicatedPodsFilter(nodeInfo))
    80  	} else {
    81  		nodeResourceCache = cache.GetCache().GetNodeResourceTopology(nodeName, nil)
    82  	}
    83  
    84  	if nodeResourceCache == nil {
    85  		klog.Warningf("node %s nodeCache is nil", nodeName)
    86  		return 0, nil
    87  	}
    88  	handler := tm.scoringHandler(pod, nodeResourceCache)
    89  	if handler == nil {
    90  		klog.V(5).Infof("pod %v not match scoring handler", pod.Name)
    91  		return 0, nil
    92  	}
    93  
    94  	return handler(pod, nodeResourceCache.TopologyZone)
    95  }
    96  
    97  func (tm *TopologyMatch) ScoreExtensions() framework.ScoreExtensions {
    98  	return nil
    99  }
   100  
   101  func (tm *TopologyMatch) scoringHandler(pod *v1.Pod, nodeResourceTopology *cache.ResourceTopology) scoringFn {
   102  	if tm.resourcePolicy == consts.ResourcePluginPolicyNameNative {
   103  		// only single-numa-node supported
   104  		if qos.GetPodQOS(pod) == v1.PodQOSGuaranteed && util.IsRequestFullCPU(pod) {
   105  			switch nodeResourceTopology.TopologyPolicy {
   106  			case v1alpha1.TopologyPolicySingleNUMANodePodLevel:
   107  				return func(pod *v1.Pod, zones []*v1alpha1.TopologyZone) (int64, *framework.Status) {
   108  					return podScopeScore(pod, zones, tm.scoreStrategyFunc, tm.resourceToWeightMap)
   109  				}
   110  			case v1alpha1.TopologyPolicySingleNUMANodeContainerLevel:
   111  				return func(pod *v1.Pod, zones []*v1alpha1.TopologyZone) (int64, *framework.Status) {
   112  					return containerScopeScore(pod, zones, tm.scoreStrategyFunc, tm.resourceToWeightMap, scoreForEachNUMANode, nil)
   113  				}
   114  			default:
   115  				// not support
   116  				return nil
   117  			}
   118  		}
   119  	}
   120  
   121  	if tm.resourcePolicy == consts.ResourcePluginPolicyNameDynamic {
   122  		// dedicated_cores + numa_binding
   123  		if util.IsDedicatedPod(pod) && util.IsNumaBinding(pod) && !util.IsExclusive(pod) {
   124  			switch nodeResourceTopology.TopologyPolicy {
   125  			case v1alpha1.TopologyPolicySingleNUMANodeContainerLevel,
   126  				v1alpha1.TopologyPolicyNumericContainerLevel:
   127  				return func(pod *v1.Pod, zones []*v1alpha1.TopologyZone) (int64, *framework.Status) {
   128  					return containerScopeScore(pod, zones, tm.scoreStrategyFunc, tm.resourceToWeightMap, scoreForEachNUMANode, tm.alignedResources)
   129  				}
   130  			default:
   131  				return nil
   132  			}
   133  		}
   134  
   135  		if util.IsDedicatedPod(pod) && util.IsNumaBinding(pod) && util.IsExclusive(pod) {
   136  			switch nodeResourceTopology.TopologyPolicy {
   137  			case v1alpha1.TopologyPolicySingleNUMANodeContainerLevel:
   138  				return func(pod *v1.Pod, zones []*v1alpha1.TopologyZone) (int64, *framework.Status) {
   139  					return containerScopeScore(pod, zones, tm.scoreStrategyFunc, tm.resourceToWeightMap, scoreForEachNUMANode, tm.alignedResources)
   140  				}
   141  
   142  			case v1alpha1.TopologyPolicyNumericContainerLevel:
   143  				return func(pod *v1.Pod, zones []*v1alpha1.TopologyZone) (int64, *framework.Status) {
   144  					return containerScopeScore(pod, zones, tm.scoreStrategyFunc, tm.resourceToWeightMap, scoreForNUMANodes, tm.alignedResources)
   145  				}
   146  			default:
   147  				return nil
   148  			}
   149  		}
   150  	}
   151  
   152  	return nil
   153  }
   154  
   155  func containerScopeScore(
   156  	pod *v1.Pod,
   157  	nodeTopologyZones []*v1alpha1.TopologyZone,
   158  	scorerFn scoreStrategyFn,
   159  	resourceToWeightMap resourceToWeightMap,
   160  	numaScoreFunc NUMAScoreFunc,
   161  	alignedResource sets.String,
   162  ) (int64, *framework.Status) {
   163  	var (
   164  		containers  = append(pod.Spec.InitContainers, pod.Spec.Containers...)
   165  		contScore   = make([]float64, len(containers))
   166  		isExclusive = util.IsExclusive(pod)
   167  	)
   168  
   169  	NUMANodeList := TopologyZonesToNUMANodeList(nodeTopologyZones)
   170  	for i, container := range containers {
   171  		identifier := fmt.Sprintf("%s/%s/%s", pod.Namespace, pod.Name, container.Name)
   172  		contScore[i] = float64(numaScoreFunc(container.Resources.Requests, NUMANodeList, scorerFn, resourceToWeightMap, alignedResource, isExclusive))
   173  		klog.V(6).InfoS("container scope scoring", "container", identifier, "score", contScore[i])
   174  	}
   175  	finalScore := int64(stat.Mean(contScore, nil))
   176  	klog.V(5).InfoS("container scope scoring final node score", "finalScore", finalScore)
   177  	return finalScore, nil
   178  }
   179  
   180  func podScopeScore(
   181  	pod *v1.Pod,
   182  	zones []*v1alpha1.TopologyZone,
   183  	scorerFn scoreStrategyFn,
   184  	resourceToWeightMap resourceToWeightMap,
   185  ) (int64, *framework.Status) {
   186  	resources := util.GetPodEffectiveRequest(pod)
   187  
   188  	NUMANodeList := TopologyZonesToNUMANodeList(zones)
   189  	finalScore := scoreForEachNUMANode(resources, NUMANodeList, scorerFn, resourceToWeightMap, nativeAlignedResources, false)
   190  	klog.V(5).InfoS("pod scope scoring final node score", "finalScore", finalScore)
   191  	return finalScore, nil
   192  }
   193  
   194  type NUMAScoreFunc func(v1.ResourceList, NUMANodeList, scoreStrategyFn, resourceToWeightMap, sets.String, bool) int64
   195  
   196  func scoreForNUMANodes(requested v1.ResourceList, numaList NUMANodeList, score scoreStrategyFn, resourceToWeightMap resourceToWeightMap, alignedResource sets.String, isExclusive bool) int64 {
   197  	// only alignedResource will be scored under numeric policy
   198  	// score for numaList which satisfy min NUMANodes count for all alignResources.
   199  	minNUMANodeCountForAlignResources := 1
   200  	numaNodeMap := make(map[int]NUMANode)
   201  	numaNodes := make([]int, 0, len(numaList))
   202  	for _, numaNode := range numaList {
   203  		numaNodeMap[numaNode.NUMAID] = numaNode
   204  		numaNodes = append(numaNodes, numaNode.NUMAID)
   205  	}
   206  	for resourceName, quantity := range requested {
   207  		if alignedResource.Has(resourceName.String()) {
   208  			minCount := minNumaNodeCount(resourceName, quantity, numaNodeMap)
   209  			if minNUMANodeCountForAlignResources < minCount {
   210  				minNUMANodeCountForAlignResources = minCount
   211  			}
   212  		}
   213  	}
   214  
   215  	scores := make(map[string]int64)
   216  	minScore := int64(0)
   217  	bitmask.IterateBitMasks(numaNodes, func(mask bitmask.BitMask) {
   218  		maskCount := mask.Count()
   219  		// if maskCount is greater than minNUMANodeCountForAlignResources, the hint will not be preferred.
   220  		if maskCount != minNUMANodeCountForAlignResources {
   221  			return
   222  		}
   223  
   224  		var resourceAvailable v1.ResourceList
   225  		maskBits := mask.GetBits()
   226  		for _, nodeID := range maskBits {
   227  			numaNode := numaNodeMap[nodeID]
   228  			numaAvailable := numaNode.Available
   229  			if isExclusive {
   230  				numaAvailable = exclusiveAvailable(numaNode, alignedResource)
   231  			}
   232  			resourceAvailable = mergeResourceList(resourceAvailable, numaAvailable)
   233  		}
   234  
   235  		numaScore := score(requested, resourceAvailable, resourceToWeightMap, alignedResource)
   236  		if (minScore == 0) || (numaScore != 0 && numaScore < minScore) {
   237  			minScore = numaScore
   238  		}
   239  		scores[mask.String()] = numaScore
   240  	})
   241  
   242  	klog.V(6).Infof("numa score result: %v, numaScores: %v", minScore, scores)
   243  
   244  	return minScore
   245  }
   246  
   247  func scoreForEachNUMANode(requested v1.ResourceList, numaList NUMANodeList, score scoreStrategyFn, resourceToWeightMap resourceToWeightMap, alignedResource sets.String, isExclusive bool) int64 {
   248  	numaScores := make([]int64, len(numaList))
   249  	minScore := int64(0)
   250  
   251  	for _, numa := range numaList {
   252  		available := numa.Available
   253  		if isExclusive {
   254  			available = exclusiveAvailable(numa, alignedResource)
   255  		}
   256  		numaScore := score(requested, available, resourceToWeightMap, alignedResource)
   257  		// if NUMA's score is 0, i.e. not fit at all, it won't be taken under consideration by Kubelet.
   258  		if (minScore == 0) || (numaScore != 0 && numaScore < minScore) {
   259  			minScore = numaScore
   260  		}
   261  		numaScores[numa.NUMAID] = numaScore
   262  		klog.V(6).InfoS("numa score result", "numaID", numa.NUMAID, "score", numaScore)
   263  	}
   264  	return minScore
   265  }
   266  
   267  func mergeResourceList(a, b v1.ResourceList) v1.ResourceList {
   268  	if a == nil {
   269  		return b.DeepCopy()
   270  	}
   271  	ret := a.DeepCopy()
   272  
   273  	for resourceName, quantity := range b {
   274  		q, ok := ret[resourceName]
   275  		if ok {
   276  			q.Add(quantity)
   277  		} else {
   278  			q = quantity.DeepCopy()
   279  		}
   280  		ret[resourceName] = q
   281  	}
   282  	return ret
   283  }
   284  
   285  func exclusiveAvailable(numa NUMANode, alignedResource sets.String) v1.ResourceList {
   286  	numaAvailable := numa.Available.DeepCopy()
   287  
   288  	for _, resourceName := range alignedResource.UnsortedList() {
   289  		available, ok := numaAvailable[v1.ResourceName(resourceName)]
   290  		if !ok {
   291  			continue
   292  		}
   293  		// if there are resource allocated on numaNode, set available to zero
   294  		if !available.Equal(numa.Allocatable[v1.ResourceName(resourceName)]) {
   295  			numaAvailable[v1.ResourceName(resourceName)] = *resource.NewQuantity(0, available.Format)
   296  		}
   297  	}
   298  
   299  	return numaAvailable
   300  }