This is an automated email from the ASF dual-hosted git repository.

mani pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/yunikorn-core.git


The following commit(s) were added to refs/heads/master by this push:
     new fac9b6ba [YUNIKORN-3233] Improve victim selection algorithm (#1072)
fac9b6ba is described below

commit fac9b6bae0ab703ca21e7fb6bc4d2a74dcc7e211
Author: mani <[email protected]>
AuthorDate: Tue Mar 24 14:53:43 2026 +0530

    [YUNIKORN-3233] Improve victim selection algorithm (#1072)
    
    Closes: #1072
    
    Signed-off-by: mani <[email protected]>
---
 pkg/common/resources/resources.go                  | 45 ++++++++++++
 pkg/common/resources/resources_test.go             | 69 ++++++++++++++++++
 pkg/scheduler/objects/preemption_utilities.go      | 58 ++++++++++++++++
 pkg/scheduler/objects/preemption_utilities_test.go | 81 ++++++++++++++++++++++
 pkg/scheduler/objects/quota_preemptor.go           |  2 +-
 5 files changed, 254 insertions(+), 1 deletion(-)

diff --git a/pkg/common/resources/resources.go 
b/pkg/common/resources/resources.go
index 290ba022..33ba8d57 100644
--- a/pkg/common/resources/resources.go
+++ b/pkg/common/resources/resources.go
@@ -1209,3 +1209,48 @@ func (r *Resource) DominantResourceType(capacity 
*Resource) string {
        }
        return dominant
 }
+
+func (r *Resource) TypeMatching(other *Resource) uint64 {
+       if r == nil || other == nil {
+               return 0
+       }
+       matchingResTypes := 0
+       for k := range other.Resources {
+               if _, ok := r.Resources[k]; ok {
+                       matchingResTypes++
+               }
+       }
+       return uint64(matchingResTypes * 100 / len(r.Resources))
+}
+
+// CompUsageRatioSpecificTypes Compare the left and right resources based on 
the ask resources.
+// Collect resource types of both left and right resources from ask resource 
perspective and ignore others.
+// Sort the shares for the collected resource types in increasing order and 
decide the largest resource
+// by comparing each share starting from last index to 0th index.
+func CompUsageRatioSpecificTypes(left, right, total, specificTypes *Resource) 
int {
+       lShareTypeWise := GetSharesTypeWise(left, total)
+       rShareTypeWise := GetSharesTypeWise(right, total)
+       if specificTypes != nil {
+               var lShare []float64
+               var rShare []float64
+               for k := range specificTypes.Resources {
+                       lValue, lExists := lShareTypeWise[k]
+                       rValue, rExists := rShareTypeWise[k]
+                       if lExists {
+                               lShare = append(lShare, lValue)
+                       }
+                       if rExists {
+                               rShare = append(rShare, rValue)
+                       }
+               }
+               if len(lShare) == 0 && len(rShare) == 0 {
+                       return 0
+               }
+               sort.Float64s(lShare)
+               sort.Float64s(rShare)
+               return compareShares(lShare, rShare)
+       } else {
+               // fall back to usual way of comparing usage shares.
+               return CompUsageRatio(left, right, total)
+       }
+}
diff --git a/pkg/common/resources/resources_test.go 
b/pkg/common/resources/resources_test.go
index 5c3b844d..24baf4fa 100644
--- a/pkg/common/resources/resources_test.go
+++ b/pkg/common/resources/resources_test.go
@@ -2470,3 +2470,72 @@ func TestResource_Prune(t *testing.T) {
                })
        }
 }
+
+func TestTypeMatching(t *testing.T) {
+       var tests = []struct {
+               name     string
+               base     *Resource
+               other    *Resource
+               expected uint64
+       }{
+               {"nil base resource", nil, 
NewResourceFromMap(map[string]Quantity{"first": 20}), 0},
+               {"nil other resource", 
NewResourceFromMap(map[string]Quantity{"first": 10}), nil, 0},
+               {"both nil resources", nil, nil, 0},
+               {"same resources", 
NewResourceFromMap(map[string]Quantity{"first": 10}), 
NewResourceFromMap(map[string]Quantity{"first": 20}), 100},
+               {"half of the resources matches", 
NewResourceFromMap(map[string]Quantity{"first": 10, "second": 10}), 
NewResourceFromMap(map[string]Quantity{"first": 10}), 50},
+               {"one third of the resources matches", 
NewResourceFromMap(map[string]Quantity{"first": 10, "second": 10, "third": 
10}), NewResourceFromMap(map[string]Quantity{"first": 10}), 33},
+               {"one fourth of the resources matches", 
NewResourceFromMap(map[string]Quantity{"first": 10, "second": 10, "third": 10, 
"fourth": 10}), NewResourceFromMap(map[string]Quantity{"first": 10}), 25},
+               {"one fourth of the resources matches, extra types in other 
resource should not affect", NewResourceFromMap(map[string]Quantity{"first": 
10, "second": 10, "third": 10, "fourth": 10}), 
NewResourceFromMap(map[string]Quantity{"first": 10, "fifth": 10, "sixth": 10}), 
25},
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       assert.Equal(t, tt.base.TypeMatching(tt.other), 
tt.expected)
+               })
+       }
+}
+
+func TestCompUsageRatioSpecificTypes(t *testing.T) {
+       tests := []struct {
+               name     string
+               left     *Resource
+               right    *Resource
+               total    *Resource
+               ask      *Resource
+               expected int
+       }{
+               {"nil resources", nil, nil, nil, 
NewResourceFromMap(map[string]Quantity{"first": 10}), 0},
+               {"empty resource with total nil", NewResource(), NewResource(), 
nil, NewResourceFromMap(map[string]Quantity{"first": 10}), 0},
+               {"empty resource", NewResource(), NewResource(), NewResource(), 
NewResourceFromMap(map[string]Quantity{"first": 10}), 0},
+               {"zero valued resource but extra types differs and not present 
in ask", NewResourceFromMap(map[string]Quantity{"zero": 0, "extra_a": 1}), 
NewResourceFromMap(map[string]Quantity{"zero": 0, "extra_a": 2}), nil, 
NewResourceFromMap(map[string]Quantity{"zero": 10, "first": 10}), 0},
+               {"negative valued resource", 
NewResourceFromMap(map[string]Quantity{"large": 5, "zero": 0, "small": -2, 
"extra_a": 1}), NewResourceFromMap(map[string]Quantity{"large": 5, "zero": 0, 
"small": -5, "extra_a": 2}), nil, 
NewResourceFromMap(map[string]Quantity{"large": 5, "small": 1}), 1},
+               {"negative valued resource on left side, ask is nil", 
NewResourceFromMap(map[string]Quantity{"large": 5, "zero": 0, "small": -5}), 
NewResourceFromMap(map[string]Quantity{"large": 5, "zero": 0}), nil, nil, -1},
+               {"negative valued resource on right side, ask is nil", 
NewResourceFromMap(map[string]Quantity{"large": 5, "zero": 0}), 
NewResourceFromMap(map[string]Quantity{"large": 5, "zero": 0, "small": -5}), 
nil, nil, 1},
+               {"negative valued resource but extra types differs and not 
present in ask", NewResourceFromMap(map[string]Quantity{"large": 5, "zero": 0, 
"small": -5, "extra_a": 1}), NewResourceFromMap(map[string]Quantity{"large": 5, 
"zero": 0, "small": -5, "extra_a": 2}), nil, 
NewResourceFromMap(map[string]Quantity{"large": 5}), 0},
+               {"zero valued resource with total but extra types differs and 
not present in ask", NewResourceFromMap(map[string]Quantity{"zero": 0, 
"extra_a": 1}), NewResourceFromMap(map[string]Quantity{"zero": 0, "extra_a": 
2}), NewResource(), NewResourceFromMap(map[string]Quantity{"zero": 0}), 0},
+               {"same resource and total but extra types differs and not 
present in ask", NewResourceFromMap(map[string]Quantity{"large": 5, "zero": 0, 
"small": -5, "extra_a": 1}), NewResourceFromMap(map[string]Quantity{"large": 5, 
"zero": 0, "small": -5, "extra_a": 2}), 
NewResourceFromMap(map[string]Quantity{"large": 5, "zero": 0, "small": -5, 
"extra_a": 10}), NewResourceFromMap(map[string]Quantity{"large": 5, "zero": 0, 
"small": -5}), 0},
+               {"left side has more one negative value for type present in ask 
but not present in right", NewResourceFromMap(map[string]Quantity{"large": 5, 
"zero": 0, "small": -5}), NewResourceFromMap(map[string]Quantity{"large": 5, 
"zero": 0}), NewResourceFromMap(map[string]Quantity{"large": 10, "zero": 10}), 
NewResourceFromMap(map[string]Quantity{"large": 100, "zero": 100, "small": 5}), 
-1},
+               {"left side has more one negative value but not present in 
ask", NewResourceFromMap(map[string]Quantity{"large": 5, "zero": 0, "small": 
-5}), NewResourceFromMap(map[string]Quantity{"large": 5, "zero": 0}), 
NewResourceFromMap(map[string]Quantity{"large": 10, "zero": 10}), 
NewResourceFromMap(map[string]Quantity{"large": 100, "zero": 100}), 0},
+               {"right side has more one negative value for type present in 
ask but not present in left", NewResourceFromMap(map[string]Quantity{"large": 
5, "zero": 0}), NewResourceFromMap(map[string]Quantity{"large": 5, "zero": 0, 
"small": -5}), NewResourceFromMap(map[string]Quantity{"large": 10, "zero": 
10}), NewResourceFromMap(map[string]Quantity{"large": 100, "zero": 100, 
"small": 5}), 1},
+               {"right side has more one negative value but not present in 
ask", NewResourceFromMap(map[string]Quantity{"large": 5, "zero": 0}), 
NewResourceFromMap(map[string]Quantity{"large": 5, "zero": 0, "small": -5}), 
NewResourceFromMap(map[string]Quantity{"large": 10, "zero": 10}), 
NewResourceFromMap(map[string]Quantity{"large": 100, "zero": 100}), 0},
+               {"left side first one bigger, last one smaller. right side vice 
versa. but share wise same", NewResourceFromMap(map[string]Quantity{"first": 
10, "second": 5}), NewResourceFromMap(map[string]Quantity{"first": 5, "second": 
10}), NewResourceFromMap(map[string]Quantity{"first": 15, "second": 15}), 
NewResourceFromMap(map[string]Quantity{"first": 1, "second": 1}), 0},
+               {"left side first one bigger, last one smaller. right side vice 
versa. but share wise not same", 
NewResourceFromMap(map[string]Quantity{"first": 10, "second": 5}), 
NewResourceFromMap(map[string]Quantity{"first": 5, "second": 10}), 
NewResourceFromMap(map[string]Quantity{"first": 15}), 
NewResourceFromMap(map[string]Quantity{"first": 1, "second": 1}), -1},
+               {"left side first one bigger, last one smaller. right side vice 
versa. but share wise not same. none of the ask res types match", 
NewResourceFromMap(map[string]Quantity{"first": 10, "second": 5}), 
NewResourceFromMap(map[string]Quantity{"first": 5, "second": 10}), 
NewResourceFromMap(map[string]Quantity{"first": 15}), 
NewResourceFromMap(map[string]Quantity{"third": 1, "fourth": 1}), 0},
+               {"left side first one smaller, last one bigger. right side vice 
versa. but share wise not same", 
NewResourceFromMap(map[string]Quantity{"first": 5, "second": 10}), 
NewResourceFromMap(map[string]Quantity{"first": 10, "second": 5}), 
NewResourceFromMap(map[string]Quantity{"first": 15}), 
NewResourceFromMap(map[string]Quantity{"first": 1, "second": 1}), 1},
+               {"left side first one smaller, last one bigger. right side vice 
versa. but share wise not same. none of the ask res types match", 
NewResourceFromMap(map[string]Quantity{"first": 5, "second": 10}), 
NewResourceFromMap(map[string]Quantity{"first": 10, "second": 5}), 
NewResourceFromMap(map[string]Quantity{"first": 15}), 
NewResourceFromMap(map[string]Quantity{"third": 1, "fourth": 1}), 0},
+               {"left side key order not same as right side which is aligned 
with ask order ", NewResourceFromMap(map[string]Quantity{"second": 10, "first": 
5}), NewResourceFromMap(map[string]Quantity{"first": 10, "second": 5}), 
NewResourceFromMap(map[string]Quantity{}), 
NewResourceFromMap(map[string]Quantity{"first": 1, "second": 1}), 0},
+               {"right side key order not same as left side which is aligned 
with ask order", NewResourceFromMap(map[string]Quantity{"second": 10, "first": 
5}), NewResourceFromMap(map[string]Quantity{"first": 10, "second": 5}), 
NewResourceFromMap(map[string]Quantity{}), 
NewResourceFromMap(map[string]Quantity{"second": 1, "first": 1}), 0},
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       ratio := CompUsageRatioSpecificTypes(tc.left, tc.right, 
tc.total, tc.ask)
+                       if ratio != tc.expected {
+                               t.Errorf("incorrect ratio, expected %v got: 
%v", tc.expected, ratio)
+                       }
+                       if tc.ask == nil {
+                               assert.Equal(t, ratio, CompUsageRatio(tc.left, 
tc.right, tc.total))
+                               assert.Equal(t, ratio, tc.expected)
+                       }
+               })
+       }
+}
diff --git a/pkg/scheduler/objects/preemption_utilities.go 
b/pkg/scheduler/objects/preemption_utilities.go
index c522ed68..660102c6 100644
--- a/pkg/scheduler/objects/preemption_utilities.go
+++ b/pkg/scheduler/objects/preemption_utilities.go
@@ -20,6 +20,7 @@ package objects
 
 import (
        "sort"
+       "time"
 
        "github.com/apache/yunikorn-core/pkg/common/resources"
 )
@@ -79,3 +80,60 @@ func SortAllocations(allocations []*Allocation) {
                return true
        })
 }
+
+func SortAllocationsBasedOnAsk(allocations []*Allocation, total, ask 
*resources.Resource) {
+       sort.SliceStable(allocations, func(i, j int) bool {
+               l := allocations[i]
+               r := allocations[j]
+
+               scoreLeft := scoreAllocationBasedOnAsk(l, ask)
+               scoreRight := scoreAllocationBasedOnAsk(r, ask)
+               if scoreLeft != scoreRight {
+                       return scoreLeft > scoreRight
+               }
+
+               // sort based on the priority
+               lPriority := l.GetPriority()
+               rPriority := r.GetPriority()
+               if lPriority < rPriority {
+                       return true
+               }
+               if lPriority > rPriority {
+                       return false
+               }
+
+               // sort based on the age (limiting the boundary to hour max)
+               lHour := l.GetCreateTime().Truncate(time.Hour)
+               rHour := r.GetCreateTime().Truncate(time.Hour)
+               if !lHour.Equal(rHour) {
+                       return lHour.After(rHour)
+               }
+
+               // sort based on the allocated resource
+               lResource := l.GetAllocatedResource()
+               rResource := r.GetAllocatedResource()
+               comp := resources.CompUsageRatioSpecificTypes(lResource, 
rResource, total, ask)
+               if comp == -1 {
+                       return true
+               }
+               if comp == 1 {
+                       return false
+               }
+               return true
+       })
+}
+
+// scoreAllocation generates a relative score for an allocation. Lower-scored 
allocations are considered more likely
+// preemption candidates. Tasks which have opted into preemption are 
considered first, then tasks which are not
+// application originators.
+func scoreAllocationBasedOnAsk(allocation *Allocation, ask 
*resources.Resource) uint64 {
+       var score uint64 = 0
+       if allocation.IsOriginator() {
+               score |= scoreOriginator
+       }
+       if !allocation.IsAllowPreemptSelf() {
+               score |= scoreNoPreempt
+       }
+       score += allocation.GetAllocatedResource().TypeMatching(ask)
+       return score
+}
diff --git a/pkg/scheduler/objects/preemption_utilities_test.go 
b/pkg/scheduler/objects/preemption_utilities_test.go
index b63c85b3..9574af49 100644
--- a/pkg/scheduler/objects/preemption_utilities_test.go
+++ b/pkg/scheduler/objects/preemption_utilities_test.go
@@ -211,3 +211,84 @@ func TestSortAllocations(t *testing.T) {
 
        removeAllocationAsks(node, asks)
 }
+
+func TestSortAllocationsBasedOnAsk(t *testing.T) {
+       node := NewNode(&si.NodeInfo{
+               NodeID:     "node",
+               Attributes: nil,
+               SchedulableResource: &si.Resource{
+                       Resources: map[string]*si.Quantity{"first": {Value: 
100}, "second": {Value: 100}, "third": {Value: 100}, "fourth": {Value: 100},
+                               "extra_a": {Value: 100}, "extra_b": {Value: 
100}, "extra_c": {Value: 100}, "extra_d": {Value: 100}, "extra_e": {Value: 100},
+                               "extra_f": {Value: 100}, "extra_g": {Value: 
100}, "extra_h": {Value: 100}, "extra_i": {Value: 100}, "extra_j": {Value: 
100}},
+               },
+       })
+
+       type vStruct struct {
+               key       string
+               allocated *resources.Resource
+       }
+
+       // Victims for res types matching comparison
+       res1 := vStruct{"ask1", 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 13})}
+       res2 := vStruct{"ask2", 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 12, 
"second": 10})}
+       res3 := vStruct{"ask3", 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 11, 
"second": 10, "third": 10})}
+       res4 := vStruct{"ask4", 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 10, 
"second": 10, "third": 10, "fourth": 10})}
+       askWithExtraRes := 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 10, 
"extra_a": 10, "extra_b": 10})
+
+       // Victims for allocated resources comparison
+       res5 := vStruct{"ask4", 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 13, 
"extra_c": 10, "extra_d": 10})}
+       res6 := vStruct{"ask3", 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 12, 
"extra_e": 10, "extra_f": 10})}
+       res7 := vStruct{"ask2", 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 11, 
"extra_g": 10, "extra_h": 10})}
+       res8 := vStruct{"ask1", 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 10, 
"extra_i": 10, "extra_j": 10})}
+       res9 := vStruct{"ask4", 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": -13, 
"extra_c": 10, "extra_d": 10})}
+
+       res10 := vStruct{"ask2", 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 5, 
"second": 10})}
+       res11 := vStruct{"ask1", 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 10, 
"second": 5})}
+       res12 := vStruct{"ask1", 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 5})}
+       res13 := vStruct{"ask2", 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 10, 
"second": 10})}
+       askWithExtraRes1 := 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 1, 
"second": 1})
+
+       total := 
resources.NewResourceFromMap(map[string]resources.Quantity{"first": 100, 
"second": 100, "third": 100, "fourth": 100})
+
+       testCases := []struct {
+               name            string
+               victims         []vStruct
+               total           *resources.Resource
+               ask             *resources.Resource
+               expectedVictims []string
+       }{
+               {"100% matching victims, final order should be based on 
matching % in decreasing order. ask extra res should not create any impact",
+                       []vStruct{res1}, total, askWithExtraRes, 
[]string{"ask1"}},
+               {"50% matching victims, final order should be based on matching 
% in decreasing order. ask extra res should not create any impact",
+                       []vStruct{res2, res1}, total, askWithExtraRes, 
[]string{"ask1", "ask2"}},
+               {"33% matching victims, final order should be based on matching 
% in decreasing order. ask extra res should not create any impact",
+                       []vStruct{res3, res2, res1}, total, askWithExtraRes, 
[]string{"ask1", "ask2", "ask3"}},
+               {"25% matching victims, final order should be based on matching 
% in decreasing order. ask extra res should not create any impact",
+                       []vStruct{res4, res3, res2, res1}, total, 
askWithExtraRes, []string{"ask1", "ask2", "ask3", "ask4"}},
+               {"ordering based on matching res types, non matching res types 
does not matter",
+                       []vStruct{res5, res6, res7, res8}, total, 
askWithExtraRes, []string{"ask1", "ask2", "ask3", "ask4"}},
+               {"negative value resource, ordering based on matching res 
types, non matching res types does not matter",
+                       []vStruct{res6, res7, res8, res9}, total, 
askWithExtraRes, []string{"ask4", "ask1", "ask2", "ask3"}},
+               {"first victim one res type bigger and another smaller and vice 
versa on second victim",
+                       []vStruct{res10, res11}, total, askWithExtraRes1, 
[]string{"ask1", "ask2"}},
+               {"first victim one res type bigger and another smaller and vice 
versa on second victim",
+                       []vStruct{res13, res12}, total, askWithExtraRes1, 
[]string{"ask1", "ask2"}},
+       }
+       for _, tc := range testCases {
+               t.Run(tc.name, func(t *testing.T) {
+                       var victimAllocations []*Allocation
+                       for _, vRes := range tc.victims {
+                               // Except victim used resources and ask 
resources, have all other criteria same for all victims
+                               victim := createAllocation(vRes.key, "app1", 
node.NodeID, true, false, 10, false,
+                                       vRes.allocated)
+                               assert.Assert(t, node.TryAddAllocation(victim))
+                               victimAllocations = append(victimAllocations, 
victim)
+                       }
+                       SortAllocationsBasedOnAsk(victimAllocations, tc.total, 
tc.ask)
+                       for index, sortedAsk := range victimAllocations {
+                               assert.Equal(t, sortedAsk.GetAllocationKey(), 
tc.expectedVictims[index])
+                       }
+                       removeAllocationAsks(node, victimAllocations)
+               })
+       }
+}
diff --git a/pkg/scheduler/objects/quota_preemptor.go 
b/pkg/scheduler/objects/quota_preemptor.go
index 17a751a7..108c2ce4 100644
--- a/pkg/scheduler/objects/quota_preemptor.go
+++ b/pkg/scheduler/objects/quota_preemptor.go
@@ -231,7 +231,7 @@ func (qpc *QuotaPreemptionContext) filterAllocations() {
 // sortAllocations Sort the allocations running in the queue
 func (qpc *QuotaPreemptionContext) sortAllocations() {
        if len(qpc.allocations) > 0 {
-               SortAllocations(qpc.allocations)
+               SortAllocationsBasedOnAsk(qpc.allocations, qpc.maxResource, 
qpc.preemptableResource)
        }
 }
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to