mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
242 lines
7.0 KiB
Go
242 lines
7.0 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package feasible
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"math"
|
|
|
|
"github.com/hashicorp/go-set/v3"
|
|
"github.com/hashicorp/nomad/client/lib/numalib"
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
psstructs "github.com/hashicorp/nomad/plugins/shared/structs"
|
|
)
|
|
|
|
// deviceAllocator is used to allocate devices to allocations. The allocator
|
|
// tracks availability as to not double allocate devices.
|
|
type deviceAllocator struct {
|
|
*structs.DeviceAccounter
|
|
|
|
ctx Context
|
|
}
|
|
|
|
// newDeviceAllocator returns a new device allocator. The node is used to
|
|
// populate the set of available devices based on what healthy device instances
|
|
// exist on the node.
|
|
func newDeviceAllocator(ctx Context, n *structs.Node) *deviceAllocator {
|
|
return &deviceAllocator{
|
|
ctx: ctx,
|
|
DeviceAccounter: structs.NewDeviceAccounter(n),
|
|
}
|
|
}
|
|
|
|
func (d *deviceAllocator) Copy() *deviceAllocator {
|
|
accounter := d.DeviceAccounter.Copy()
|
|
allocator := &deviceAllocator{accounter, d.ctx}
|
|
return allocator
|
|
}
|
|
|
|
type memoryNodeMatcher struct {
|
|
memoryNode int // the target memory node (-1 indicates don't care)
|
|
topology *numalib.Topology // the topology of the candidate node
|
|
devices *set.Set[string] // the set of devices requiring numa associativity
|
|
}
|
|
|
|
// equalBusID will compare the instance specific device bus id values in a way
|
|
// that handles non-uniform domain strings (e.g. "0000" vs "00000000").
|
|
//
|
|
// e.g. 0000:03:00.1 is equal to 00000000:03.00.1
|
|
func equalBusID(a, b string) bool {
|
|
if a == b {
|
|
return true
|
|
}
|
|
noDomainA := strings.TrimLeft(a, "0")
|
|
noDomainB := strings.TrimLeft(b, "0")
|
|
return noDomainA == noDomainB
|
|
}
|
|
|
|
// Matches returns whether the given device instance is on a PCI bus that is
|
|
// on the same NUMA node as the memory node of the matcher.
|
|
//
|
|
// instanceID is something like "GPU-6b5fa173-5fa6-2d38-54fe-d64c1fe4fe10"
|
|
//
|
|
// device is the grouping of device instance this instance belongs to and is
|
|
// how we find the pci bus locality.
|
|
func (m *memoryNodeMatcher) Matches(instanceID string, device *structs.NodeDeviceResource) bool {
|
|
// -1 is the sentinel value for not caring about the associated memory
|
|
// node, in which case we simply treat the device as a match
|
|
if m.memoryNode == -1 {
|
|
return true
|
|
}
|
|
|
|
// if the device is not listed in the numa block of the task resources then
|
|
// we do not care about what node is is on
|
|
if !m.devices.Contains(device.ID().String()) {
|
|
return true
|
|
}
|
|
|
|
// check if the hardware locality of the device matches the nume node of this
|
|
// memoryNodeMatcher instance. we do so by finding the specific device of
|
|
// the given instance id, looking at its locality, and comparing the locality
|
|
// using equalBusID because direct == equality does not work, due to
|
|
// differences in pci bus domain representations
|
|
for _, instance := range device.Instances {
|
|
if instance.ID == instanceID {
|
|
if instance.Locality != nil {
|
|
instanceBusID := instance.Locality.PciBusID
|
|
for busID, node := range m.topology.BusAssociativity {
|
|
if equalBusID(busID, instanceBusID) {
|
|
result := int(node) == m.memoryNode
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// createOffer takes a device request and returns an assignment as well as a
|
|
// score for the assignment. If no assignment is possible, an error is
|
|
// returned explaining why.
|
|
func (d *deviceAllocator) createOffer(mem *memoryNodeMatcher, ask *structs.RequestedDevice) (out *structs.AllocatedDeviceResource, score float64, err error) {
|
|
// Try to hot path
|
|
if len(d.Devices) == 0 {
|
|
return nil, 0.0, fmt.Errorf("no devices available")
|
|
}
|
|
if ask.Count == 0 {
|
|
return nil, 0.0, fmt.Errorf("invalid request of zero devices")
|
|
}
|
|
|
|
// Hold the current best offer
|
|
var offer *structs.AllocatedDeviceResource
|
|
var offerScore float64
|
|
var matchedWeights float64
|
|
|
|
// Determine the devices that are feasible based on availability and
|
|
// constraints
|
|
for id, devInst := range d.Devices {
|
|
// Check if we have enough unused instances to use this
|
|
assignable := uint64(0)
|
|
for instanceID, v := range devInst.Instances {
|
|
if v != 0 {
|
|
continue
|
|
}
|
|
if !mem.Matches(instanceID, devInst.Device) {
|
|
continue
|
|
}
|
|
assignable++
|
|
}
|
|
|
|
// This device doesn't have enough instances
|
|
if assignable < ask.Count {
|
|
continue
|
|
}
|
|
|
|
// Check if the device works
|
|
if !nodeDeviceMatches(d.ctx, devInst.Device, ask) {
|
|
continue
|
|
}
|
|
|
|
// Score the choice
|
|
var choiceScore float64
|
|
|
|
// Track the sum of matched affinity weights in a separate variable
|
|
// We return this if this device had the best score compared to other devices considered
|
|
var sumMatchedWeights float64
|
|
if l := len(ask.Affinities); l != 0 {
|
|
totalWeight := 0.0
|
|
for _, a := range ask.Affinities {
|
|
// Resolve the targets
|
|
lVal, lOk := resolveDeviceTarget(a.LTarget, devInst.Device)
|
|
rVal, rOk := resolveDeviceTarget(a.RTarget, devInst.Device)
|
|
|
|
totalWeight += math.Abs(float64(a.Weight))
|
|
|
|
// Check if satisfied
|
|
if !checkAttributeAffinity(d.ctx, a.Operand, lVal, rVal, lOk, rOk) {
|
|
continue
|
|
}
|
|
choiceScore += float64(a.Weight)
|
|
sumMatchedWeights += float64(a.Weight)
|
|
}
|
|
|
|
// normalize
|
|
choiceScore /= totalWeight
|
|
}
|
|
|
|
// Only use the device if it is a higher score than we have already seen
|
|
if offer != nil && choiceScore < offerScore {
|
|
continue
|
|
}
|
|
|
|
// Set the new highest score
|
|
offerScore = choiceScore
|
|
|
|
// Set the new sum of matching affinity weights
|
|
matchedWeights = sumMatchedWeights
|
|
|
|
// Build the choice
|
|
offer = &structs.AllocatedDeviceResource{
|
|
Vendor: id.Vendor,
|
|
Type: id.Type,
|
|
Name: id.Name,
|
|
DeviceIDs: make([]string, 0, ask.Count),
|
|
}
|
|
|
|
assigned := uint64(0)
|
|
for id, v := range devInst.Instances {
|
|
if v == 0 && assigned < ask.Count &&
|
|
d.deviceIDMatchesConstraint(id, ask.Constraints, devInst.Device) {
|
|
assigned++
|
|
offer.DeviceIDs = append(offer.DeviceIDs, id)
|
|
if assigned == ask.Count {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Failed to find a match
|
|
if offer == nil {
|
|
return nil, 0.0, fmt.Errorf("no devices match request")
|
|
}
|
|
|
|
return offer, matchedWeights, nil
|
|
}
|
|
|
|
// deviceIDMatchesConstraint checks a device instance ID against the constraints
|
|
// to ensure we're only assigning instance IDs that match. This is a narrower
|
|
// check than nodeDeviceMatches because we've already asserted that the device
|
|
// matches and now need to filter by instance ID.
|
|
func (d *deviceAllocator) deviceIDMatchesConstraint(id string, constraints structs.Constraints, device *structs.NodeDeviceResource) bool {
|
|
|
|
// There are no constraints to consider
|
|
if len(constraints) == 0 {
|
|
return true
|
|
}
|
|
|
|
deviceID := psstructs.NewStringAttribute(id)
|
|
|
|
for _, c := range constraints {
|
|
var target *psstructs.Attribute
|
|
if c.LTarget == "${device.ids}" {
|
|
target, _ = resolveDeviceTarget(c.RTarget, device)
|
|
} else if c.RTarget == "${device.ids}" {
|
|
target, _ = resolveDeviceTarget(c.LTarget, device)
|
|
} else {
|
|
continue
|
|
}
|
|
|
|
if !checkAttributeConstraint(d.ctx, c.Operand, target, deviceID, true, true) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|