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 }