diff --git a/cmd/plugins/balloons/policy/balloons-policy.go b/cmd/plugins/balloons/policy/balloons-policy.go index 26eafc724..0165526f3 100644 --- a/cmd/plugins/balloons/policy/balloons-policy.go +++ b/cmd/plugins/balloons/policy/balloons-policy.go @@ -2151,15 +2151,15 @@ func mergeCpuClassHints(opts *cpuTreeAllocatorOptions, provider cpuClassHints, i hints := provider.Hints(intent) for i, pref := range hints.Prefer { name := fmt.Sprintf("%spref_%d_%s", cpuClassHintDevPrefix, i, pref.Name) - opts.virtDevCpusets[name] = []cpuset.CPUSet{pref.Cpus} + opts.virtDevCpusets[name] = pref.Cpus opts.preferCloseToDevices = append(opts.preferCloseToDevices, name) - log.Debugf("cpuclass hint: prefer %q -> %s", name, pref.Cpus) + log.Debugf("cpuclass hint: prefer %q -> %v", name, pref.Cpus) } for i, av := range hints.Avoid { name := fmt.Sprintf("%savoid_%d_%s", cpuClassHintDevPrefix, i, av.Name) - opts.virtDevCpusets[name] = []cpuset.CPUSet{av.Cpus} + opts.virtDevCpusets[name] = av.Cpus opts.preferFarFromDevices = append(opts.preferFarFromDevices, name) - log.Debugf("cpuclass hint: avoid %q -> %s", name, av.Cpus) + log.Debugf("cpuclass hint: avoid %q -> %v", name, av.Cpus) } } diff --git a/cmd/plugins/balloons/policy/cpuclass_test.go b/cmd/plugins/balloons/policy/cpuclass_test.go index c2aa63345..e86ed4dcd 100644 --- a/cmd/plugins/balloons/policy/cpuclass_test.go +++ b/cmd/plugins/balloons/policy/cpuclass_test.go @@ -74,23 +74,23 @@ func TestMergeCpuClassHintsNoAccumulation(t *testing.T) { script: []cpuclass.AllocationHints{ // Round 1: one prefer (A), one avoid. { - Prefer: []cpuclass.CpuPreference{{Name: "hp-reserve", Cpus: cpusA}}, - Avoid: []cpuclass.CpuPreference{{Name: "lp-clos", Cpus: cpusAvoid}}, + Prefer: []cpuclass.CpuPreference{{Name: "hp-reserve", Cpus: []cpuset.CPUSet{cpusA}}}, + Avoid: []cpuclass.CpuPreference{{Name: "lp-clos", Cpus: []cpuset.CPUSet{cpusAvoid}}}, }, // Round 2: two prefers (A, B) - different name at index 1 // so the slot-0 name stays stable, slot-1 is new. { Prefer: []cpuclass.CpuPreference{ - {Name: "hp-reserve", Cpus: cpusA}, - {Name: "extra", Cpus: cpusB}, + {Name: "hp-reserve", Cpus: []cpuset.CPUSet{cpusA}}, + {Name: "extra", Cpus: []cpuset.CPUSet{cpusB}}, }, - Avoid: []cpuclass.CpuPreference{{Name: "lp-clos", Cpus: cpusAvoid}}, + Avoid: []cpuclass.CpuPreference{{Name: "lp-clos", Cpus: []cpuset.CPUSet{cpusAvoid}}}, }, // Round 3: name at slot 0 CHANGES to C - without proper // cleanup the stale "__cls_pref_0_hp-reserve" map key from // rounds 1+2 would survive into round 3. { - Prefer: []cpuclass.CpuPreference{{Name: "third", Cpus: cpusC}}, + Prefer: []cpuclass.CpuPreference{{Name: "third", Cpus: []cpuset.CPUSet{cpusC}}}, Avoid: nil, }, }, diff --git a/pkg/resmgr/cpuclass/cpuclass.go b/pkg/resmgr/cpuclass/cpuclass.go index 21da445aa..66ee6b22d 100644 --- a/pkg/resmgr/cpuclass/cpuclass.go +++ b/pkg/resmgr/cpuclass/cpuclass.go @@ -265,22 +265,30 @@ func (h *Handler) Shutdown() error { } // intersectHints returns a copy of hints with every CpuPreference -// constrained to the given bound. Empty preferences are dropped. +// candidate set constrained to the given bound. Empty candidate sets +// are dropped; a preference is dropped only when all of its candidate +// sets become empty. Candidate order is preserved. func intersectHints(hints AllocationHints, bound cpuset.CPUSet) AllocationHints { out := AllocationHints{} - for _, p := range hints.Prefer { - s := p.Cpus.Intersection(bound) - if s.IsEmpty() { - continue - } - out.Prefer = append(out.Prefer, CpuPreference{Name: p.Name, Cpus: s}) - } - for _, p := range hints.Avoid { - s := p.Cpus.Intersection(bound) - if s.IsEmpty() { - continue + clip := func(prefs []CpuPreference) []CpuPreference { + var res []CpuPreference + for _, p := range prefs { + sets := make([]cpuset.CPUSet, 0, len(p.Cpus)) + for _, c := range p.Cpus { + s := c.Intersection(bound) + if s.IsEmpty() { + continue + } + sets = append(sets, s) + } + if len(sets) == 0 { + continue + } + res = append(res, CpuPreference{Name: p.Name, Cpus: sets}) } - out.Avoid = append(out.Avoid, CpuPreference{Name: p.Name, Cpus: s}) + return res } + out.Prefer = clip(hints.Prefer) + out.Avoid = clip(hints.Avoid) return out } diff --git a/pkg/resmgr/cpuclass/internal/pct/pct.go b/pkg/resmgr/cpuclass/internal/pct/pct.go index 5e6e3f30c..10fc88f34 100644 --- a/pkg/resmgr/cpuclass/internal/pct/pct.go +++ b/pkg/resmgr/cpuclass/internal/pct/pct.go @@ -696,31 +696,37 @@ func (a *Allocator) hpInUseCpus() cpuset.CPUSet { return out } -// hpReserveCpus returns the CPU set the upcoming HP allocation +// hpReserveCpus returns the CPU sets the upcoming HP allocation // should prefer, computed with punit-granular HP-room accounting: // // room(punit) = MaxHpCpus(punit) - len(hpUsed[punit] \ excludeBln) // +// The result is a priority-ordered list of disjoint candidate sets, +// from most to least preferred. One should honor the first possible +// candidate and ignore others. +// // Selection follows a strict tier order: // -// - Tier A (single-punit win): the punit with the largest -// non-zero room and at least requested free CPUs. Returns the -// free CPUs of that punit. +// - Tier A (single-punit wins): every punit with non-zero room and +// at least requested free CPUs, ordered by descending room, then +// descending free-CPU count, then ascending package/punit ID. +// Each punit's free CPUs form one candidate set. // - Tier B (same-package union): when no single punit can host -// `requested` HP CPUs but some package's punits jointly can, -// return the union of free CPUs across that package's punits. -// The picked package is the one with the largest aggregate -// room; ties broken by largest aggregate free-CPU count. +// `requested` HP CPUs but some packages' punits jointly can, +// each such package's union of free CPUs forms one candidate +// set, ordered by descending aggregate room, then descending +// aggregate free-CPU count, then ascending package ID. // - Tier C (cross-package): never. Steering HP work across // sockets defeats the turbo gains it would obtain, because // cross-socket data traffic typically dominates per-core // frequency benefits. // +// Tiers are never mixed, so all returned candidate sets are disjoint. // When `requested` is 0 the function falls back to Tier A only -- -// pick the punit with the most HP room and at least one free CPU. -// Returns the empty set when no punit/package satisfies any tier -// or no free CPUs remain after Allowed-intersection; the caller -// then falls back to topology-only placement. +// every punit with HP room and at least one free CPU. +// Returns nil when no punit/package satisfies any tier or no free +// CPUs remain after Allowed-intersection; the caller then falls back +// to topology-only placement. // // - free: free CPUs to consider for placement. // - excludeBln: CPUs to exclude from HP-room accounting (the @@ -729,15 +735,15 @@ func (a *Allocator) hpInUseCpus() cpuset.CPUSet { // - requested: number of CPUs the upcoming allocation wants. // 0 means "unknown" (initial priming before the count is // known); Tier A is used. -func (a *Allocator) hpReserveCpus(free cpuset.CPUSet, excludeBln cpuset.CPUSet, requested int) cpuset.CPUSet { +func (a *Allocator) hpReserveCpus(free cpuset.CPUSet, excludeBln cpuset.CPUSet, requested int) []cpuset.CPUSet { if !a.hpHintsActive() { - return cpuset.New() + return nil } if a.allowed.Size() > 0 { free = free.Intersection(a.allowed) } if free.IsEmpty() { - return cpuset.New() + return nil } type punitState struct { @@ -767,17 +773,16 @@ func (a *Allocator) hpReserveCpus(free cpuset.CPUSet, excludeBln cpuset.CPUSet, states[i].room = room } if !anyKnown { - return cpuset.New() + return nil } - // Tier A: best single punit that satisfies the request. + // Tier A: every single punit that can host the request, best + // first. Punit free sets are disjoint by construction. need := requested if need < 1 { need = 1 } - bestIdx := -1 - bestRoom := 0 - bestFree := -1 + tierA := make([]int, 0, len(a.punits)) for i := range a.punits { s := states[i] if s.free.IsEmpty() || s.room <= 0 { @@ -788,20 +793,34 @@ func (a *Allocator) hpReserveCpus(free cpuset.CPUSet, excludeBln cpuset.CPUSet, if s.free.Size() < need || s.room < need { continue } - if s.room > bestRoom || (s.room == bestRoom && s.free.Size() > bestFree) { - bestIdx = i - bestRoom = s.room - bestFree = s.free.Size() - } + tierA = append(tierA, i) } - if bestIdx >= 0 { - log.Debugf("pct: hpReserveCpus tier=A punit=%d/%d room=%d free=%s", - a.punits[bestIdx].PkgID, a.punits[bestIdx].PunitID, bestRoom, states[bestIdx].free) - return states[bestIdx].free + if len(tierA) > 0 { + sort.Slice(tierA, func(x, y int) bool { + ix, iy := tierA[x], tierA[y] + if states[ix].room != states[iy].room { + return states[ix].room > states[iy].room + } + if states[ix].free.Size() != states[iy].free.Size() { + return states[ix].free.Size() > states[iy].free.Size() + } + if a.punits[ix].PkgID != a.punits[iy].PkgID { + return a.punits[ix].PkgID < a.punits[iy].PkgID + } + return a.punits[ix].PunitID < a.punits[iy].PunitID + }) + reserve := make([]cpuset.CPUSet, 0, len(tierA)) + for _, i := range tierA { + reserve = append(reserve, states[i].free) + log.Debugf("pct: hpReserveCpus tier=A punit=%d/%d room=%d free=%s", + a.punits[i].PkgID, a.punits[i].PunitID, states[i].room, states[i].free) + } + return reserve } - // Tier B: aggregate per package; pick the package whose - // punits together have the most room (and free CPUs). + // Tier B: aggregate per package; every package whose punits + // together can host the request, best first. Package unions + // are disjoint by construction. if requested > 0 { type pkgAgg struct { room int @@ -824,37 +843,36 @@ func (a *Allocator) hpReserveCpus(free cpuset.CPUSet, excludeBln cpuset.CPUSet, pkgIDs := make([]int, 0, len(agg)) for id, e := range agg { e.freeN = e.free.Size() + if e.room < requested || e.freeN < requested { + continue + } pkgIDs = append(pkgIDs, id) } - sort.Ints(pkgIDs) // deterministic tie-break order - bestPkg := -1 - bestPkgRoom := 0 - bestPkgFree := -1 - for _, id := range pkgIDs { - e := agg[id] - if e.room < requested { - continue + sort.Slice(pkgIDs, func(x, y int) bool { + ex, ey := agg[pkgIDs[x]], agg[pkgIDs[y]] + if ex.room != ey.room { + return ex.room > ey.room } - if e.freeN < requested { - continue + if ex.freeN != ey.freeN { + return ex.freeN > ey.freeN } - if e.room > bestPkgRoom || (e.room == bestPkgRoom && e.freeN > bestPkgFree) { - bestPkg = id - bestPkgRoom = e.room - bestPkgFree = e.freeN + return pkgIDs[x] < pkgIDs[y] + }) + if len(pkgIDs) > 0 { + reserve := make([]cpuset.CPUSet, 0, len(pkgIDs)) + for _, id := range pkgIDs { + reserve = append(reserve, agg[id].free) + log.Debugf("pct: hpReserveCpus tier=B pkg=%d room=%d free=%s", + id, agg[id].room, agg[id].free) } - } - if bestPkg >= 0 { - log.Debugf("pct: hpReserveCpus tier=B pkg=%d room=%d free=%s", - bestPkg, bestPkgRoom, agg[bestPkg].free) - return agg[bestPkg].free + return reserve } } // Tier C is never taken: do not hint across packages. log.Debugf("pct: hpReserveCpus tier=none (no punit or package has %d HP room with %d free CPUs)", requested, free.Size()) - return cpuset.New() + return nil } // classClosID returns the CLOS ID that the named cpuClass maps to, @@ -892,8 +910,9 @@ func virtDevSstClosHint(closID int) string { // - Class has an explicit CLOS plan (assoc-only or managed): Prefer // CLOS-member CPUs. // - Class is currently classified HP: Prefer hpReserveCpus -// (best-fit punit; same-package union as fallback), and also -// CLOS-member CPUs. No cross-package hint is ever emitted. +// (per-punit candidates best first, same-package unions as +// fallback), and also CLOS-member CPUs. No cross-package hint is +// ever emitted. // - Class is not HP and at least one HP class exists: Avoid // hpInUseCpus (punits currently hosting HP work). func (a *Allocator) Hints(intent types.AllocationIntent) types.AllocationHints { @@ -904,17 +923,18 @@ func (a *Allocator) Hints(intent types.AllocationIntent) types.AllocationHints { if closID, ok := a.classClosID(intent.ClassName); ok { closCpus := a.closCpus(closID) - if !closCpus.IsEmpty() { + freeClosCpus := closCpus.Intersection(intent.FreeCpus) + if !freeClosCpus.IsEmpty() && freeClosCpus.Size() >= intent.RequestedCount { out.Prefer = append(out.Prefer, types.CpuPreference{ Name: virtDevSstClosHint(closID), - Cpus: closCpus, + Cpus: []cpuset.CPUSet{freeClosCpus}, }) } } if a.classIsHighPriority(intent.ClassName) { reserve := a.hpReserveCpus(intent.FreeCpus, intent.CurrentCpus, intent.RequestedCount) - if !reserve.IsEmpty() { + if len(reserve) > 0 { out.Prefer = append(out.Prefer, types.CpuPreference{ Name: virtDevSstHpReserveHint, Cpus: reserve, @@ -928,7 +948,7 @@ func (a *Allocator) Hints(intent types.AllocationIntent) types.AllocationHints { if !inUse.IsEmpty() { out.Avoid = append(out.Avoid, types.CpuPreference{ Name: virtDevSstHpInUseHint, - Cpus: inUse, + Cpus: []cpuset.CPUSet{inUse}, }) } } diff --git a/pkg/resmgr/cpuclass/internal/pct/pct_test.go b/pkg/resmgr/cpuclass/internal/pct/pct_test.go index 61df8bd37..007b19730 100644 --- a/pkg/resmgr/cpuclass/internal/pct/pct_test.go +++ b/pkg/resmgr/cpuclass/internal/pct/pct_test.go @@ -326,8 +326,8 @@ func TestPctHintsAssocOnlyPreferClosCpus(t *testing.T) { t.Errorf("Prefer[0].Name = %q, want %q", got.Prefer[0].Name, virtDevSstClosHint(1)) } want := cpuset.MustParse("2-3") - if !got.Prefer[0].Cpus.Equals(want) { - t.Errorf("Prefer[0].Cpus = %s, want %s", got.Prefer[0].Cpus, want) + if !got.Prefer[0].Cpus[0].Equals(want) { + t.Errorf("Prefer[0].Cpus = %v, want %s", got.Prefer[0].Cpus, want) } if len(got.Avoid) != 0 { t.Errorf("assoc-only mode must not emit Avoid hints: %+v", got.Avoid) @@ -382,8 +382,8 @@ func TestPctHintsHighPriorityReserveAndClosCpus(t *testing.T) { t.Errorf("Prefer[1].Name = %q, want %q", got.Prefer[1].Name, virtDevSstHpReserveHint) } wantReserve := cpuset.MustParse("4-7") - if !got.Prefer[1].Cpus.Equals(wantReserve) { - t.Errorf("HP reserve = %s, want %s (largest-room package)", got.Prefer[1].Cpus, wantReserve) + if !got.Prefer[1].Cpus[0].Equals(wantReserve) { + t.Errorf("HP reserve = %v, want %s (largest-room package)", got.Prefer[1].Cpus, wantReserve) } // HP-class hints must NOT carry an Avoid (HP picks first). if len(got.Avoid) != 0 { @@ -439,8 +439,8 @@ func TestPctHintsManagedNonHpAvoidsHpInUse(t *testing.T) { t.Errorf("Avoid[0].Name = %q, want %q", got.Avoid[0].Name, virtDevSstHpInUseHint) } wantAvoid := cpuset.MustParse("0-3") // entire pkg0 - if !got.Avoid[0].Cpus.Equals(wantAvoid) { - t.Errorf("Avoid[0].Cpus = %s, want %s (pkg0 == HP-in-use package)", got.Avoid[0].Cpus, wantAvoid) + if !got.Avoid[0].Cpus[0].Equals(wantAvoid) { + t.Errorf("Avoid[0].Cpus = %v, want %s (pkg0 == HP-in-use package)", got.Avoid[0].Cpus, wantAvoid) } } @@ -481,15 +481,15 @@ func TestPctHintsAllowedBoundsResults(t *testing.T) { if len(got.Prefer) == 0 { t.Fatalf("Prefer empty, want at least closCpus hint") } - if !got.Prefer[0].Cpus.Equals(cpuset.MustParse("0")) { - t.Errorf("Prefer[0].Cpus = %s, want {0} (cpu 4 outside allowed)", got.Prefer[0].Cpus) + if !got.Prefer[0].Cpus[0].Equals(cpuset.MustParse("0")) { + t.Errorf("Prefer[0].Cpus = %v, want {0} (cpu 4 outside allowed)", got.Prefer[0].Cpus) } // HP reserve must come from a package whose free CPUs are // inside allowed; only pkg0 qualifies. if len(got.Prefer) >= 2 { want := cpuset.MustParse("1-3") - if !got.Prefer[1].Cpus.Equals(want) { - t.Errorf("HP reserve = %s, want %s (pkg0 free cpus inside allowed)", got.Prefer[1].Cpus, want) + if !got.Prefer[1].Cpus[0].Equals(want) { + t.Errorf("HP reserve = %v, want %s (pkg0 free cpus inside allowed)", got.Prefer[1].Cpus, want) } } } @@ -556,7 +556,7 @@ func TestPctHints_HpRoomTierAPunitWins(t *testing.T) { var reserve cpuset.CPUSet for _, p := range got.Prefer { if p.Name == virtDevSstHpReserveHint { - reserve = p.Cpus + reserve = p.Cpus[0] } } if reserve.IsEmpty() { @@ -607,7 +607,7 @@ func TestPctHints_HpRoomTierBSamePackage(t *testing.T) { var reserve cpuset.CPUSet for _, p := range got.Prefer { if p.Name == virtDevSstHpReserveHint { - reserve = p.Cpus + reserve = p.Cpus[0] } } if reserve.IsEmpty() { @@ -693,8 +693,8 @@ func TestPctHints_HpInUseIsPunitGranular(t *testing.T) { } // Must be punit-0 (cpus 0-3) ONLY, not all of pkg0 (0-7). want := cpuset.MustParse("0-3") - if !got.Avoid[0].Cpus.Equals(want) { - t.Errorf("Avoid = %s, want %s (punit-0 only, not full pkg0)", got.Avoid[0].Cpus, want) + if !got.Avoid[0].Cpus[0].Equals(want) { + t.Errorf("Avoid = %v, want %s (punit-0 only, not full pkg0)", got.Avoid[0].Cpus, want) } } diff --git a/pkg/resmgr/cpuclass/internal/types/types.go b/pkg/resmgr/cpuclass/internal/types/types.go index b554beb63..842a39d63 100644 --- a/pkg/resmgr/cpuclass/internal/types/types.go +++ b/pkg/resmgr/cpuclass/internal/types/types.go @@ -71,11 +71,14 @@ type AllocationIntent struct { RequestedCount int } -// CpuPreference is a named CPU set carrying a single placement -// preference (prefer or avoid depending on the slice it appears in). +// CpuPreference is a named, priority-ordered list of candidate CPU +// sets carrying a single placement preference (prefer or avoid +// depending on the slice it appears in). Cpus lists candidate sets +// ordered from most to least preferred: an allocation should honor +// the first candidate it can and ignore others. type CpuPreference struct { Name string - Cpus cpuset.CPUSet + Cpus []cpuset.CPUSet } // AllocationHints carries technology-agnostic placement preferences