github.com/kubewharf/katalyst-core@v0.5.3/pkg/agent/orm/topology/policy.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 topology
    18  
    19  import (
    20  	"k8s.io/klog/v2"
    21  
    22  	"github.com/kubewharf/katalyst-core/pkg/util/bitmask"
    23  )
    24  
    25  // Policy interface for Topology Manager Pod Admit Result
    26  type Policy interface {
    27  	// Returns Policy Name
    28  	Name() string
    29  	// Returns a merged TopologyHint based on input from hint providers
    30  	// and a Pod Admit Handler Response based on hints and policy type
    31  	Merge(providersHints []map[string][]TopologyHint) (map[string]TopologyHint, bool)
    32  }
    33  
    34  // Merge a TopologyHints permutation to a single hint by performing a bitwise-AND
    35  // of their affinity masks. The hint shall be preferred if all hits in the permutation
    36  // are preferred.
    37  func mergePermutation(numaNodes []int, permutation []TopologyHint) TopologyHint {
    38  	// Get the NUMANodeAffinity from each hint in the permutation and see if any
    39  	// of them encode unpreferred allocations.
    40  	preferred := true
    41  	defaultAffinity, _ := bitmask.NewBitMask(numaNodes...)
    42  	var numaAffinities []bitmask.BitMask
    43  	for _, hint := range permutation {
    44  		// Only consider hints that have an actual NUMANodeAffinity set.
    45  		if hint.NUMANodeAffinity != nil {
    46  			numaAffinities = append(numaAffinities, hint.NUMANodeAffinity)
    47  			// Only mark preferred if all affinities are equal.
    48  			if !hint.NUMANodeAffinity.IsEqual(numaAffinities[0]) {
    49  				preferred = false
    50  			}
    51  		}
    52  		// Only mark preferred if all affinities are preferred.
    53  		if !hint.Preferred {
    54  			preferred = false
    55  		}
    56  	}
    57  
    58  	// Merge the affinities using a bitwise-and operation.
    59  	mergedAffinity := bitmask.And(defaultAffinity, numaAffinities...)
    60  	// Build a mergedHint from the merged affinity mask, setting preferred as
    61  	// appropriate based on the logic above.
    62  	return TopologyHint{mergedAffinity, preferred}
    63  }
    64  
    65  func filterProvidersHints(providersHints []map[string][]TopologyHint) ([][]TopologyHint, []string) {
    66  	// Loop through all hint providers and save an accumulated list of the
    67  	// hints returned by each hint provider. If no hints are provided, assume
    68  	// that provider has no preference for topology-aware allocation.
    69  	var (
    70  		allProviderHints [][]TopologyHint
    71  		resourceNames    []string
    72  	)
    73  	for _, hints := range providersHints {
    74  		// If hints is nil, insert a single, preferred any-numa hint into allProviderHints.
    75  		if len(hints) == 0 {
    76  			klog.InfoS("Hint Provider has no preference for NUMA affinity with any resource")
    77  			allProviderHints = append(allProviderHints, []TopologyHint{{nil, true}})
    78  			// Here, we add a defaultResourceKey to resourceNames because we don't know
    79  			// which resource this hint is for.
    80  			resourceNames = append(resourceNames, defaultResourceKey)
    81  			continue
    82  		}
    83  
    84  		// Otherwise, accumulate the hints for each resource type into allProviderHints.
    85  		for resource := range hints {
    86  			resourceNames = append(resourceNames, resource)
    87  			if hints[resource] == nil {
    88  				klog.InfoS("Hint Provider has no preference for NUMA affinity with resource", "resource", resource)
    89  				allProviderHints = append(allProviderHints, []TopologyHint{{nil, true}})
    90  				continue
    91  			}
    92  
    93  			if len(hints[resource]) == 0 {
    94  				klog.InfoS("Hint Provider has no possible NUMA affinities for resource", "resource", resource)
    95  				allProviderHints = append(allProviderHints, []TopologyHint{{nil, false}})
    96  				continue
    97  			}
    98  
    99  			allProviderHints = append(allProviderHints, hints[resource])
   100  		}
   101  	}
   102  	return allProviderHints, resourceNames
   103  }
   104  
   105  func narrowestHint(hints []TopologyHint) *TopologyHint {
   106  	if len(hints) == 0 {
   107  		return nil
   108  	}
   109  	var narrowestHint *TopologyHint
   110  	for i := range hints {
   111  		if hints[i].NUMANodeAffinity == nil {
   112  			continue
   113  		}
   114  		if narrowestHint == nil {
   115  			narrowestHint = &hints[i]
   116  		}
   117  		if hints[i].NUMANodeAffinity.IsNarrowerThan(narrowestHint.NUMANodeAffinity) {
   118  			narrowestHint = &hints[i]
   119  		}
   120  	}
   121  	return narrowestHint
   122  }
   123  
   124  func maxOfMinAffinityCounts(filteredHints [][]TopologyHint) int {
   125  	maxOfMinCount := 0
   126  	for _, resourceHints := range filteredHints {
   127  		narrowestHint := narrowestHint(resourceHints)
   128  		if narrowestHint == nil {
   129  			continue
   130  		}
   131  		if narrowestHint.NUMANodeAffinity.Count() > maxOfMinCount {
   132  			maxOfMinCount = narrowestHint.NUMANodeAffinity.Count()
   133  		}
   134  	}
   135  	return maxOfMinCount
   136  }
   137  
   138  func compareHints(bestNonPreferredAffinityCount int, current *TopologyHint, candidate *TopologyHint) *TopologyHint {
   139  	// Only consider candidates that result in a NUMANodeAffinity > 0 to
   140  	// replace the current bestHint.
   141  	if candidate.NUMANodeAffinity.Count() == 0 {
   142  		return current
   143  	}
   144  
   145  	// If no current bestHint is set, return the candidate as the bestHint.
   146  	if current == nil {
   147  		return candidate
   148  	}
   149  
   150  	// If the current bestHint is non-preferred and the candidate hint is
   151  	// preferred, always choose the preferred hint over the non-preferred one.
   152  	if !current.Preferred && candidate.Preferred {
   153  		return candidate
   154  	}
   155  
   156  	// If the current bestHint is preferred and the candidate hint is
   157  	// non-preferred, never update the bestHint, regardless of the
   158  	// candidate hint's narowness.
   159  	if current.Preferred && !candidate.Preferred {
   160  		return current
   161  	}
   162  
   163  	// If the current bestHint and the candidate hint are both preferred,
   164  	// then only consider candidate hints that have a narrower
   165  	// NUMANodeAffinity than the NUMANodeAffinity in the current bestHint.
   166  	if current.Preferred && candidate.Preferred {
   167  		if candidate.NUMANodeAffinity.IsNarrowerThan(current.NUMANodeAffinity) {
   168  			return candidate
   169  		}
   170  		return current
   171  	}
   172  
   173  	// The only case left is if the current best bestHint and the candidate
   174  	// hint are both non-preferred. In this case, try and find a hint whose
   175  	// affinity count is as close to (but not higher than) the
   176  	// bestNonPreferredAffinityCount as possible. To do this we need to
   177  	// consider the following cases and react accordingly:
   178  	//
   179  	//   1. current.NUMANodeAffinity.Count() >  bestNonPreferredAffinityCount
   180  	//   2. current.NUMANodeAffinity.Count() == bestNonPreferredAffinityCount
   181  	//   3. current.NUMANodeAffinity.Count() <  bestNonPreferredAffinityCount
   182  	//
   183  	// For case (1), the current bestHint is larger than the
   184  	// bestNonPreferredAffinityCount, so updating to any narrower mergeHint
   185  	// is preferred over staying where we are.
   186  	//
   187  	// For case (2), the current bestHint is equal to the
   188  	// bestNonPreferredAffinityCount, so we would like to stick with what
   189  	// we have *unless* the candidate hint is also equal to
   190  	// bestNonPreferredAffinityCount and it is narrower.
   191  	//
   192  	// For case (3), the current bestHint is less than
   193  	// bestNonPreferredAffinityCount, so we would like to creep back up to
   194  	// bestNonPreferredAffinityCount as close as we can. There are three
   195  	// cases to consider here:
   196  	//
   197  	//   3a. candidate.NUMANodeAffinity.Count() >  bestNonPreferredAffinityCount
   198  	//   3b. candidate.NUMANodeAffinity.Count() == bestNonPreferredAffinityCount
   199  	//   3c. candidate.NUMANodeAffinity.Count() <  bestNonPreferredAffinityCount
   200  	//
   201  	// For case (3a), we just want to stick with the current bestHint
   202  	// because choosing a new hint that is greater than
   203  	// bestNonPreferredAffinityCount would be counter-productive.
   204  	//
   205  	// For case (3b), we want to immediately update bestHint to the
   206  	// candidate hint, making it now equal to bestNonPreferredAffinityCount.
   207  	//
   208  	// For case (3c), we know that *both* the current bestHint and the
   209  	// candidate hint are less than bestNonPreferredAffinityCount, so we
   210  	// want to choose one that brings us back up as close to
   211  	// bestNonPreferredAffinityCount as possible. There are three cases to
   212  	// consider here:
   213  	//
   214  	//   3ca. candidate.NUMANodeAffinity.Count() >  current.NUMANodeAffinity.Count()
   215  	//   3cb. candidate.NUMANodeAffinity.Count() <  current.NUMANodeAffinity.Count()
   216  	//   3cc. candidate.NUMANodeAffinity.Count() == current.NUMANodeAffinity.Count()
   217  	//
   218  	// For case (3ca), we want to immediately update bestHint to the
   219  	// candidate hint because that will bring us closer to the (higher)
   220  	// value of bestNonPreferredAffinityCount.
   221  	//
   222  	// For case (3cb), we want to stick with the current bestHint because
   223  	// choosing the candidate hint would strictly move us further away from
   224  	// the bestNonPreferredAffinityCount.
   225  	//
   226  	// Finally, for case (3cc), we know that the current bestHint and the
   227  	// candidate hint are equal, so we simply choose the narrower of the 2.
   228  
   229  	// Case 1
   230  	if current.NUMANodeAffinity.Count() > bestNonPreferredAffinityCount {
   231  		if candidate.NUMANodeAffinity.IsNarrowerThan(current.NUMANodeAffinity) {
   232  			return candidate
   233  		}
   234  		return current
   235  	}
   236  	// Case 2
   237  	if current.NUMANodeAffinity.Count() == bestNonPreferredAffinityCount {
   238  		if candidate.NUMANodeAffinity.Count() != bestNonPreferredAffinityCount {
   239  			return current
   240  		}
   241  		if candidate.NUMANodeAffinity.IsNarrowerThan(current.NUMANodeAffinity) {
   242  			return candidate
   243  		}
   244  		return current
   245  	}
   246  	// Case 3a
   247  	if candidate.NUMANodeAffinity.Count() > bestNonPreferredAffinityCount {
   248  		return current
   249  	}
   250  	// Case 3b
   251  	if candidate.NUMANodeAffinity.Count() == bestNonPreferredAffinityCount {
   252  		return candidate
   253  	}
   254  	// Case 3ca
   255  	if candidate.NUMANodeAffinity.Count() > current.NUMANodeAffinity.Count() {
   256  		return candidate
   257  	}
   258  	// Case 3cb
   259  	if candidate.NUMANodeAffinity.Count() < current.NUMANodeAffinity.Count() {
   260  		return current
   261  	}
   262  	// Case 3cc
   263  	if candidate.NUMANodeAffinity.IsNarrowerThan(current.NUMANodeAffinity) {
   264  		return candidate
   265  	}
   266  	return current
   267  }
   268  
   269  func mergeFilteredHints(numaNodes []int, filteredHints [][]TopologyHint) TopologyHint {
   270  	// Set bestNonPreferredAffinityCount to help decide which affinity mask is
   271  	// preferred amongst all non-preferred hints. We calculate this value as
   272  	// the maximum of the minimum affinity counts supplied for any given hint
   273  	// provider. In other words, prefer a hint that has an affinity mask that
   274  	// includes all of the NUMA nodes from the provider that requires the most
   275  	// NUMA nodes to satisfy its allocation.
   276  	bestNonPreferredAffinityCount := maxOfMinAffinityCounts(filteredHints)
   277  
   278  	var bestHint *TopologyHint
   279  	iterateAllProviderTopologyHints(filteredHints, func(permutation []TopologyHint) {
   280  		// Get the NUMANodeAffinity from each hint in the permutation and see if any
   281  		// of them encode unpreferred allocations.
   282  		mergedHint := mergePermutation(numaNodes, permutation)
   283  
   284  		// Compare the current bestHint with the candidate mergedHint and
   285  		// update bestHint if appropriate.
   286  		bestHint = compareHints(bestNonPreferredAffinityCount, bestHint, &mergedHint)
   287  	})
   288  
   289  	if bestHint == nil {
   290  		defaultAffinity, _ := bitmask.NewBitMask(numaNodes...)
   291  		bestHint = &TopologyHint{defaultAffinity, false}
   292  	}
   293  
   294  	return *bestHint
   295  }
   296  
   297  // Iterate over all permutations of hints in 'allProviderHints [][]TopologyHint'.
   298  //
   299  // This procedure is implemented as a recursive function over the set of hints
   300  // in 'allproviderHints[i]'. It applies the function 'callback' to each
   301  // permutation as it is found. It is the equivalent of:
   302  //
   303  // for i := 0; i < len(providerHints[0]); i++
   304  //
   305  //	for j := 0; j < len(providerHints[1]); j++
   306  //	    for k := 0; k < len(providerHints[2]); k++
   307  //	        ...
   308  //	        for z := 0; z < len(providerHints[-1]); z++
   309  //	            permutation := []TopologyHint{
   310  //	                providerHints[0][i],
   311  //	                providerHints[1][j],
   312  //	                providerHints[2][k],
   313  //	                ...
   314  //	                providerHints[-1][z]
   315  //	            }
   316  //	            callback(permutation)
   317  func iterateAllProviderTopologyHints(allProviderHints [][]TopologyHint, callback func([]TopologyHint)) {
   318  	// Internal helper function to accumulate the permutation before calling the callback.
   319  	var iterate func(i int, accum []TopologyHint)
   320  	iterate = func(i int, accum []TopologyHint) {
   321  		// Base case: we have looped through all providers and have a full permutation.
   322  		if i == len(allProviderHints) {
   323  			callback(accum)
   324  			return
   325  		}
   326  
   327  		// Loop through all hints for provider 'i', and recurse to build the
   328  		// the permutation of this hint with all hints from providers 'i++'.
   329  		for j := range allProviderHints[i] {
   330  			iterate(i+1, append(accum, allProviderHints[i][j]))
   331  		}
   332  	}
   333  	iterate(0, []TopologyHint{})
   334  }
   335  
   336  // generateResourceHints generates the map from resourceName to given hint.
   337  // all providers get a same bestHint under native policy(None,best_effort,restricted,single_numa_node),
   338  // we just map resources to the bestHint
   339  func generateResourceHints(resourceNames []string, hint TopologyHint) map[string]TopologyHint {
   340  	result := make(map[string]TopologyHint)
   341  	for _, resource := range resourceNames {
   342  		result[resource] = hint
   343  	}
   344  	// If non resourceNames are provided, we add defaultResourceKey here.
   345  	if len(resourceNames) == 0 {
   346  		result[defaultResourceKey] = hint
   347  	}
   348  	return result
   349  }