Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 4 additions & 72 deletions cmd/plugins/balloons/policy/balloons-policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -1702,79 +1702,11 @@ func (p *balloons) validateConfig(bpoptions *BalloonsOptions) error {
return balloonsError("schedulingClass(es) defined in balloonTypes but missing from schedulingClasses: %v", undefinedSchedulingClasses)
}
// Validate CPUClasses.
cpuClassNames := map[string]struct{}{}
pctManaged := map[string]string{} // class name -> "high"/"low"
pctAssocOnly := map[string]int{} // class name -> CLOS id
for _, cc := range bpoptions.CPUClasses {
if cc.Name == "" {
return balloonsError("missing or empty name in a cpuClasses entry")
}
if _, dup := cpuClassNames[cc.Name]; dup {
return balloonsError("duplicate cpuClasses name: %q", cc.Name)
}
cpuClassNames[cc.Name] = struct{}{}
// Validate PCT fields.
if cc.PctPriority != "" && cc.SstClosID != nil {
return balloonsError("cpuClass %q: pctPriority and sstClosID are mutually exclusive", cc.Name)
}
switch cc.PctPriority {
case "", "high", "low":
default:
return balloonsError("cpuClass %q: invalid pctPriority %q (allowed: \"high\", \"low\")", cc.Name, cc.PctPriority)
}
if cc.PctPriority != "" {
pctManaged[cc.Name] = cc.PctPriority
}
if cc.SstClosID != nil {
if *cc.SstClosID < 0 {
return balloonsError("cpuClass %q: sstClosID must be >= 0, got %d", cc.Name, *cc.SstClosID)
}
pctAssocOnly[cc.Name] = *cc.SstClosID
}
// pctMinFreq/pctMaxFreq only take effect in managed
// mode (pctPriority); they program the SST CLOS that
// balloons owns. With sstClosID the CLOS is
// pre-programmed by intel-speed-select/BIOS, and
// without any PCT field the cpuClass is not a PCT
// class at all. In both cases these fields are silent
// no-ops; reject them so users don't tweak values that
// have no effect.
if cc.PctMinFreq != 0 || cc.PctMaxFreq != 0 {
switch {
case cc.SstClosID != nil:
return balloonsError("cpuClass %q: pctMinFreq/pctMaxFreq require pctPriority (managed mode); they are incompatible with sstClosID, where the SST CLOS is pre-programmed by intel-speed-select/BIOS", cc.Name)
case cc.PctPriority == "":
return balloonsError("cpuClass %q: pctMinFreq/pctMaxFreq require pctPriority (managed mode); the cpuClass is currently not a PCT class", cc.Name)
}
}
// publishExtendedResource only makes sense for PCT
// classes -- the agent computes capacity from a PCT
// plan. Reject it on non-PCT classes so users don't
// expect a node-level resource that will never be
// published.
if cc.PublishExtendedResource && cc.PctPriority == "" && cc.SstClosID == nil {
return balloonsError("cpuClass %q: publishExtendedResource requires the cpuClass to be a PCT class (set pctPriority or sstClosID)", cc.Name)
}
}
if len(pctManaged) > 0 && len(pctAssocOnly) > 0 {
return balloonsError("mixing managed (pctPriority) and assoc-only (sstClosID) PCT cpuClasses is not allowed: managed=%v, assocOnly=%v", pctManaged, pctAssocOnly)
}
if len(pctManaged) > 0 {
hpClasses, lpClasses := []string{}, []string{}
for name, prio := range pctManaged {
if prio == "high" {
hpClasses = append(hpClasses, name)
} else {
lpClasses = append(lpClasses, name)
}
}
if len(hpClasses) > 1 {
return balloonsError("at most one managed PCT cpuClass with pctPriority=high allowed, got %d: %v", len(hpClasses), hpClasses)
}
if len(lpClasses) > 1 {
return balloonsError("at most one managed PCT cpuClass with pctPriority=low allowed, got %d: %v", len(lpClasses), lpClasses)
}
cpuClassNames, _, _, err := cfgapi.ValidateCPUClasses(bpoptions.CPUClasses)
if err != nil {
return err
}

// Verify that cpuClass references in balloon types are
// defined in cpuClasses. Using the legacy control.cpu.classes
// configuration is discouraged and it is possibly out-of-date
Expand Down
19 changes: 9 additions & 10 deletions config/crd/bases/config.nri_balloonspolicies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -809,14 +809,13 @@ spec:
publishExtendedResource:
description: |-
PublishExtendedResource opts this CPU class into publishing
a node-level extended resource named
"cpuclass.balloons.nri.io/<class-name>" whose value reflects
the number of logical CPUs that the balloons policy is
currently able to route into this class on the node. The
scheduler can then bin-pack/spread balloons by adding the
same resource to pod requests, avoiding HP-CPU
over-subscription on a single node. Has effect only when
the class also carries PctPriority or SstClosID. Experimental.
a node-level extended resource, using a policy chosen name,
whose value reflects the number of logical CPUs that the policy
is currently able to route into this class on the node. The
scheduler can then bin-pack/spread containers by adding the
same resource to pod requests, avoiding HP-CPU over-subscription
on a single node. Has effect only when the class also carries
PctPriority or SstClosID. Experimental.
type: boolean
sstClosID:
description: |-
Expand All @@ -832,8 +831,8 @@ spec:
turboPriority:
description: |-
TurboPriority controls exclusive turbo frequency access.
Among CPU classes with active balloons, only the class with
the highest turboPriority gets the symbolic frequency "turbo"
Among CPU classes with active CPU allocations, only the class
with the highest turboPriority gets the symbolic frequency "turbo"
resolved to the actual turbo frequency. All other classes get
"turbo" resolved to the base frequency instead.
If all classes have turboPriority 0 (default), every class
Expand Down
19 changes: 9 additions & 10 deletions deployment/helm/balloons/crds/config.nri_balloonspolicies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -809,14 +809,13 @@ spec:
publishExtendedResource:
description: |-
PublishExtendedResource opts this CPU class into publishing
a node-level extended resource named
"cpuclass.balloons.nri.io/<class-name>" whose value reflects
the number of logical CPUs that the balloons policy is
currently able to route into this class on the node. The
scheduler can then bin-pack/spread balloons by adding the
same resource to pod requests, avoiding HP-CPU
over-subscription on a single node. Has effect only when
the class also carries PctPriority or SstClosID. Experimental.
a node-level extended resource, using a policy chosen name,
whose value reflects the number of logical CPUs that the policy
is currently able to route into this class on the node. The
scheduler can then bin-pack/spread containers by adding the
same resource to pod requests, avoiding HP-CPU over-subscription
on a single node. Has effect only when the class also carries
PctPriority or SstClosID. Experimental.
type: boolean
sstClosID:
description: |-
Expand All @@ -832,8 +831,8 @@ spec:
turboPriority:
description: |-
TurboPriority controls exclusive turbo frequency access.
Among CPU classes with active balloons, only the class with
the highest turboPriority gets the symbolic frequency "turbo"
Among CPU classes with active CPU allocations, only the class
with the highest turboPriority gets the symbolic frequency "turbo"
resolved to the actual turbo frequency. All other classes get
"turbo" resolved to the base frequency instead.
If all classes have turboPriority 0 (default), every class
Expand Down
1 change: 1 addition & 0 deletions pkg/apis/config/v1alpha1/resmgr/policy/balloons/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const (

var (
CPUTopologyLevelCount = policy.CPUTopologyLevelCount
ValidateCPUClasses = policy.ValidateCPUClasses
)

// +kubebuilder:object:generate=true
Expand Down
116 changes: 106 additions & 10 deletions pkg/apis/config/v1alpha1/resmgr/policy/cpuclass.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@

package policy

import (
"errors"
"fmt"
)

// CPUClass specifies CPU frequency, C-state, and turbo attributes
// for a CPU class.
// +k8s:deepcopy-gen=true
Expand Down Expand Up @@ -48,8 +53,8 @@ type CPUClass struct {
// Example: ["C4", "C6", "C8", "C10"]
DisabledCstates []string `json:"disabledCstates,omitempty"`
// TurboPriority controls exclusive turbo frequency access.
// Among CPU classes with active balloons, only the class with
// the highest turboPriority gets the symbolic frequency "turbo"
// Among CPU classes with active CPU allocations, only the class
// with the highest turboPriority gets the symbolic frequency "turbo"
// resolved to the actual turbo frequency. All other classes get
// "turbo" resolved to the base frequency instead.
// If all classes have turboPriority 0 (default), every class
Expand Down Expand Up @@ -86,13 +91,104 @@ type CPUClass struct {
// Same caveat as PctMinFreq.
PctMaxFreq Frequency `json:"pctMaxFreq,omitempty"`
// PublishExtendedResource opts this CPU class into publishing
// a node-level extended resource named
// "cpuclass.balloons.nri.io/<class-name>" whose value reflects
// the number of logical CPUs that the balloons policy is
// currently able to route into this class on the node. The
// scheduler can then bin-pack/spread balloons by adding the
// same resource to pod requests, avoiding HP-CPU
// over-subscription on a single node. Has effect only when
// the class also carries PctPriority or SstClosID. Experimental.
// a node-level extended resource, using a policy chosen name,
// whose value reflects the number of logical CPUs that the policy
// is currently able to route into this class on the node. The
// scheduler can then bin-pack/spread containers by adding the
// same resource to pod requests, avoiding HP-CPU over-subscription
// on a single node. Has effect only when the class also carries
// PctPriority or SstClosID. Experimental.
PublishExtendedResource bool `json:"publishExtendedResource,omitempty"`
}

func (cc *CPUClass) Validate() error {
if cc.Name == "" {
return errors.New("missing or empty name in a cpuClasses entry")
}
// Validate PCT fields.
if cc.PctPriority != "" && cc.SstClosID != nil {
return fmt.Errorf("cpuClass %q: pctPriority and sstClosID are mutually exclusive", cc.Name)
}
switch cc.PctPriority {
case "", "high", "low":
default:
return fmt.Errorf("cpuClass %q: invalid pctPriority %q (allowed: \"high\", \"low\")", cc.Name, cc.PctPriority)
}
if cc.SstClosID != nil {
if *cc.SstClosID < 0 {
return fmt.Errorf("cpuClass %q: sstClosID must be >= 0, got %d", cc.Name, *cc.SstClosID)
}
}
// pctMinFreq/pctMaxFreq only take effect in managed
// mode (pctPriority); they program the SST CLOS that
// policies own. With sstClosID the CLOS is
// pre-programmed by intel-speed-select/BIOS, and
// without any PCT field the cpuClass is not a PCT
// class at all. In both cases these fields are silent
// no-ops; reject them so users don't tweak values that
// have no effect.
if cc.PctMinFreq != 0 || cc.PctMaxFreq != 0 {
switch {
case cc.SstClosID != nil:
return fmt.Errorf("cpuClass %q: pctMinFreq/pctMaxFreq require pctPriority (managed mode); they are incompatible with sstClosID, where the SST CLOS is pre-programmed by intel-speed-select/BIOS", cc.Name)
case cc.PctPriority == "":
return fmt.Errorf("cpuClass %q: pctMinFreq/pctMaxFreq require pctPriority (managed mode); the cpuClass is currently not a PCT class", cc.Name)
}
}
// publishExtendedResource only makes sense for PCT
// classes -- the agent computes capacity from a PCT
// plan. Reject it on non-PCT classes so users don't
// expect a node-level resource that will never be
// published.
if cc.PublishExtendedResource && cc.PctPriority == "" && cc.SstClosID == nil {
return fmt.Errorf("cpuClass %q: publishExtendedResource requires the cpuClass to be a PCT class (set pctPriority or sstClosID)", cc.Name)
}

return nil
}

func ValidateCPUClasses(cpuClasses []*CPUClass) (cpuClassNames map[string]struct{}, pctManaged map[string]string, pctAssocOnly map[string]int, err error) {
cpuClassNames = map[string]struct{}{}
pctManaged = map[string]string{}
pctAssocOnly = map[string]int{}

for _, cc := range cpuClasses {
if err = cc.Validate(); err != nil {
return nil, nil, nil, err
}

if _, dup := cpuClassNames[cc.Name]; dup {
return nil, nil, nil, fmt.Errorf("duplicate cpuClasses name: %q", cc.Name)
}

switch {
case cc.PctPriority != "":
pctManaged[cc.Name] = cc.PctPriority
case cc.SstClosID != nil:
pctAssocOnly[cc.Name] = *cc.SstClosID
}
cpuClassNames[cc.Name] = struct{}{}
}

if len(pctManaged) > 0 && len(pctAssocOnly) > 0 {
return nil, nil, nil, fmt.Errorf("mixing managed (pctPriority) and assoc-only (sstClosID) PCT cpuClasses is not allowed: managed=%v, assocOnly=%v", pctManaged, pctAssocOnly)
}
if len(pctManaged) > 0 {
hpClasses, lpClasses := []string{}, []string{}
for name, prio := range pctManaged {
if prio == "high" {
hpClasses = append(hpClasses, name)
} else {
lpClasses = append(lpClasses, name)
}
}
if len(hpClasses) > 1 {
return nil, nil, nil, fmt.Errorf("at most one managed PCT cpuClass with pctPriority=high allowed, got %d: %v", len(hpClasses), hpClasses)
}
if len(lpClasses) > 1 {
return nil, nil, nil, fmt.Errorf("at most one managed PCT cpuClass with pctPriority=low allowed, got %d: %v", len(lpClasses), lpClasses)
}
}

return cpuClassNames, pctManaged, pctAssocOnly, nil
}
15 changes: 15 additions & 0 deletions pkg/resmgr/cpuclass/cpuclass.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ type Handler struct {
// defs maps synthetic class name -> resolved class definition.
// Populated by SetClassDef calls from the cpufreq allocator.
defs map[string]types.ClassDef
// classNames list all known/configured class names
classNames map[string]struct{}
// cpuClass maps cpu id -> synthetic class name. Value "" means
// "explicitly assigned to no class". Absent CPUs are unmanaged.
cpuClass map[int]string
Expand Down Expand Up @@ -155,9 +157,22 @@ func (h *Handler) Configure(spec ConfigSpec) error {
if err := h.pct.Configure(spec.Classes, spec.Allowed); err != nil {
return fmt.Errorf("cpuclass: pct configure: %w", err)
}

h.classNames = map[string]struct{}{}
for _, cls := range spec.Classes {
h.classNames[cls.Name] = struct{}{}
}

return nil
}

// IsKnownClass returns true if the named CPU class exists. Always returns
// false before Configure() has been called at least once.
func (h *Handler) IsKnownClass(name string) bool {
_, ok := h.classNames[name]
return ok
}

// SetClassDef records a class definition keyed by its synthetic
// name. If the definition materially changes, every CPU currently
// assigned to that synthetic class is marked dirty. Implements the
Expand Down
3 changes: 2 additions & 1 deletion pkg/resmgr/cpuclass/internal/cpufreq/sysfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,9 @@ func (w *Writer) Enforce(class string, def types.ClassDef, cpus []int) error {

var firstErr error
for _, cpu := range cpus {
state := w.lastWritten[cpu]
log.Debugf("enforcing cpu frequency from class %q on cpu %d", class, cpu)

state := w.lastWritten[cpu]
if min > 0 && (!state.hasMin || state.min != min) {
log.Debugf("enforcing cpu frequency min %d from class %q on cpu %d", min, class, cpu)
if err := w.callSetMin(cpu, int(min)); err != nil {
Expand Down
6 changes: 3 additions & 3 deletions pkg/resmgr/cpuclass/internal/pct/pct.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ func (a *Allocator) Configure(classes []*policyapi.CPUClass, allowed cpuset.CPUS
// defined). Leaving them on CLOS 0 inflates the SST-TF
// active-HP-core count on every punit and prevents bucket-0
// turbo selection on punits hosting both an HP and an LP
// balloon.
// allocations.
if lpClos != nil {
a.fallbackClos = *lpClos
log.Infof("pct: fallback CLOS for non-PCT CPUs set to %d (LP)", a.fallbackClos)
Expand Down Expand Up @@ -452,9 +452,9 @@ func (a *Allocator) Active() bool {
return a != nil && a.mode != pctModeDisabled
}

// freeClassCapacity returns the number of logical CPUs that can
// FreeClassCapacity returns the number of logical CPUs that can
// still be allocated to className, given that 'held' lists CPUs
// already consumed by some balloon on this node (any class).
// already consumed by some allocations on this node (any class).
//
// Same formula in managed and assoc-only modes:
// - HP class: sum over HP-eligible punits of
Expand Down
Loading