Skip to content

Commit 3371695

Browse files
committed
Implementing maxAvailableComponentSets for resource models
Signed-off-by: mszacillo <[email protected]>
1 parent 0fde1d2 commit 3371695

File tree

2 files changed

+489
-9
lines changed

2 files changed

+489
-9
lines changed

pkg/estimator/client/general.go

Lines changed: 193 additions & 9 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"
@@ -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

Comments
 (0)