From a9a4ebc888760670b78e9bb3327c8efa033641f0 Mon Sep 17 00:00:00 2001 From: Krisztian Litkey Date: Mon, 3 Aug 2026 09:51:34 +0300 Subject: [PATCH 1/6] config/cpuclass: reword using more policy-neutral terms. Avoid using 'balloon' in cpuclass configuration comments / description. Use the more neutral 'policy' term. Signed-off-by: Krisztian Litkey --- .../bases/config.nri_balloonspolicies.yaml | 19 +++++++++---------- .../crds/config.nri_balloonspolicies.yaml | 19 +++++++++---------- .../config/v1alpha1/resmgr/policy/cpuclass.go | 19 +++++++++---------- 3 files changed, 27 insertions(+), 30 deletions(-) diff --git a/config/crd/bases/config.nri_balloonspolicies.yaml b/config/crd/bases/config.nri_balloonspolicies.yaml index 011760141..077067512 100644 --- a/config/crd/bases/config.nri_balloonspolicies.yaml +++ b/config/crd/bases/config.nri_balloonspolicies.yaml @@ -809,14 +809,13 @@ spec: publishExtendedResource: description: |- PublishExtendedResource opts this CPU class into publishing - a node-level extended resource named - "cpuclass.balloons.nri.io/" 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: |- @@ -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 diff --git a/deployment/helm/balloons/crds/config.nri_balloonspolicies.yaml b/deployment/helm/balloons/crds/config.nri_balloonspolicies.yaml index 011760141..077067512 100644 --- a/deployment/helm/balloons/crds/config.nri_balloonspolicies.yaml +++ b/deployment/helm/balloons/crds/config.nri_balloonspolicies.yaml @@ -809,14 +809,13 @@ spec: publishExtendedResource: description: |- PublishExtendedResource opts this CPU class into publishing - a node-level extended resource named - "cpuclass.balloons.nri.io/" 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: |- @@ -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 diff --git a/pkg/apis/config/v1alpha1/resmgr/policy/cpuclass.go b/pkg/apis/config/v1alpha1/resmgr/policy/cpuclass.go index f373ecf10..498fd65fb 100644 --- a/pkg/apis/config/v1alpha1/resmgr/policy/cpuclass.go +++ b/pkg/apis/config/v1alpha1/resmgr/policy/cpuclass.go @@ -48,8 +48,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 @@ -86,13 +86,12 @@ 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/" 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"` } From 8d4a5217a19b75144859795fbfa3ac8eddc71616 Mon Sep 17 00:00:00 2001 From: Krisztian Litkey Date: Wed, 5 Aug 2026 13:40:16 +0300 Subject: [PATCH 2/6] pct: fix old/incorrect function name in docstring comment. Signed-off-by: Krisztian Litkey --- pkg/resmgr/cpuclass/internal/pct/pct.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/resmgr/cpuclass/internal/pct/pct.go b/pkg/resmgr/cpuclass/internal/pct/pct.go index 36bc173c7..4398e084d 100644 --- a/pkg/resmgr/cpuclass/internal/pct/pct.go +++ b/pkg/resmgr/cpuclass/internal/pct/pct.go @@ -452,7 +452,7 @@ 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). // From d460e433233cf667757be046791a9a0b336f5b87 Mon Sep 17 00:00:00 2001 From: Krisztian Litkey Date: Wed, 5 Aug 2026 13:41:31 +0300 Subject: [PATCH 3/6] pct: use more policy agnostic terms instead of balloons. Signed-off-by: Krisztian Litkey --- pkg/resmgr/cpuclass/internal/pct/pct.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/resmgr/cpuclass/internal/pct/pct.go b/pkg/resmgr/cpuclass/internal/pct/pct.go index 4398e084d..c40c9bb7b 100644 --- a/pkg/resmgr/cpuclass/internal/pct/pct.go +++ b/pkg/resmgr/cpuclass/internal/pct/pct.go @@ -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) @@ -454,7 +454,7 @@ func (a *Allocator) Active() bool { // 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 From 883fe5a510d87a62531d5d9a23f941b21d12d727 Mon Sep 17 00:00:00 2001 From: Krisztian Litkey Date: Wed, 29 Jul 2026 14:45:59 +0300 Subject: [PATCH 4/6] config,balloons: split out common CPU class validation. Signed-off-by: Krisztian Litkey --- .../balloons/policy/balloons-policy.go | 76 +-------------- .../v1alpha1/resmgr/policy/balloons/config.go | 1 + .../config/v1alpha1/resmgr/policy/cpuclass.go | 97 +++++++++++++++++++ 3 files changed, 102 insertions(+), 72 deletions(-) diff --git a/cmd/plugins/balloons/policy/balloons-policy.go b/cmd/plugins/balloons/policy/balloons-policy.go index 0165526f3..9d3ceb279 100644 --- a/cmd/plugins/balloons/policy/balloons-policy.go +++ b/cmd/plugins/balloons/policy/balloons-policy.go @@ -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 diff --git a/pkg/apis/config/v1alpha1/resmgr/policy/balloons/config.go b/pkg/apis/config/v1alpha1/resmgr/policy/balloons/config.go index 4b3eec2c0..0238a2a1a 100644 --- a/pkg/apis/config/v1alpha1/resmgr/policy/balloons/config.go +++ b/pkg/apis/config/v1alpha1/resmgr/policy/balloons/config.go @@ -75,6 +75,7 @@ const ( var ( CPUTopologyLevelCount = policy.CPUTopologyLevelCount + ValidateCPUClasses = policy.ValidateCPUClasses ) // +kubebuilder:object:generate=true diff --git a/pkg/apis/config/v1alpha1/resmgr/policy/cpuclass.go b/pkg/apis/config/v1alpha1/resmgr/policy/cpuclass.go index 498fd65fb..3e4258fda 100644 --- a/pkg/apis/config/v1alpha1/resmgr/policy/cpuclass.go +++ b/pkg/apis/config/v1alpha1/resmgr/policy/cpuclass.go @@ -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 @@ -95,3 +100,95 @@ type CPUClass struct { // 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 +} From 069c5b457354bafff245c4f561b28701d50ca118 Mon Sep 17 00:00:00 2001 From: Krisztian Litkey Date: Mon, 3 Aug 2026 08:28:59 +0300 Subject: [PATCH 5/6] cpuclass: add IsKnownClass Signed-off-by: Krisztian Litkey --- pkg/resmgr/cpuclass/cpuclass.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pkg/resmgr/cpuclass/cpuclass.go b/pkg/resmgr/cpuclass/cpuclass.go index 66ee6b22d..34548cea5 100644 --- a/pkg/resmgr/cpuclass/cpuclass.go +++ b/pkg/resmgr/cpuclass/cpuclass.go @@ -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 @@ -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 From 9582b9c2deec9aba0d37e92f898c87eefbc1528e Mon Sep 17 00:00:00 2001 From: Krisztian Litkey Date: Thu, 6 Aug 2026 09:16:16 +0300 Subject: [PATCH 6/6] cpufreq: always (debug-)log class enforcement intent. Always debug log cpufreq class enforcement intent. This helps with log-based verification in e2e tests with repeated class assignments. Other otherwise some assignment of a cpufreq-only class could be omitted from logs if it does not change cpufreq state for a CPU. Signed-off-by: Krisztian Litkey --- pkg/resmgr/cpuclass/internal/cpufreq/sysfs.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/resmgr/cpuclass/internal/cpufreq/sysfs.go b/pkg/resmgr/cpuclass/internal/cpufreq/sysfs.go index cf91491be..327ef088f 100644 --- a/pkg/resmgr/cpuclass/internal/cpufreq/sysfs.go +++ b/pkg/resmgr/cpuclass/internal/cpufreq/sysfs.go @@ -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 {