k8s.io/apiserver@v0.31.1/pkg/util/flowcontrol/conc_alloc.go (about)

     1  /*
     2  Copyright 2022 The Kubernetes 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 flowcontrol
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"math"
    23  	"sort"
    24  )
    25  
    26  // allocProblemItem is one of the classes to which computeConcurrencyAllocation should make an allocation
    27  type allocProblemItem struct {
    28  	target     float64
    29  	lowerBound float64
    30  	upperBound float64
    31  }
    32  
    33  // relativeAllocItem is like allocProblemItem but with target avoiding zero and the bounds divided by the target
    34  type relativeAllocItem struct {
    35  	target             float64
    36  	relativeLowerBound float64
    37  	relativeUpperBound float64
    38  }
    39  
    40  // relativeAllocProblem collects together all the classes and holds the result of sorting by increasing bounds.
    41  // For J <= K, ascendingIndices[J] identifies a bound that is <= the one of ascendingIndices[K].
    42  // When ascendingIndices[J] = 2*N + 0, this identifies the lower bound of items[N].
    43  // When ascendingIndices[J] = 2*N + 1, this identifies the upper bound of items[N].
    44  type relativeAllocProblem struct {
    45  	items            []relativeAllocItem
    46  	ascendingIndices []int
    47  }
    48  
    49  // initIndices fills in ascendingIndices and sorts them
    50  func (rap *relativeAllocProblem) initIndices() *relativeAllocProblem {
    51  	rap.ascendingIndices = make([]int, len(rap.items)*2)
    52  	for idx := 0; idx < len(rap.ascendingIndices); idx++ {
    53  		rap.ascendingIndices[idx] = idx
    54  	}
    55  	sort.Sort(rap)
    56  	return rap
    57  }
    58  
    59  func (rap *relativeAllocProblem) getItemIndex(idx int) (int, bool) {
    60  	packedIndex := rap.ascendingIndices[idx]
    61  	itemIndex := packedIndex / 2
    62  	return itemIndex, packedIndex == itemIndex*2
    63  }
    64  
    65  // decode(J) returns the bound associated with ascendingIndices[J], the associated items index,
    66  // and a bool indicating whether the bound is the item's lower bound.
    67  func (rap *relativeAllocProblem) decode(idx int) (float64, int, bool) {
    68  	itemIdx, lower := rap.getItemIndex(idx)
    69  	if lower {
    70  		return rap.items[itemIdx].relativeLowerBound, itemIdx, lower
    71  	}
    72  	return rap.items[itemIdx].relativeUpperBound, itemIdx, lower
    73  }
    74  
    75  func (rap *relativeAllocProblem) getProportion(idx int) float64 {
    76  	prop, _, _ := rap.decode(idx)
    77  	return prop
    78  }
    79  
    80  func (rap *relativeAllocProblem) Len() int { return len(rap.items) * 2 }
    81  
    82  func (rap *relativeAllocProblem) Less(i, j int) bool {
    83  	return rap.getProportion(i) < rap.getProportion(j)
    84  }
    85  
    86  func (rap *relativeAllocProblem) Swap(i, j int) {
    87  	rap.ascendingIndices[i], rap.ascendingIndices[j] = rap.ascendingIndices[j], rap.ascendingIndices[i]
    88  }
    89  
    90  // minMax records the minimum and maximum value seen while scanning a set of numbers
    91  type minMax struct {
    92  	min float64
    93  	max float64
    94  }
    95  
    96  // note scans one more number
    97  func (mm *minMax) note(x float64) {
    98  	mm.min = math.Min(mm.min, x)
    99  	mm.max = math.Max(mm.max, x)
   100  }
   101  
   102  const MinTarget = 0.001
   103  const epsilon = 0.0000001
   104  
   105  // computeConcurrencyAllocation returns the unique `allocs []float64`, and
   106  // an associated `fairProp float64`, that jointly have
   107  // all of the following properties (to the degree that floating point calculations allow)
   108  // if possible otherwise returns an error saying why it is impossible.
   109  // `allocs` sums to `requiredSum`.
   110  // For each J in [0, len(classes)):
   111  //  1. `classes[J].lowerBound <= allocs[J] <= classes[J].upperBound` and
   112  //  2. exactly one of the following is true:
   113  //     2a. `allocs[J] == fairProp * classes[J].target`,
   114  //     2b. `allocs[J] == classes[J].lowerBound && classes[J].lowerBound > fairProp * classes[J].target`, or
   115  //     2c. `allocs[J] == classes[J].upperBound && classes[J].upperBound < fairProp * classes[J].target`.
   116  //
   117  // Each allocProblemItem is required to have `target >= lowerBound >= 0` and `upperBound >= lowerBound`.
   118  // A target smaller than MinTarget is treated as if it were MinTarget.
   119  func computeConcurrencyAllocation(requiredSum int, classes []allocProblemItem) ([]float64, float64, error) {
   120  	if requiredSum < 0 {
   121  		return nil, 0, errors.New("negative sums are not supported")
   122  	}
   123  	requiredSumF := float64(requiredSum)
   124  	var lowSum, highSum, targetSum float64
   125  	ubRange := minMax{min: float64(math.MaxFloat32)}
   126  	lbRange := minMax{min: float64(math.MaxFloat32)}
   127  	relativeItems := make([]relativeAllocItem, len(classes))
   128  	for idx, item := range classes {
   129  		target := item.target
   130  		if item.lowerBound < 0 {
   131  			return nil, 0, fmt.Errorf("lower bound %d is %v but negative lower bounds are not allowed", idx, item.lowerBound)
   132  		}
   133  		if target < item.lowerBound {
   134  			return nil, 0, fmt.Errorf("target %d is %v, which is below its lower bound of %v", idx, target, item.lowerBound)
   135  		}
   136  		if item.upperBound < item.lowerBound {
   137  			return nil, 0, fmt.Errorf("upper bound %d is %v but should not be less than the lower bound %v", idx, item.upperBound, item.lowerBound)
   138  		}
   139  		if target < MinTarget {
   140  			// tweak this to a non-zero value so avoid dividing by zero
   141  			target = MinTarget
   142  		}
   143  		lowSum += item.lowerBound
   144  		highSum += item.upperBound
   145  		targetSum += target
   146  		relativeItem := relativeAllocItem{
   147  			target:             target,
   148  			relativeLowerBound: item.lowerBound / target,
   149  			relativeUpperBound: item.upperBound / target,
   150  		}
   151  		ubRange.note(relativeItem.relativeUpperBound)
   152  		lbRange.note(relativeItem.relativeLowerBound)
   153  		relativeItems[idx] = relativeItem
   154  	}
   155  	if lbRange.max > 1 {
   156  		return nil, 0, fmt.Errorf("lbRange.max-1=%v, which is impossible because lbRange.max can not be greater than 1", lbRange.max-1)
   157  	}
   158  	if lowSum-requiredSumF > epsilon {
   159  		return nil, 0, fmt.Errorf("lower bounds sum to %v, which is higher than the required sum of %v", lowSum, requiredSum)
   160  	}
   161  	if requiredSumF-highSum > epsilon {
   162  		return nil, 0, fmt.Errorf("upper bounds sum to %v, which is lower than the required sum of %v", highSum, requiredSum)
   163  	}
   164  	ans := make([]float64, len(classes))
   165  	if requiredSum == 0 {
   166  		return ans, 0, nil
   167  	}
   168  	if lowSum-requiredSumF > -epsilon { // no wiggle room, constrained from below
   169  		for idx, item := range classes {
   170  			ans[idx] = item.lowerBound
   171  		}
   172  		return ans, lbRange.min, nil
   173  	}
   174  	if requiredSumF-highSum > -epsilon { // no wiggle room, constrained from above
   175  		for idx, item := range classes {
   176  			ans[idx] = item.upperBound
   177  		}
   178  		return ans, ubRange.max, nil
   179  	}
   180  	// Now we know the solution is a unique fairProp in [lbRange.min, ubRange.max].
   181  	// See if the solution does not run into any bounds.
   182  	fairProp := requiredSumF / targetSum
   183  	if lbRange.max <= fairProp && fairProp <= ubRange.min { // no bounds matter
   184  		for idx := range classes {
   185  			ans[idx] = relativeItems[idx].target * fairProp
   186  		}
   187  		return ans, fairProp, nil
   188  	}
   189  	// Sadly, some bounds matter.
   190  	// We find the solution by sorting the bounds and considering progressively
   191  	// higher values of fairProp, starting from lbRange.min.
   192  	rap := (&relativeAllocProblem{items: relativeItems}).initIndices()
   193  	sumSoFar := lowSum
   194  	fairProp = lbRange.min
   195  	var sensitiveTargetSum, deltaSensitiveTargetSum float64
   196  	var numSensitiveClasses, deltaSensitiveClasses int
   197  	var nextIdx int
   198  	// `nextIdx` is the next `rap` index to consider.
   199  	// `sumSoFar` is what the allocs would sum to if the current
   200  	// value of `fairProp` solves the problem.
   201  	// If the current value of fairProp were the answer then
   202  	// `sumSoFar == requiredSum`.
   203  	// Otherwise the next increment in fairProp involves changing the allocations
   204  	// of `numSensitiveClasses` classes whose targets sum to `sensitiveTargetSum`;
   205  	// for the other classes, an upper or lower bound has applied and will continue to apply.
   206  	// The last increment of nextIdx calls for adding `deltaSensitiveClasses`
   207  	// to `numSensitiveClasses` and adding `deltaSensitiveTargetSum` to `sensitiveTargetSum`.
   208  	for sumSoFar < requiredSumF {
   209  		// There might be more than one bound that is equal to the current value
   210  		// of fairProp; find all of them because they will all be relevant to
   211  		// the next change in fairProp.
   212  		// Set nextBound to the next bound that is NOT equal to fairProp,
   213  		// and advance nextIdx to the index of that bound.
   214  		var nextBound float64
   215  		for {
   216  			sensitiveTargetSum += deltaSensitiveTargetSum
   217  			numSensitiveClasses += deltaSensitiveClasses
   218  			if nextIdx >= rap.Len() {
   219  				return nil, 0, fmt.Errorf("impossible: ran out of bounds to consider in bound-constrained problem")
   220  			}
   221  			var itemIdx int
   222  			var lower bool
   223  			nextBound, itemIdx, lower = rap.decode(nextIdx)
   224  			if lower {
   225  				deltaSensitiveClasses = 1
   226  				deltaSensitiveTargetSum = rap.items[itemIdx].target
   227  			} else {
   228  				deltaSensitiveClasses = -1
   229  				deltaSensitiveTargetSum = -rap.items[itemIdx].target
   230  			}
   231  			nextIdx++
   232  			if nextBound > fairProp {
   233  				break
   234  			}
   235  		}
   236  		// fairProp can increase to nextBound without passing any intermediate bounds.
   237  		if numSensitiveClasses == 0 {
   238  			// No classes are affected by the next range of fairProp; skip right past it
   239  			fairProp = nextBound
   240  			continue
   241  		}
   242  		// See whether fairProp can increase to the solution before passing the next bound.
   243  		deltaFairProp := (requiredSumF - sumSoFar) / sensitiveTargetSum
   244  		nextProp := fairProp + deltaFairProp
   245  		if nextProp <= nextBound {
   246  			fairProp = nextProp
   247  			break
   248  		}
   249  		// No, fairProp has to increase above nextBound
   250  		sumSoFar += (nextBound - fairProp) * sensitiveTargetSum
   251  		fairProp = nextBound
   252  	}
   253  	for idx, item := range classes {
   254  		ans[idx] = math.Max(item.lowerBound, math.Min(item.upperBound, fairProp*relativeItems[idx].target))
   255  	}
   256  	return ans, fairProp, nil
   257  }