@@ -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"
@@ -54,7 +55,7 @@ func (ge *GeneralEstimator) MaxAvailableReplicas(_ context.Context, clusters []*
5455}
5556
5657func (ge * GeneralEstimator ) maxAvailableReplicas (cluster * clusterv1alpha1.Cluster , replicaRequirements * workv1alpha2.ReplicaRequirements ) int32 {
57- //Note: resourceSummary must be deep-copied before using in the function to avoid modifying the original data structure.
58+ // Note: resourceSummary must be deep-copied before using in the function to avoid modifying the original data structure.
5859 resourceSummary := cluster .Status .ResourceSummary .DeepCopy ()
5960 if resourceSummary == nil {
6061 return 0
@@ -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,253 @@ 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
165+ // replica kinds (demand + count) and performs a first-fit-decreasing placement onto model-grade nodes.
166+ // `upperBound` caps the search. We can set 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+ // Compressed one-set: per-kind (identical replicas grouped)
177+ oneSetKinds := expandKindsOneSet (components )
178+ if len (oneSetKinds ) == 0 {
179+ // If there are no pods to schedule, just return upperBound
180+ return upperBound , nil
181+ }
182+
183+ // Use cluster "available" totals (allocatable - allocated - allocating) for normalized scoring
184+ // This reflects what the cluster can actually accept now
185+ totals := availableResourceMap (cluster .Status .ResourceSummary )
186+
187+ for i := range oneSetKinds {
188+ oneSetKinds [i ].score = demandScoreNormalized (oneSetKinds [i ].dem , totals )
189+ }
190+ sort .Slice (oneSetKinds , func (i , j int ) bool {
191+ if oneSetKinds [i ].score == oneSetKinds [j ].score {
192+ return demandSum (oneSetKinds [i ].dem ) > demandSum (oneSetKinds [j ].dem )
193+ }
194+ return oneSetKinds [i ].score > oneSetKinds [j ].score
195+ })
196+
197+ //Build model nodes from Spec.ResourceModels and Status.AllocatableModelings
198+ nodes , err := buildModelNodes (cluster )
199+ if err != nil {
200+ return - 1 , err
201+ }
202+ if len (nodes ) == 0 {
203+ return 0 , nil
204+ }
205+
206+ var sets int64
207+ for sets < upperBound {
208+ if ! placeOneSet (oneSetKinds , nodes ) {
209+ break
210+ }
211+ sets ++
212+ }
213+ return sets , nil
214+ }
215+
216+ // placeOneSet attempts to place exactly ONE full set (all kinds with their per-set replica counts)
217+ // onto the provided working node capacities (in-place)
218+ // Returns true if successful
219+ func placeOneSet (orderedKinds []replicaKind , work []modelNode ) bool {
220+ for _ , k := range orderedKinds {
221+ remaining := k .count
222+ if remaining <= 0 {
223+ continue
224+ }
225+ // first-fit across nodes
226+ for n := range work {
227+ if remaining <= 0 {
228+ break
229+ }
230+ fit := maxFit (work [n ].cap , k .dem )
231+ if fit <= 0 {
232+ continue
233+ }
234+ place := fit
235+ if place > remaining {
236+ place = remaining
237+ }
238+ consumeMul (work [n ].cap , k .dem , place )
239+ remaining -= place
240+ }
241+ if remaining > 0 {
242+ return false
243+ }
244+ }
245+ return true
246+ }
247+
248+ // modelNode holds remaining capacity for a given node across all resource types
249+ type modelNode struct {
250+ cap map [corev1.ResourceName ]int64
251+ }
252+
253+ // buildModelNodes constructs identical nodes for each model grade using its Min vector,
254+ // repeated AllocatableModelings[grade].Count times. Grades are indexed directly.
255+ func buildModelNodes (cluster * clusterv1alpha1.Cluster ) ([]modelNode , error ) {
256+ if cluster == nil {
257+ return nil , fmt .Errorf ("nil cluster" )
258+ }
259+ if cluster .Status .ResourceSummary == nil {
260+ return nil , fmt .Errorf ("resource summary is nil" )
261+ }
262+ spec := cluster .Spec .ResourceModels
263+ allocs := cluster .Status .ResourceSummary .AllocatableModelings
264+ if len (spec ) == 0 {
265+ return nil , fmt .Errorf ("no resource models defined" )
266+ }
267+
268+ // Build capacity template per grade
269+ capsByGrade := make (map [uint ]map [corev1.ResourceName ]int64 , len (spec ))
270+ for _ , m := range spec {
271+ tmpl := make (map [corev1.ResourceName ]int64 , len (m .Ranges ))
272+ for _ , r := range m .Ranges {
273+ tmpl [r .Name ] = quantityAsInt64 (r .Min )
274+ }
275+ capsByGrade [m .Grade ] = tmpl
276+ }
277+
278+ // Accumulate counts by grade
279+ countByGrade := make (map [uint ]int , len (allocs ))
280+ for _ , a := range allocs {
281+ if a .Count < 0 {
282+ return nil , fmt .Errorf ("negative node count for grade %d" , a .Grade )
283+ }
284+ countByGrade [a .Grade ] += a .Count
285+ }
286+
287+ // Emit nodes for grades present in both spec & status.
288+ var nodes []modelNode
289+ for g , cnt := range countByGrade {
290+ if cnt == 0 {
291+ continue
292+ }
293+ tmpl , ok := capsByGrade [g ]
294+ if ! ok {
295+ return nil , fmt .Errorf ("status references grade %d not defined in spec" , g )
296+ }
297+ for range cnt {
298+ capCopy := make (map [corev1.ResourceName ]int64 , len (tmpl ))
299+ for k , v := range tmpl {
300+ capCopy [k ] = v
301+ }
302+ nodes = append (nodes , modelNode {cap : capCopy })
303+ }
304+ }
305+ return nodes , nil
306+ }
307+
308+ // replicaKind represents a single type of component, including replica demand and count
309+ type replicaKind struct {
310+ dem map [corev1.ResourceName ]int64 // per-replica demand
311+ count int64 // how many replicas
312+ score float64 // ordering heuristic (higher first)
313+ }
314+
315+ // expandKindsOneSet flattens components into a slice of unique replica kinds.
316+ // Each entry holds the per-replica demand and how many replicas of that kind a set needs.
317+ func expandKindsOneSet (components []workv1alpha2.Component ) []replicaKind {
318+ kinds := make ([]replicaKind , 0 , len (components ))
319+ for _ , c := range components {
320+ if c .ReplicaRequirements == nil || c .ReplicaRequirements .ResourceRequest == nil {
321+ continue
322+ }
323+ // normalize per-replica demand
324+ base := make (map [corev1.ResourceName ]int64 , len (c .ReplicaRequirements .ResourceRequest ))
325+ for name , qty := range c .ReplicaRequirements .ResourceRequest {
326+ base [name ] = quantityAsInt64 (qty )
327+ }
328+ // skip zero-demand or non-positive replica count
329+ if allZero (base ) || c .Replicas <= 0 {
330+ continue
331+ }
332+
333+ k := replicaKind {
334+ dem : base ,
335+ count : int64 (c .Replicas ),
336+ // score is filled later once we know cluster-wide totals
337+ }
338+ kinds = append (kinds , k )
339+ }
340+ return kinds
341+ }
342+
343+ // demandScoreNormalized returns the "max utilization ratio" of a demand vector against total capacities
344+ // If a resource is missing/zero in total, treat it as maximally constrained
345+ func demandScoreNormalized (
346+ demand map [corev1.ResourceName ]int64 ,
347+ total map [corev1.ResourceName ]int64 ,
348+ ) float64 {
349+ var maxRatio float64
350+ for res , req := range demand {
351+ if req <= 0 {
352+ continue
353+ }
354+ totalCap := float64 (total [res ])
355+ if totalCap <= 0 {
356+ return math .MaxFloat64
357+ }
358+ ratio := float64 (req ) / totalCap
359+ if ratio > maxRatio {
360+ maxRatio = ratio
361+ }
362+ }
363+ return maxRatio
364+ }
365+
366+ // demandSum is used as a tie-breaker when initial scores are equal
367+ func demandSum (m map [corev1.ResourceName ]int64 ) int64 {
368+ var s int64
369+ for _ , v := range m {
370+ if v > 0 {
371+ s += v
372+ }
373+ }
374+ return s
375+ }
376+
377+ // maxFit returns how many copies of `dem` fit in `cap` simultaneously
378+ func maxFit (capacity map [corev1.ResourceName ]int64 , dem map [corev1.ResourceName ]int64 ) int64 {
379+ var limit int64 = math .MaxInt64
380+ for k , req := range dem {
381+ if req <= 0 {
382+ continue
383+ }
384+ avail := capacity [k ]
385+ if avail <= 0 {
386+ return 0
387+ }
388+ bound := avail / req
389+ if bound < limit {
390+ limit = bound
391+ }
392+ }
393+ if limit == math .MaxInt64 {
394+ return 0
395+ }
396+ return limit
397+ }
398+
399+ // consumeMul subtracts mult * dem from cap
400+ func consumeMul (capacity map [corev1.ResourceName ]int64 , dem map [corev1.ResourceName ]int64 , mult int64 ) {
401+ if mult <= 0 {
402+ return
403+ }
404+ for k , req := range dem {
405+ if req <= 0 {
406+ continue
407+ }
408+ capacity [k ] -= req * mult
409+ }
170410}
171411
172412// podsInSet computes the total number of pods in the CRD
0 commit comments