@@ -20,6 +20,7 @@ import (
2020 "context"
2121 "fmt"
2222 "math"
23+ "sort"
2324
2425 corev1 "k8s.io/api/core/v1"
2526 "k8s.io/apimachinery/pkg/api/resource"
@@ -149,8 +150,7 @@ func (ge *GeneralEstimator) maxAvailableComponentSets(cluster *clusterv1alpha1.C
149150 }
150151
151152 if features .FeatureGate .Enabled (features .CustomizedClusterResourceModeling ) && len (cluster .Status .ResourceSummary .AllocatableModelings ) > 0 {
152- num , err := getMaximumSetsBasedOnResourceModels (cluster , components )
153- if err != nil {
153+ if num , err := getMaximumSetsBasedOnResourceModels (cluster , components , podBound ); err != nil {
154154 klog .Warningf ("Failed to get maximum sets based on resource models, skipping: %v" , err )
155155 } else if num < maxSets {
156156 maxSets = num
@@ -160,13 +160,197 @@ func (ge *GeneralEstimator) maxAvailableComponentSets(cluster *clusterv1alpha1.C
160160 return int32 (maxSets ) // #nosec G115: integer overflow conversion int64 -> int32
161161}
162162
163- // getMaximumSetsBasedOnResourceModels is a placeholder for future implementation.
164- // It should refine the maximum sets based on cluster resource models, similar
165- // to getMaximumReplicasBasedOnResourceModels but adapted to full component sets.
166- func getMaximumSetsBasedOnResourceModels (_ * clusterv1alpha1.Cluster , _ []workv1alpha2.Component ) (int64 , error ) {
167- // TODO: implement logic based on cluster.Spec.ResourceModels
168- // For now, just return MaxInt64 so it never reduces the upper bound.
169- return math .MaxInt64 , nil
163+ // getMaximumSetsBasedOnResourceModels computes the maximum number of full sets that can be
164+ // placed on a cluster using the cluster's ResourceModels. It expands one set into individual component replicas
165+ // and performs a first-fit-decreasing placement onto model-grade nodes.
166+ // `upperBound` caps the search. We can this using the podBound (allowedPods / podsPerSet)
167+ func getMaximumSetsBasedOnResourceModels (
168+ cluster * clusterv1alpha1.Cluster ,
169+ components []workv1alpha2.Component ,
170+ upperBound int64 ,
171+ ) (int64 , error ) {
172+ if upperBound <= 0 {
173+ return 0 , nil
174+ }
175+
176+ // Build model nodes from Spec.ResourceModels and Status.AllocatableModelings
177+ nodes , err := buildModelNodes (cluster )
178+ if err != nil {
179+ return - 1 , err
180+ }
181+ if len (nodes ) == 0 {
182+ return 0 , nil
183+ }
184+
185+ // Expand one set into per-replica resource maps
186+ oneSet := expandReplicasOneSet (components )
187+ if len (oneSet ) == 0 {
188+ // No pods in a set -> nothing to schedule under models
189+ return 0 , nil
190+ }
191+
192+ // Binary search on #sets within [0, upperBound]
193+ lo , hi := int64 (0 ), upperBound
194+ for lo < hi {
195+ mid := (lo + hi + 1 ) / 2
196+ if modelsFeasible (mid , oneSet , nodes ) {
197+ lo = mid
198+ } else {
199+ hi = mid - 1
200+ }
201+ }
202+ return lo , nil
203+ }
204+
205+ // ----- Models helpers -----
206+
207+ // modelNode holds remaining capacity for a given node across all resource types
208+ type modelNode struct {
209+ cap map [corev1.ResourceName ]int64
210+ }
211+
212+ // buildModelNodes constructs identical nodes for each model grade using its Min vector,
213+ // repeated `AllocatableModelings[i].Count` times.
214+ func buildModelNodes (cluster * clusterv1alpha1.Cluster ) ([]modelNode , error ) {
215+ if len (cluster .Spec .ResourceModels ) == 0 {
216+ return nil , fmt .Errorf ("resource model is inapplicable as no grades are defined" )
217+ }
218+ // Convert Spec.ResourceModels to a map of resource -> []MinByGrade
219+ minMap := convertToResourceModelsMinMap (cluster .Spec .ResourceModels )
220+
221+ // Build nodes for each grade index i
222+ var nodes []modelNode
223+ for i := 0 ; i < len (cluster .Spec .ResourceModels ); i ++ {
224+ if i >= len (cluster .Status .ResourceSummary .AllocatableModelings ) {
225+ // Shouldn’t happen — status is malformed
226+ return nil , fmt .Errorf ("resource model/status mismatch: %d grades in spec, %d in status" ,
227+ len (cluster .Spec .ResourceModels ), len (cluster .Status .ResourceSummary .AllocatableModelings ))
228+ }
229+
230+ count := cluster .Status .ResourceSummary .AllocatableModelings [i ].Count
231+ if count == 0 {
232+ continue
233+ }
234+
235+ // Capacity vector for this grade = Min boundary of each resource at grade i (normalized)
236+ capTemplate := make (map [corev1.ResourceName ]int64 , len (minMap ))
237+ for resName , mins := range minMap {
238+ if i >= len (mins ) {
239+ // Model shape mismatch; treat as missing resource for this grade
240+ return nil , fmt .Errorf ("resource model is inapplicable as missing resource %q in grade %d" , string (resName ), i )
241+ }
242+ capTemplate [resName ] = quantityAsInt64 (mins [i ])
243+ }
244+
245+ // Append `count` identical nodes of this grade
246+ for n := 0 ; n < count ; n ++ {
247+ // Copy capTemplate to each node
248+ capCopy := make (map [corev1.ResourceName ]int64 , len (capTemplate ))
249+ for k , v := range capTemplate {
250+ capCopy [k ] = v
251+ }
252+ nodes = append (nodes , modelNode {cap : capCopy })
253+ }
254+ }
255+ return nodes , nil
256+ }
257+
258+ // expandReplicasOneSet flattens components into a slice of per-replica demand maps
259+ func expandReplicasOneSet (components []workv1alpha2.Component ) []map [corev1.ResourceName ]int64 {
260+ var reps []map [corev1.ResourceName ]int64
261+ for _ , c := range components {
262+ if c .ReplicaRequirements == nil || c .ReplicaRequirements .ResourceRequest == nil {
263+ continue
264+ }
265+ // Build normalized demand once per component
266+ base := make (map [corev1.ResourceName ]int64 , len (c .ReplicaRequirements .ResourceRequest ))
267+ for name , qty := range c .ReplicaRequirements .ResourceRequest {
268+ base [name ] = quantityAsInt64 (qty )
269+ }
270+ // Repeat for the number of replicas in the component
271+ for i := int32 (0 ); i < c .Replicas ; i ++ {
272+ m := make (map [corev1.ResourceName ]int64 , len (base ))
273+ for k , v := range base {
274+ m [k ] = v
275+ }
276+ reps = append (reps , m )
277+ }
278+ }
279+ return reps
280+ }
281+
282+ // modelsFeasible checks if the given # of copies of `oneSet` can be placed onto `nodes`
283+ // using first-fit decreasing to for packing
284+ func modelsFeasible (sets int64 , oneSet []map [corev1.ResourceName ]int64 , nodes []modelNode ) bool {
285+ work := make ([]modelNode , len (nodes ))
286+ for i := range nodes {
287+ capCopy := make (map [corev1.ResourceName ]int64 , len (nodes [i ].cap ))
288+ for k , v := range nodes [i ].cap {
289+ capCopy [k ] = v
290+ }
291+ work [i ] = modelNode {cap : capCopy }
292+ }
293+
294+ // Build replicas list: sets x oneSet
295+ total := int (sets ) * len (oneSet )
296+ replicas := make ([]map [corev1.ResourceName ]int64 , 0 , total )
297+ for i := int64 (0 ); i < sets ; i ++ {
298+ replicas = append (replicas , oneSet ... )
299+ }
300+
301+ // Sort replicas by a size heuristic (e.g. sum of demands), helps greedy packing
302+ sort .Slice (replicas , func (i , j int ) bool {
303+ return demandScore (replicas [i ]) > demandScore (replicas [j ])
304+ })
305+
306+ // Greedy first-fit
307+ for _ , r := range replicas {
308+ placed := false
309+ for i := range work {
310+ if fitsAll (r , work [i ].cap ) {
311+ consumeAll (r , work [i ].cap )
312+ placed = true
313+ break
314+ }
315+ }
316+ if ! placed {
317+ return false
318+ }
319+ }
320+ return true
321+ }
322+
323+ // demandScore is used to rank replicas in terms of their size and difficulty to schedule
324+ // Currently uses sum of values
325+ func demandScore (m map [corev1.ResourceName ]int64 ) int64 {
326+ var s int64
327+ for _ , v := range m {
328+ if v > 0 {
329+ s += v
330+ }
331+ }
332+ return s
333+ }
334+
335+ func fitsAll (demand map [corev1.ResourceName ]int64 , capacity map [corev1.ResourceName ]int64 ) bool {
336+ for k , req := range demand {
337+ if req <= 0 {
338+ continue
339+ }
340+ if capacity [k ] < req {
341+ return false
342+ }
343+ }
344+ return true
345+ }
346+
347+ func consumeAll (demand map [corev1.ResourceName ]int64 , capacity map [corev1.ResourceName ]int64 ) {
348+ for k , req := range demand {
349+ if req <= 0 {
350+ continue
351+ }
352+ capacity [k ] -= req
353+ }
170354}
171355
172356// podsInSet computes the total number of pods in the CRD
0 commit comments