volcano.sh/volcano@v1.9.0/pkg/scheduler/plugins/numaaware/provider/cpumanager/cpu_assignment.go (about)

     1  /*
     2  Copyright 2021 The Volcano 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 cpumanager
    18  
    19  import (
    20  	"fmt"
    21  	"sort"
    22  
    23  	"k8s.io/klog/v2"
    24  	"k8s.io/kubernetes/pkg/kubelet/cm/cpumanager/topology"
    25  	"k8s.io/utils/cpuset"
    26  )
    27  
    28  type cpuAccumulator struct {
    29  	topo          *topology.CPUTopology
    30  	details       topology.CPUDetails
    31  	numCPUsNeeded int
    32  	result        cpuset.CPUSet
    33  }
    34  
    35  func newCPUAccumulator(topo *topology.CPUTopology, availableCPUs cpuset.CPUSet, numCPUs int) *cpuAccumulator {
    36  	return &cpuAccumulator{
    37  		topo:          topo,
    38  		details:       topo.CPUDetails.KeepOnly(availableCPUs),
    39  		numCPUsNeeded: numCPUs,
    40  		result:        cpuset.New(),
    41  	}
    42  }
    43  
    44  func (a *cpuAccumulator) take(cpus cpuset.CPUSet) {
    45  	a.result = a.result.Union(cpus)
    46  	a.details = a.details.KeepOnly(a.details.CPUs().Difference(a.result))
    47  	a.numCPUsNeeded -= cpus.Size()
    48  }
    49  
    50  // freeSockets Returns free socket IDs as a slice sorted by:
    51  // - socket ID, ascending.
    52  func (a *cpuAccumulator) freeSockets() []int {
    53  	return a.details.Sockets().Intersection(a.details.CPUs()).List()
    54  }
    55  
    56  // freeCores Returns core IDs as a slice sorted by:
    57  // - the number of whole available cores on the socket, ascending
    58  // - socket ID, ascending
    59  // - core ID, ascending
    60  func (a *cpuAccumulator) freeCores() []int {
    61  	socketIDs := a.details.Sockets().UnsortedList()
    62  	sort.Slice(socketIDs,
    63  		func(i, j int) bool {
    64  			iCores := a.details.CoresInSockets(socketIDs[i]).Intersection(a.details.CPUs())
    65  			jCores := a.details.CoresInSockets(socketIDs[j]).Intersection(a.details.CPUs())
    66  			return iCores.Size() < jCores.Size() || socketIDs[i] < socketIDs[j]
    67  		})
    68  
    69  	coreIDs := []int{}
    70  	for _, s := range socketIDs {
    71  		coreIDs = append(coreIDs, a.details.CoresInSockets(s).Intersection(a.details.CPUs()).List()...)
    72  	}
    73  	return coreIDs
    74  }
    75  
    76  // freeCPUs Returns CPU IDs as a slice sorted by:
    77  // - socket affinity with result
    78  // - number of CPUs available on the same socket
    79  // - number of CPUs available on the same core
    80  // - socket ID.
    81  // - core ID.
    82  func (a *cpuAccumulator) freeCPUs() []int {
    83  	result := []int{}
    84  	cores := a.details.Cores().List()
    85  
    86  	sort.Slice(
    87  		cores,
    88  		func(i, j int) bool {
    89  			iCore := cores[i]
    90  			jCore := cores[j]
    91  
    92  			iCPUs := a.topo.CPUDetails.CPUsInCores(iCore).List()
    93  			jCPUs := a.topo.CPUDetails.CPUsInCores(jCore).List()
    94  
    95  			iSocket := a.topo.CPUDetails[iCPUs[0]].SocketID
    96  			jSocket := a.topo.CPUDetails[jCPUs[0]].SocketID
    97  
    98  			// Compute the number of CPUs in the result reside on the same socket
    99  			// as each core.
   100  			iSocketColoScore := a.topo.CPUDetails.CPUsInSockets(iSocket).Intersection(a.result).Size()
   101  			jSocketColoScore := a.topo.CPUDetails.CPUsInSockets(jSocket).Intersection(a.result).Size()
   102  
   103  			// Compute the number of available CPUs available on the same socket
   104  			// as each core.
   105  			iSocketFreeScore := a.details.CPUsInSockets(iSocket).Size()
   106  			jSocketFreeScore := a.details.CPUsInSockets(jSocket).Size()
   107  
   108  			// Compute the number of available CPUs on each core.
   109  			iCoreFreeScore := a.details.CPUsInCores(iCore).Size()
   110  			jCoreFreeScore := a.details.CPUsInCores(jCore).Size()
   111  
   112  			return iSocketColoScore > jSocketColoScore ||
   113  				iSocketFreeScore < jSocketFreeScore ||
   114  				iCoreFreeScore < jCoreFreeScore ||
   115  				iSocket < jSocket ||
   116  				iCore < jCore
   117  		})
   118  
   119  	// For each core, append sorted CPU IDs to result.
   120  	for _, core := range cores {
   121  		result = append(result, a.details.CPUsInCores(core).List()...)
   122  	}
   123  	return result
   124  }
   125  
   126  func (a *cpuAccumulator) needs(n int) bool {
   127  	return a.numCPUsNeeded >= n
   128  }
   129  
   130  func (a *cpuAccumulator) isSatisfied() bool {
   131  	return a.numCPUsNeeded < 1
   132  }
   133  
   134  func (a *cpuAccumulator) isFailed() bool {
   135  	return a.numCPUsNeeded > a.details.CPUs().Size()
   136  }
   137  
   138  // takeByTopology return the assigned cpuset
   139  func takeByTopology(topo *topology.CPUTopology, availableCPUs cpuset.CPUSet, numCPUs int) (cpuset.CPUSet, error) {
   140  	acc := newCPUAccumulator(topo, availableCPUs, numCPUs)
   141  	if acc.isSatisfied() {
   142  		return acc.result, nil
   143  	}
   144  	if acc.isFailed() {
   145  		return cpuset.New(), fmt.Errorf("not enough cpus available to satisfy request")
   146  	}
   147  
   148  	// Algorithm: topology-aware best-fit
   149  	// 1. Acquire whole sockets, if available and the container requires at
   150  	//    least a socket's-worth of CPUs.
   151  	if acc.needs(acc.topo.CPUsPerSocket()) {
   152  		for _, s := range acc.freeSockets() {
   153  			klog.V(4).Infof("[cpumanager] takeByTopology: claiming socket [%d]", s)
   154  			acc.take(acc.details.CPUsInSockets(s))
   155  			if acc.isSatisfied() {
   156  				return acc.result, nil
   157  			}
   158  			if !acc.needs(acc.topo.CPUsPerSocket()) {
   159  				break
   160  			}
   161  		}
   162  	}
   163  
   164  	// 2. Acquire whole cores, if available and the container requires at least
   165  	//    a core's-worth of CPUs.
   166  	if acc.needs(acc.topo.CPUsPerCore()) {
   167  		for _, c := range acc.freeCores() {
   168  			klog.V(4).Infof("[cpumanager] takeByTopology: claiming core [%d]", c)
   169  			acc.take(acc.details.CPUsInCores(c))
   170  			if acc.isSatisfied() {
   171  				return acc.result, nil
   172  			}
   173  			if !acc.needs(acc.topo.CPUsPerCore()) {
   174  				break
   175  			}
   176  		}
   177  	}
   178  
   179  	// 3. Acquire single threads, preferring to fill partially-allocated cores
   180  	//    on the same sockets as the whole cores we have already taken in this
   181  	//    allocation.
   182  	for _, c := range acc.freeCPUs() {
   183  		klog.V(4).Infof("[cpumanager] takeByTopology: claiming CPU [%d]", c)
   184  		if acc.needs(1) {
   185  			acc.take(cpuset.New(c))
   186  		}
   187  		if acc.isSatisfied() {
   188  			return acc.result, nil
   189  		}
   190  	}
   191  
   192  	return cpuset.New(), fmt.Errorf("failed to allocate cpus")
   193  }