Skip to content

Commit c7b3e9e

Browse files
committed
fix(scheduler): scope gpujoborder to victim selection only via new VictimOrderFn extension point
Fixes a sign-inversion bug where prefer-larger evicted the smaller job (the victim queue's !JobOrderFn inversion flipped the same sign used for pending-job ordering). Adds Session.VictimOrderFn/AddVictimOrderFn as a dedicated, non-inverted composition path for victim-specific comparators, falling back to the existing !JobOrderFn behavior when none are registered, so no other plugin's behavior changes. gpujoborder now registers exclusively via AddVictimOrderFn and no longer affects pending-job allocation ordering at all. Addresses @gshaibi's review comment on #1995. Signed-off-by: CoolingCube <CoolingCubeInfo@proton.me>
1 parent 8a53739 commit c7b3e9e

5 files changed

Lines changed: 85 additions & 14 deletions

File tree

pkg/scheduler/actions/utils/job_order_by_queue.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ func (jo *JobsOrderByQueues) createLeafNode(queue *queue_info.QueueInfo) *queueN
255255
queue: queue,
256256
children: scheduler_util.NewPriorityQueue(func(l, r interface{}) bool {
257257
if jo.options.VictimQueue {
258-
return !jo.ssn.JobOrderFn(l, r)
258+
return jo.ssn.VictimOrderFn(l, r)
259259
}
260260
return jo.ssn.JobOrderFn(l, r)
261261
}, jo.options.MaxJobsQueueDepth),

pkg/scheduler/framework/session.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ type Session struct {
7676
NodePreOrderFns []api.NodePreOrderFn
7777
NodeOrderFns []api.NodeOrderFn
7878
JobOrderFns []common_info.CompareFn
79+
VictimOrderFns []common_info.CompareFn
7980
SubGroupOrderFns []common_info.CompareFn
8081
TaskOrderFns []common_info.CompareFn
8182
QueueOrderFns []api.CompareQueueFn
@@ -434,6 +435,7 @@ func (ssn *Session) clear() {
434435
ssn.NodePreOrderFns = nil
435436
ssn.NodeOrderFns = nil
436437
ssn.JobOrderFns = nil
438+
ssn.VictimOrderFns = nil
437439
ssn.SubGroupOrderFns = nil
438440
ssn.TaskOrderFns = nil
439441
ssn.QueueOrderFns = nil

pkg/scheduler/framework/session_plugins.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,15 @@ func (ssn *Session) AddJobOrderFn(jof common_info.CompareFn) {
6868
ssn.JobOrderFns = append(ssn.JobOrderFns, jof)
6969
}
7070

71+
// AddVictimOrderFn registers a comparator that applies ONLY when ordering
72+
// candidates for eviction (the victim queue), not for regular pending-job
73+
// allocation ordering. Unlike JobOrderFn, no external inversion is applied
74+
// to this comparator's result: a negative return means "l is the BETTER
75+
// victim (should be evicted first)", directly.
76+
func (ssn *Session) AddVictimOrderFn(vof common_info.CompareFn) {
77+
ssn.VictimOrderFns = append(ssn.VictimOrderFns, vof)
78+
}
79+
7180
func (ssn *Session) AddTaskOrderFn(tof common_info.CompareFn) {
7281
ssn.TaskOrderFns = append(ssn.TaskOrderFns, tof)
7382
}
@@ -285,6 +294,21 @@ func (ssn *Session) JobOrderFn(l, r interface{}) bool {
285294
}
286295
}
287296

297+
// VictimOrderFn composes registered victim-specific comparators directly --
298+
// a negative result means "l is the BETTER victim", with no external
299+
// inversion needed or applied (unlike the old !JobOrderFn(l, r) pattern).
300+
// If no victim-specific comparators are registered, this falls back to the
301+
// existing !JobOrderFn(l, r) behavior, preserving exact current behavior
302+
// for any plugin that never needed the ordering/victim distinction.
303+
func (ssn *Session) VictimOrderFn(l, r interface{}) bool {
304+
for _, vof := range ssn.VictimOrderFns {
305+
if v := vof(l, r); v != 0 {
306+
return v < 0
307+
}
308+
}
309+
return !ssn.JobOrderFn(l, r)
310+
}
311+
288312
func (ssn *Session) TaskOrderFn(l, r interface{}) bool {
289313
for _, compareTasks := range ssn.TaskOrderFns {
290314
if comparison := compareTasks(l, r); comparison != 0 {

pkg/scheduler/plugins/gpujoborder/gpujoborder.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,19 @@ func (rp *gpuJobOrderPlugin) Name() string {
3131
return "gpujoborder"
3232
}
3333

34+
// OnSessionOpen registers this plugin's comparator via AddVictimOrderFn,
35+
// NOT AddJobOrderFn. This plugin is scoped to victim/eviction selection
36+
// only, per the real, confirmed distinction between the ordering and
37+
// victim-selection paths (see VictimOrderFn in session_plugins.go) --
38+
// pending-job allocation ordering is intentionally left untouched.
3439
func (rp *gpuJobOrderPlugin) OnSessionOpen(ssn *framework.Session) {
35-
ssn.AddJobOrderFn(rp.JobOrderFn)
40+
ssn.AddVictimOrderFn(rp.VictimOrderFn)
3641
}
3742

38-
func (rp *gpuJobOrderPlugin) JobOrderFn(l, r interface{}) int {
43+
// VictimOrderFn returns -1 when l is the BETTER victim (should be evicted
44+
// first), matching VictimOrderFn's direct (non-inverted) contract -- no
45+
// external sign flip is applied or needed here.
46+
func (rp *gpuJobOrderPlugin) VictimOrderFn(l, r interface{}) int {
3947
lv := l.(*podgroup_info.PodGroupInfo)
4048
rv := r.(*podgroup_info.PodGroupInfo)
4149

pkg/scheduler/plugins/gpujoborder/gpujoborder_test.go

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,48 +38,48 @@ func newPlugin(t *testing.T, mode string) *gpuJobOrderPlugin {
3838
return rp
3939
}
4040

41-
func TestJobOrderFn_PriorityDiffers_Defers(t *testing.T) {
41+
func TestVictimOrderFn_PriorityDiffers_Defers(t *testing.T) {
4242
vm := resource_info.NewResourceVectorMap()
4343
a := makeGPUPodGroup("a", 50, 1, vm)
4444
b := makeGPUPodGroup("b", 10, 1, vm)
4545
rp := newPlugin(t, "")
46-
if got := rp.JobOrderFn(a, b); got != 0 {
46+
if got := rp.VictimOrderFn(a, b); got != 0 {
4747
t.Errorf("expected 0 when priorities differ, got %d", got)
4848
}
4949
}
5050

51-
func TestJobOrderFn_SamePriority_PrefersLarger_Default(t *testing.T) {
51+
func TestVictimOrderFn_SamePriority_PrefersLarger_Default(t *testing.T) {
5252
vm := resource_info.NewResourceVectorMap()
5353
small := makeGPUPodGroup("small", 10, 1, vm)
5454
large := makeGPUPodGroup("large", 10, 2, vm)
5555
rp := newPlugin(t, "")
56-
if got := rp.JobOrderFn(large, small); got != -1 {
56+
if got := rp.VictimOrderFn(large, small); got != -1 {
5757
t.Errorf("expected -1 (larger job preferred as victim), got %d", got)
5858
}
59-
if got := rp.JobOrderFn(small, large); got != 1 {
59+
if got := rp.VictimOrderFn(small, large); got != 1 {
6060
t.Errorf("expected 1, got %d", got)
6161
}
6262
}
6363

64-
func TestJobOrderFn_SamePriority_SameGPU_FallsThrough(t *testing.T) {
64+
func TestVictimOrderFn_SamePriority_SameGPU_FallsThrough(t *testing.T) {
6565
vm := resource_info.NewResourceVectorMap()
6666
a := makeGPUPodGroup("a", 10, 1, vm)
6767
b := makeGPUPodGroup("b", 10, 1, vm)
6868
rp := newPlugin(t, "")
69-
if got := rp.JobOrderFn(a, b); got != 0 {
69+
if got := rp.VictimOrderFn(a, b); got != 0 {
7070
t.Errorf("expected 0 (equal GPU falls through), got %d", got)
7171
}
7272
}
7373

74-
func TestJobOrderFn_PreferSmallerMode(t *testing.T) {
74+
func TestVictimOrderFn_PreferSmallerMode(t *testing.T) {
7575
vm := resource_info.NewResourceVectorMap()
7676
small := makeGPUPodGroup("small", 10, 1, vm)
7777
large := makeGPUPodGroup("large", 10, 2, vm)
7878
rp := newPlugin(t, "prefer-smaller")
79-
if got := rp.JobOrderFn(small, large); got != -1 {
79+
if got := rp.VictimOrderFn(small, large); got != -1 {
8080
t.Errorf("expected -1 (smaller job preferred as victim in prefer-smaller mode), got %d", got)
8181
}
82-
if got := rp.JobOrderFn(large, small); got != 1 {
82+
if got := rp.VictimOrderFn(large, small); got != 1 {
8383
t.Errorf("expected 1, got %d", got)
8484
}
8585
}
@@ -89,7 +89,44 @@ func TestNew_UnrecognizedMode_FallsBackToPreferLarger(t *testing.T) {
8989
small := makeGPUPodGroup("small", 10, 1, vm)
9090
large := makeGPUPodGroup("large", 10, 2, vm)
9191
rp := newPlugin(t, "totally-not-a-real-mode")
92-
if got := rp.JobOrderFn(large, small); got != -1 {
92+
if got := rp.VictimOrderFn(large, small); got != -1 {
9393
t.Errorf("expected fallback to prefer-larger behavior, got %d", got)
9494
}
9595
}
96+
97+
// TestGpujoborder_DoesNotAffect_PendingJobOrdering is the specific test
98+
// gshaibi asked for: confirms that registering gpujoborder's comparator
99+
// has NO EFFECT on ssn.JobOrderFn (pending-job allocation ordering),
100+
// since the plugin now registers exclusively via AddVictimOrderFn.
101+
func TestGpujoborder_DoesNotAffect_PendingJobOrdering(t *testing.T) {
102+
rp := newPlugin(t, "")
103+
104+
vm := resource_info.NewResourceVectorMap()
105+
small := makeGPUPodGroup("small", 10, 1, vm)
106+
large := makeGPUPodGroup("large", 10, 2, vm)
107+
108+
// Session WITHOUT gpujoborder registered: falls through to the
109+
// existing CreationTimestamp/UID fallback for JobOrderFn.
110+
ssnWithout := &framework.Session{}
111+
baselineResult := ssnWithout.JobOrderFn(small, large)
112+
113+
// Session WITH gpujoborder registered via OnSessionOpen (the real
114+
// registration path), same two jobs, same JobOrderFn call.
115+
ssnWith := &framework.Session{}
116+
rp.OnSessionOpen(ssnWith)
117+
withPluginResult := ssnWith.JobOrderFn(small, large)
118+
119+
if len(ssnWith.JobOrderFns) != 0 {
120+
t.Errorf("expected gpujoborder to register 0 JobOrderFns (it should only "+
121+
"register via AddVictimOrderFn now), got %d", len(ssnWith.JobOrderFns))
122+
}
123+
if len(ssnWith.VictimOrderFns) != 1 {
124+
t.Errorf("expected gpujoborder to register exactly 1 VictimOrderFn, got %d",
125+
len(ssnWith.VictimOrderFns))
126+
}
127+
if baselineResult != withPluginResult {
128+
t.Errorf("pending-job ordering changed after registering gpujoborder: "+
129+
"without=%v, with=%v -- expected identical, since gpujoborder must not "+
130+
"affect JobOrderFn at all", baselineResult, withPluginResult)
131+
}
132+
}

0 commit comments

Comments
 (0)