Skip to content

Commit 336f08f

Browse files
committed
Implementing maxAvailableComponentSets for resource models
Signed-off-by: mszacillo <[email protected]>
1 parent 90c05cd commit 336f08f

File tree

2 files changed

+579
-10
lines changed

2 files changed

+579
-10
lines changed

pkg/estimator/client/general.go

Lines changed: 250 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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

5657
func (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

Comments
 (0)