Repository: armadaproject/armada
Pull request #2880 (2023-08-14 15:35:33)

Improved preemption logic
Better account for fairness when choosing what to preempt.

Despite a more complex algorithm, performance doesn't change much.
```
goos: windows
goarch: amd64
pkg: github.com/armadaproject/armada/internal/scheduler
cpu: 12th Gen Intel(R) Core(TM) i7-12700H
                                                             │   old.txt   │            new.txt             │
                                                             │   sec/op    │   sec/op     vs base           │
PreemptingQueueScheduler/1000_nodes_10_queues_320000_jobs-20   1.267 ± ∞ ¹   1.343 ± ∞ ¹  ~ (p=1.000 n=1) ²
¹ need >= 6 samples for confidence interval at level 0.95
² need >= 4 samples to detect a difference at alpha level 0.05

                                                             │    old.txt    │             new.txt              │
                                                             │     B/op      │     B/op       vs base           │
PreemptingQueueScheduler/1000_nodes_10_queues_320000_jobs-20   1.647Gi ± ∞ ¹   1.645Gi ± ∞ ¹  ~ (p=1.000 n=1) ²
¹ need >= 6 samples for confidence interval at level 0.95
² need >= 4 samples to detect a difference at alpha level 0.05

                                                             │   old.txt    │             new.txt             │
                                                             │  allocs/op   │  allocs/op    vs base           │
PreemptingQueueScheduler/1000_nodes_10_queues_320000_jobs-20   14.47M ± ∞ ¹   14.40M ± ∞ ¹  ~ (p=1.000 n=1) ²
¹ need >= 6 samples for confidence interval at level 0.95
² need >= 4 samples to detect a difference at alpha level 0.05
```

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/armadaproject/armada/pull/2880?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject) Report
Patch coverage has no change and project coverage change: **`-12.49%`** :warning:
> Comparison is base [(`510d59b`)](https://app.codecov.io/gh/armadaproject/armada/commit/510d59bd58a60e354168ebc84fcf10ff7b8b65d1?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject) 47.04% compared to head [(`7127e87`)](https://app.codecov.io/gh/armadaproject/armada/pull/2880?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject) 34.55%.
> Report is 15 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           master    #2880       +/-   ##
===========================================
- Coverage   47.04%   34.55%   -12.49%     
===========================================
  Files         391      113      -278     
  Lines       43055     3533    -39522     
  Branches      487      487               
===========================================
- Hits        20255     1221    -19034     
+ Misses      21285     2222    -19063     
+ Partials     1515       90     -1425     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `34.55% <ø> (-12.49%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#carryforward-flags-in-the-pull-request-comment) to find out more.

[see 278 files with indirect coverage changes](https://app.codecov.io/gh/armadaproject/armada/pull/2880/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject)


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/armadaproject/armada/pull/2880?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject).


Patch:
diff --git a/internal/armada/configuration/types.go b/internal/armada/configuration/types.go
index 79cfc371..977fdc74 100644
--- a/internal/armada/configuration/types.go
+++ b/internal/armada/configuration/types.go
@@ -220,6 +220,8 @@ type SchedulingConfig struct {
 	AlwaysAttemptScheduling bool
 	// The frequency at which the scheduler updates the cluster state.
 	ExecutorUpdateFrequency time.Duration
+	// Enable new preemption strategy.
+	EnableNewPreemptionStrategy bool
 }
 
 // FairnessModel controls how fairness is computed.
diff --git a/internal/armada/server/lease.go b/internal/armada/server/lease.go
index daaddb94..508206d9 100644
--- a/internal/armada/server/lease.go
+++ b/internal/armada/server/lease.go
@@ -40,6 +40,7 @@ import (
 	schedulerconstraints "github.com/armadaproject/armada/internal/scheduler/constraints"
 	schedulercontext "github.com/armadaproject/armada/internal/scheduler/context"
 	"github.com/armadaproject/armada/internal/scheduler/database"
+	"github.com/armadaproject/armada/internal/scheduler/fairness"
 	"github.com/armadaproject/armada/internal/scheduler/interfaces"
 	schedulerinterfaces "github.com/armadaproject/armada/internal/scheduler/interfaces"
 	"github.com/armadaproject/armada/internal/scheduler/nodedb"
@@ -468,17 +469,30 @@ func (q *AggregatedQueueServer) getJobs(ctx context.Context, req *api.StreamingL
 		defer cancel()
 	}
 
+	var fairnessCostProvider fairness.FairnessCostProvider
+	totalResources := schedulerobjects.ResourceList{Resources: totalCapacity}
+	if q.schedulingConfig.FairnessModel == configuration.DominantResourceFairness {
+		fairnessCostProvider, err = fairness.NewDominantResourceFairness(
+			totalResources,
+			q.schedulingConfig.DominantResourceFairnessResourcesToConsider,
+		)
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		fairnessCostProvider, err = fairness.NewAssetFairness(q.schedulingConfig.ResourceScarcity)
+		if err != nil {
+			return nil, err
+		}
+	}
 	sctx := schedulercontext.NewSchedulingContext(
 		req.ClusterId,
 		req.Pool,
 		q.schedulingConfig.Preemption.PriorityClasses,
 		q.schedulingConfig.Preemption.DefaultPriorityClass,
-		q.schedulingConfig.ResourceScarcity,
-		schedulerobjects.ResourceList{Resources: totalCapacity},
+		fairnessCostProvider,
+		totalResources,
 	)
-	if q.schedulingConfig.FairnessModel == configuration.DominantResourceFairness {
-		sctx.EnableDominantResourceFairness(q.schedulingConfig.DominantResourceFairnessResourcesToConsider)
-	}
 	for queue, priorityFactor := range priorityFactorByQueue {
 		if !isActiveByQueueName[queue] {
 			// To ensure fair share is computed only from active queues, i.e., queues with jobs queued or running.
@@ -518,6 +532,9 @@ func (q *AggregatedQueueServer) getJobs(ctx context.Context, req *api.StreamingL
 	if q.schedulingConfig.EnableAssertions {
 		sch.EnableAssertions()
 	}
+	if q.schedulingConfig.EnableNewPreemptionStrategy {
+		sch.EnableNewPreemptionStrategy()
+	}
 	log.Infof(
 		"starting scheduling with total resources %s",
 		schedulerobjects.ResourceList{Resources: totalCapacity}.CompactString(),
diff --git a/internal/scheduler/context/context.go b/internal/scheduler/context/context.go
index 7f5899fd..30f2c87e 100644
--- a/internal/scheduler/context/context.go
+++ b/internal/scheduler/context/context.go
@@ -10,7 +10,6 @@ import (
 	"github.com/pkg/errors"
 	"golang.org/x/exp/maps"
 	"golang.org/x/exp/slices"
-	"k8s.io/apimachinery/pkg/api/resource"
 
 	"github.com/armadaproject/armada/internal/armada/configuration"
 	"github.com/armadaproject/armada/internal/common/armadaerrors"
@@ -18,6 +17,7 @@ import (
 	armadaslices "github.com/armadaproject/armada/internal/common/slices"
 	"github.com/armadaproject/armada/internal/common/types"
 	schedulerconfig "github.com/armadaproject/armada/internal/scheduler/configuration"
+	"github.com/armadaproject/armada/internal/scheduler/fairness"
 	"github.com/armadaproject/armada/internal/scheduler/interfaces"
 	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
 )
@@ -37,19 +37,13 @@ type SchedulingContext struct {
 	// Default priority class.
 	DefaultPriorityClass string
 	// Determines how fairness is computed.
-	FairnessModel configuration.FairnessModel
-	// Resources considered when computing DominantResourceFairness.
-	DominantResourceFairnessResourcesToConsider []string
-	// Weights used when computing AssetFairness.
-	ResourceScarcity map[string]float64
+	FairnessCostProvider fairness.FairnessCostProvider
 	// Sum of queue weights across all queues.
 	WeightSum float64
 	// Per-queue scheduling contexts.
 	QueueSchedulingContexts map[string]*QueueSchedulingContext
 	// Total resources across all clusters available at the start of the scheduling cycle.
 	TotalResources schedulerobjects.ResourceList
-	// = TotalResources.AsWeightedMillis(ResourceScarcity).
-	TotalResourcesAsWeightedMillis int64
 	// Resources assigned across all queues during this scheduling cycle.
 	ScheduledResources                schedulerobjects.ResourceList
 	ScheduledResourcesByPriorityClass schedulerobjects.QuantityByTAndResourceType[string]
@@ -78,7 +72,7 @@ func NewSchedulingContext(
 	pool string,
 	priorityClasses map[string]types.PriorityClass,
 	defaultPriorityClass string,
-	resourceScarcity map[string]float64,
+	fairnessCostProvider fairness.FairnessCostProvider,
 	totalResources schedulerobjects.ResourceList,
 ) *SchedulingContext {
 	return &SchedulingContext{
@@ -87,11 +81,9 @@ func NewSchedulingContext(
 		Pool:                              pool,
 		PriorityClasses:                   priorityClasses,
 		DefaultPriorityClass:              defaultPriorityClass,
-		FairnessModel:                     configuration.AssetFairness,
-		ResourceScarcity:                  resourceScarcity,
+		FairnessCostProvider:              fairnessCostProvider,
 		QueueSchedulingContexts:           make(map[string]*QueueSchedulingContext),
 		TotalResources:                    totalResources.DeepCopy(),
-		TotalResourcesAsWeightedMillis:    totalResources.AsWeightedMillis(resourceScarcity),
 		ScheduledResources:                schedulerobjects.NewResourceListWithDefaultSize(),
 		ScheduledResourcesByPriorityClass: make(schedulerobjects.QuantityByTAndResourceType[string]),
 		EvictedResourcesByPriorityClass:   make(schedulerobjects.QuantityByTAndResourceType[string]),
@@ -100,11 +92,6 @@ func NewSchedulingContext(
 	}
 }
 
-func (sctx *SchedulingContext) EnableDominantResourceFairness(dominantResourceFairnessResourcesToConsider []string) {
-	sctx.FairnessModel = configuration.DominantResourceFairness
-	sctx.DominantResourceFairnessResourcesToConsider = dominantResourceFairnessResourcesToConsider
-}
-
 func (sctx *SchedulingContext) SchedulingKeyFromLegacySchedulerJob(job interfaces.LegacySchedulerJob) schedulerobjects.SchedulingKey {
 	var priority int32
 	if priorityClass, ok := sctx.PriorityClasses[job.GetPriorityClassName()]; ok {
@@ -163,11 +150,17 @@ func (sctx *SchedulingContext) String() string {
 	return sctx.ReportString(0)
 }
 
+// GetQueue is necessary to implement the fairness.QueueRepository interface.
+func (sctx *SchedulingContext) GetQueue(queue string) (fairness.Queue, bool) {
+	qctx, ok := sctx.QueueSchedulingContexts[queue]
+	return qctx, ok
+}
+
 // TotalCost returns the sum of the costs across all queues.
 func (sctx *SchedulingContext) TotalCost() float64 {
 	var rv float64
 	for _, qctx := range sctx.QueueSchedulingContexts {
-		rv += qctx.TotalCostForQueue()
+		rv += sctx.FairnessCostProvider.CostFromQueue(qctx)
 	}
 	return rv
 }
@@ -197,7 +190,7 @@ func (sctx *SchedulingContext) ReportString(verbosity int32) string {
 		fmt.Fprint(w, "Scheduled queues:\n")
 		for queueName, qctx := range scheduled {
 			fmt.Fprintf(w, "\t%s:\n", queueName)
-			fmt.Fprintf(w, indent.String("\t\t", qctx.ReportString(verbosity-2)))
+			fmt.Fprint(w, indent.String("\t\t", qctx.ReportString(verbosity-2)))
 		}
 	}
 	preempted := armadamaps.Filter(
@@ -212,7 +205,7 @@ func (sctx *SchedulingContext) ReportString(verbosity int32) string {
 		fmt.Fprint(w, "Preempted queues:\n")
 		for queueName, qctx := range preempted {
 			fmt.Fprintf(w, "\t%s:\n", queueName)
-			fmt.Fprintf(w, indent.String("\t\t", qctx.ReportString(verbosity-2)))
+			fmt.Fprint(w, indent.String("\t\t", qctx.ReportString(verbosity-2)))
 		}
 	}
 	w.Flush()
@@ -371,6 +364,16 @@ func (qctx *QueueSchedulingContext) String() string {
 	return qctx.ReportString(0)
 }
 
+// GetAllocation is necessary to implement the fairness.Queue interface.
+func (qctx *QueueSchedulingContext) GetAllocation() schedulerobjects.ResourceList {
+	return qctx.Allocated
+}
+
+// GetWeight is necessary to implement the fairness.Queue interface.
+func (qctx *QueueSchedulingContext) GetWeight() float64 {
+	return qctx.Weight
+}
+
 const maxJobIdsToPrint = 1
 
 func (qctx *QueueSchedulingContext) ReportString(verbosity int32) string {
@@ -517,50 +520,6 @@ func (qctx *QueueSchedulingContext) ClearJobSpecs() {
 	}
 }
 
-// TotalCostForQueue returns the total cost of this queue.
-func (qctx *QueueSchedulingContext) TotalCostForQueue() float64 {
-	return qctx.TotalCostForQueueWithAllocation(qctx.Allocated)
-}
-
-// TotalCostForQueueWithAllocation returns the total cost of this queue if its total allocation is given by allocated.
-func (qctx *QueueSchedulingContext) TotalCostForQueueWithAllocation(allocated schedulerobjects.ResourceList) float64 {
-	switch qctx.SchedulingContext.FairnessModel {
-	case configuration.AssetFairness:
-		return qctx.assetFairnessCostWithAllocation(allocated)
-	case configuration.DominantResourceFairness:
-		return qctx.dominantResourceFairnessCostWithAllocation(allocated)
-	default:
-		panic(fmt.Sprintf("unknown fairness type: %s", qctx.SchedulingContext.FairnessModel))
-	}
-}
-
-func (qctx *QueueSchedulingContext) assetFairnessCostWithAllocation(allocated schedulerobjects.ResourceList) float64 {
-	if len(qctx.SchedulingContext.ResourceScarcity) == 0 {
-		panic("ResourceScarcity is not set")
-	}
-	return float64(allocated.AsWeightedMillis(qctx.SchedulingContext.ResourceScarcity)) / qctx.Weight
-}
-
-func (qctx *QueueSchedulingContext) dominantResourceFairnessCostWithAllocation(allocated schedulerobjects.ResourceList) float64 {
-	if len(qctx.SchedulingContext.DominantResourceFairnessResourcesToConsider) == 0 {
-		panic("DominantResourceFairnessResourcesToConsider is not set")
-	}
-	var cost float64
-	for _, t := range qctx.SchedulingContext.DominantResourceFairnessResourcesToConsider {
-		capacity := qctx.SchedulingContext.TotalResources.Get(t)
-		if capacity.Equal(resource.Quantity{}) {
-			// Ignore any resources with zero capacity.
-			continue
-		}
-		q := allocated.Get(t)
-		tcost := float64(q.MilliValue()) / float64(capacity.MilliValue())
-		if tcost > cost {
-			cost = tcost
-		}
-	}
-	return cost / qctx.Weight
-}
-
 type GangSchedulingContext struct {
 	Created               time.Time
 	Queue                 string
diff --git a/internal/scheduler/fairness/fairness.go b/internal/scheduler/fairness/fairness.go
new file mode 100644
index 00000000..36509d65
--- /dev/null
+++ b/internal/scheduler/fairness/fairness.go
@@ -0,0 +1,86 @@
+package fairness
+
+import (
+	"github.com/pkg/errors"
+	"k8s.io/apimachinery/pkg/api/resource"
+
+	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
+)
+
+// QueueRepository is a minimal representation of a queue repository used for computing fairness.
+type QueueRepository interface {
+	GetQueue(name string) (Queue, bool)
+}
+
+// Queue is a minimal representation of a queue used for computing fairness.
+type Queue interface {
+	// GetAllocation returns the current allocation of the queue.
+	GetAllocation() schedulerobjects.ResourceList
+	GetWeight() float64
+}
+
+// FairnessCostProvider captures algorithms to compute the cost of an allocation.
+type FairnessCostProvider interface {
+	CostFromQueue(queue Queue) float64
+	CostFromAllocationAndWeight(allocation schedulerobjects.ResourceList, weight float64) float64
+}
+
+type AssetFairness struct {
+	// Weights used when computing asset fairness.
+	resourceScarcity map[string]float64
+}
+
+func NewAssetFairness(resourceScarcity map[string]float64) (*AssetFairness, error) {
+	if len(resourceScarcity) == 0 {
+		return nil, errors.New("resourceScarcity is empty")
+	}
+	return &AssetFairness{
+		resourceScarcity: resourceScarcity,
+	}, nil
+}
+
+func (f *AssetFairness) CostFromQueue(queue Queue) float64 {
+	return f.CostFromAllocationAndWeight(queue.GetAllocation(), queue.GetWeight())
+}
+
+func (f *AssetFairness) CostFromAllocationAndWeight(allocation schedulerobjects.ResourceList, weight float64) float64 {
+	return float64(allocation.AsWeightedMillis(f.resourceScarcity)) / weight
+}
+
+type DominantResourceFairness struct {
+	// Total resources across all nodes.
+	totalResources schedulerobjects.ResourceList
+	// Resources considered when computing DominantResourceFairness.
+	resourcesToConsider []string
+}
+
+func NewDominantResourceFairness(totalResources schedulerobjects.ResourceList, resourcesToConsider []string) (*DominantResourceFairness, error) {
+	if len(resourcesToConsider) == 0 {
+		return nil, errors.New("resourcesToConsider is empty")
+	}
+	return &DominantResourceFairness{
+		totalResources:      totalResources,
+		resourcesToConsider: resourcesToConsider,
+	}, nil
+}
+
+func (f *DominantResourceFairness) CostFromQueue(queue Queue) float64 {
+	return f.CostFromAllocationAndWeight(queue.GetAllocation(), queue.GetWeight())
+}
+
+func (f *DominantResourceFairness) CostFromAllocationAndWeight(allocation schedulerobjects.ResourceList, weight float64) float64 {
+	var cost float64
+	for _, t := range f.resourcesToConsider {
+		capacity := f.totalResources.Get(t)
+		if capacity.Equal(resource.Quantity{}) {
+			// Ignore any resources with zero capacity.
+			continue
+		}
+		q := allocation.Get(t)
+		tcost := float64(q.MilliValue()) / float64(capacity.MilliValue())
+		if tcost > cost {
+			cost = tcost
+		}
+	}
+	return cost / weight
+}
diff --git a/internal/scheduler/nodedb/encoding.go b/internal/scheduler/nodedb/encoding.go
index b5ea044b..a5d88716 100644
--- a/internal/scheduler/nodedb/encoding.go
+++ b/internal/scheduler/nodedb/encoding.go
@@ -25,7 +25,7 @@ func NodeIndexKey(out []byte, nodeTypeId uint64, resources []resource.Quantity)
 	return out
 }
 
-// RoundedNodeIndexKeyFromResourceList works like NodeIndexKey, except that prior to constructing a the key
+// RoundedNodeIndexKeyFromResourceList works like NodeIndexKey, except that prior to constructing the key
 // the i-th resource is rounded down to the closest multiple of resourceResolutionMillis[i].
 // It also takes as arguments a list of resource names and a resourceList, instead of a list of resources.
 func RoundedNodeIndexKeyFromResourceList(out []byte, nodeTypeId uint64, resourceNames []string, resourceResolutionMillis []int64, rl schedulerobjects.ResourceList) []byte {
diff --git a/internal/scheduler/nodedb/nodedb.go b/internal/scheduler/nodedb/nodedb.go
index 87aab622..a2351d1e 100644
--- a/internal/scheduler/nodedb/nodedb.go
+++ b/internal/scheduler/nodedb/nodedb.go
@@ -18,6 +18,7 @@ import (
 	"github.com/armadaproject/armada/internal/armada/configuration"
 	"github.com/armadaproject/armada/internal/common/armadaerrors"
 	armadamaps "github.com/armadaproject/armada/internal/common/maps"
+	armadaslices "github.com/armadaproject/armada/internal/common/slices"
 	"github.com/armadaproject/armada/internal/common/types"
 	"github.com/armadaproject/armada/internal/common/util"
 	schedulerconfig "github.com/armadaproject/armada/internal/scheduler/configuration"
@@ -199,6 +200,20 @@ func (nodeDb *NodeDb) CreateAndInsertWithJobDbJobsWithTxn(txn *memdb.Txn, jobs [
 	return nil
 }
 
+// EvictedJobSchedulingContext represents an evicted job.
+// NodeDb may track these to ensure preemptions are fair.
+type EvictedJobSchedulingContext struct {
+	// Id of the evicted job.
+	JobId string
+	// Each evicted job is assigned a unique integer indicating the order in which it is re-scheduled.
+	// I.e., index establishes a global order among all evicted jobs.
+	//
+	// When choosing on which node to schedule a job that would prevent re-scheduling evicted jobs,
+	// nodeDb choses the node that would prevent re-scheduling jobs with as a large an index as possible.
+	Index                int
+	JobSchedulingContext *schedulercontext.JobSchedulingContext
+}
+
 // NodeDb is the scheduler-internal system used to efficiently find nodes on which a pod could be scheduled.
 type NodeDb struct {
 	// In-memory database storing *Node.
@@ -215,12 +230,13 @@ type NodeDb struct {
 	// Because the number of database indices scales linearly with the number of distinct priorities,
 	// the efficiency of the NodeDb relies on the number of distinct priorities being small.
 	priorityClasses map[string]types.PriorityClass
-	// Priorities, in increasing order, to try to schedule pods at.
-	// In particular, if a pod has priority class priority p, try to schedule that pod at priority
-	// prioritiesToTryAssigningAt[0], ..., prioritiesToTryAssigningAt[i],
-	// for all i such that prioritiesToTryAssigningAt[i] <= the priority of the pod.
-	// We do this to, when possible, avoid preempting running jobs. Includes evictedPriority.
-	prioritiesToTryAssigningAt []int32
+	// Prioritiy class priorities in increasing order.
+	priorityClassPriorities []int32
+	// Job priorities supported by the NodeDb. Composed of priority class priorities and nodeDb-internal priorities.
+	// In particular, if a job has priority class priority p, nodeDb tries to schedule that job at priority
+	// nodeDbPriorities[0], ..., nodeDbPriorities[i],
+	// for all i such that nodeDbPriorities[i] <= the priority of the job.
+	nodeDbPriorities []int32
 	// Resources, e.g., "cpu", "memory", and "nvidia.com/gpu",
 	// for which indexes are created to enable efficient lookup.
 	indexedResources []string
@@ -266,6 +282,9 @@ type NodeDb struct {
 	// Map from podRequirementsNotMetReason Sum64() to the string representation of that reason.
 	// Used to avoid allocs.
 	podRequirementsNotMetReasonStringCache map[uint64]string
+
+	// If true, use experimental preemption strategy.
+	enableNewPreemptionStrategy bool
 }
 
 func NewNodeDb(
@@ -275,13 +294,6 @@ func NewNodeDb(
 	indexedTaints,
 	indexedNodeLabels []string,
 ) (*NodeDb, error) {
-	allowedPriorities := map[int32]bool{evictedPriority: true}
-	for _, pc := range priorityClasses {
-		allowedPriorities[pc.Priority] = true
-	}
-	prioritiesToTryAssigningAt := maps.Keys(allowedPriorities)
-	slices.Sort(prioritiesToTryAssigningAt)
-
 	if len(indexedResources) == 0 {
 		return nil, errors.WithStack(&armadaerrors.ErrInvalidArgument{
 			Name:    "indexedResources",
@@ -290,7 +302,14 @@ func NewNodeDb(
 		})
 	}
 	indexedResourceNames := util.Map(indexedResources, func(v configuration.IndexedResource) string { return v.Name })
-	schema, indexNameByPriority := nodeDbSchema(prioritiesToTryAssigningAt, indexedResourceNames)
+	allowedPriorities := make(map[int32]bool, len(priorityClasses))
+	for _, pc := range priorityClasses {
+		allowedPriorities[pc.Priority] = true
+	}
+	priorityClassPriorities := maps.Keys(allowedPriorities)
+	slices.Sort(priorityClassPriorities)
+	nodeDbPriorities := armadaslices.Concatenate([]int32{evictedPriority}, priorityClassPriorities)
+	schema, indexNameByPriority := nodeDbSchema(nodeDbPriorities, indexedResourceNames)
 	db, err := memdb.NewMemDB(schema)
 	if err != nil {
 		return nil, errors.WithStack(err)
@@ -323,11 +342,12 @@ func NewNodeDb(
 	}
 
 	return &NodeDb{
-		priorityClasses:            priorityClasses,
-		prioritiesToTryAssigningAt: prioritiesToTryAssigningAt,
-		maxExtraNodesToConsider:    maxExtraNodesToConsider,
-		indexedResources:           indexedResourceNames,
-		indexedResourcesSet:        mapFromSlice(indexedResourceNames),
+		priorityClasses:         priorityClasses,
+		priorityClassPriorities: priorityClassPriorities,
+		nodeDbPriorities:        nodeDbPriorities,
+		maxExtraNodesToConsider: maxExtraNodesToConsider,
+		indexedResources:        indexedResourceNames,
+		indexedResourcesSet:     mapFromSlice(indexedResourceNames),
 		indexedResourceResolutionMillis: util.Map(
 			indexedResources,
 			func(v configuration.IndexedResource) int64 { return v.Resolution.MilliValue() },
@@ -345,6 +365,28 @@ func NewNodeDb(
 	}, nil
 }
 
+// Reset clears out data specific to one scheduling round to prepare for a new scheduling round.
+// Only necessary when nodeDb.enableNewPreemptionStrategy is true.
+func (nodeDb *NodeDb) Reset() error {
+	txn := nodeDb.Txn(true)
+	defer txn.Abort()
+	it, err := txn.LowerBound("evictedJobs", "id", "")
+	if err != nil {
+		return errors.WithStack(err)
+	}
+	for obj := it.Next(); obj != nil; obj = it.Next() {
+		if err := txn.Delete("evictedJobs", obj); err != nil {
+			return errors.WithStack(err)
+		}
+	}
+	txn.Commit()
+	return nil
+}
+
+func (nodeDb *NodeDb) EnableNewPreemptionStrategy() {
+	nodeDb.enableNewPreemptionStrategy = true
+}
+
 func (nodeDb *NodeDb) String() string {
 	var sb strings.Builder
 	w := tabwriter.NewWriter(&sb, 1, 1, 1, ' ', 0)
@@ -472,6 +514,7 @@ func (nodeDb *NodeDb) ScheduleManyWithTxn(txn *memdb.Txn, jctxs []*schedulercont
 		if err != nil {
 			return false, err
 		}
+
 		// If we found a node for this pod, bind it and continue to the next pod.
 		if node != nil {
 			if node, err := bindJobToNode(nodeDb.priorityClasses, jctx.Job, node); err != nil {
@@ -484,10 +527,27 @@ func (nodeDb *NodeDb) ScheduleManyWithTxn(txn *memdb.Txn, jctxs []*schedulercont
 		} else {
 			return false, nil
 		}
+
+		// Once a job is scheduled, it should no longer be considered for preemption.
+		if nodeDb.enableNewPreemptionStrategy {
+			if err := deleteEvictedJobSchedulingContextIfExistsWithTxn(txn, jctx.JobId); err != nil {
+				return false, err
+			}
+		}
 	}
 	return true, nil
 }
 
+func deleteEvictedJobSchedulingContextIfExistsWithTxn(txn *memdb.Txn, jobId string) error {
+	if err := txn.Delete("evictedJobs", &EvictedJobSchedulingContext{JobId: jobId}); err == memdb.ErrNotFound {
+		return nil
+	} else if err != nil {
+		return errors.WithStack(err)
+	} else {
+		return nil
+	}
+}
+
 // SelectNodeForJobWithTxn selects a node on which the job can be scheduled.
 func (nodeDb *NodeDb) SelectNodeForJobWithTxn(txn *memdb.Txn, jctx *schedulercontext.JobSchedulingContext) (*Node, error) {
 	req := jctx.PodRequirements
@@ -500,9 +560,10 @@ func (nodeDb *NodeDb) SelectNodeForJobWithTxn(txn *memdb.Txn, jctx *schedulercon
 
 	// Create a pctx to be returned to the caller.
 	pctx := &schedulercontext.PodSchedulingContext{
-		Created:                  time.Now(),
-		MatchingNodeTypes:        matchingNodeTypes,
-		NumNodes:                 nodeDb.numNodes,
+		Created:           time.Now(),
+		MatchingNodeTypes: matchingNodeTypes,
+		NumNodes:          nodeDb.numNodes,
+		// TODO: This clone looks unnecessary.
 		NumExcludedNodesByReason: maps.Clone(numExcludedNodesByReason),
 	}
 	jctx.PodSchedulingContext = pctx
@@ -522,8 +583,7 @@ func (nodeDb *NodeDb) SelectNodeForJobWithTxn(txn *memdb.Txn, jctx *schedulercon
 		}
 	}()
 
-	// If the targetNodeIdAnnocation is set, consider only that node,
-	// and schedule onto that node even if it requires preempting other jobs.
+	// If the targetNodeIdAnnocation is set, consider only that node.
 	if nodeId, ok := req.NodeSelector[schedulerconfig.NodeIdLabel]; ok {
 		if it, err := txn.Get("nodes", "id", nodeId); err != nil {
 			return nil, errors.WithStack(err)
@@ -536,9 +596,81 @@ func (nodeDb *NodeDb) SelectNodeForJobWithTxn(txn *memdb.Txn, jctx *schedulercon
 		}
 	}
 
-	// Try to schedule this pod normally.
-	// To avoid preempting running jobs, try scheduling at each available priority from lowest to highest.
-	for _, priority := range nodeDb.prioritiesToTryAssigningAt {
+	// Try scheduling at evictedPriority. If this succeeds, no preemption is necessary.
+	pctx.NumExcludedNodesByReason = maps.Clone(numExcludedNodesByReason)
+	if node, err := nodeDb.selectNodeForPodAtPriority(txn, pctx, evictedPriority, jctx.PodRequirements); err != nil {
+		return nil, err
+	} else if err := assertPodSchedulingContextNode(pctx, node); err != nil {
+		return nil, err
+	} else if node != nil {
+		return node, nil
+	}
+
+	// Try scheduling at the job priority. If this fails, scheduling is impossible and we return.
+	// This is an optimisation to avoid looking for preemption targets for unschedulable jobs.
+	pctx.NumExcludedNodesByReason = maps.Clone(numExcludedNodesByReason)
+	if node, err := nodeDb.selectNodeForPodAtPriority(txn, pctx, jctx.PodRequirements.Priority, jctx.PodRequirements); err != nil {
+		return nil, err
+	} else if err := assertPodSchedulingContextNode(pctx, node); err != nil {
+		return nil, err
+	} else if node == nil {
+		return nil, nil
+	}
+	pctx.NodeId = ""
+	pctx.Score = 0
+	pctx.ScheduledAtPriority = 0
+
+	// Schedule by preventing evicted jobs from being re-scheduled.
+	// This method respect fairness by preventing from re-scheduling jobs that appear as far back in the total order as possible.
+	if nodeDb.enableNewPreemptionStrategy {
+		if node, err := nodeDb.selectNodeForJobWithFairPreemption(txn, jctx); err != nil {
+			return nil, err
+		} else if err := assertPodSchedulingContextNode(pctx, node); err != nil {
+			return nil, err
+		} else if node != nil {
+			return node, nil
+		}
+	}
+	pctx.NodeId = ""
+	pctx.Score = 0
+	pctx.ScheduledAtPriority = 0
+
+	// Schedule by kicking off jobs currently bound to a node.
+	// This method does not respect fairness when choosing on which node to schedule the job.
+	if node, err := nodeDb.selectNodeForJobWithUrgencyPreemption(txn, jctx); err != nil {
+		return nil, err
+	} else if err := assertPodSchedulingContextNode(pctx, node); err != nil {
+		return nil, err
+	} else if node != nil {
+		return node, nil
+	}
+
+	return nil, nil
+}
+
+func assertPodSchedulingContextNode(pctx *schedulercontext.PodSchedulingContext, node *Node) error {
+	if node != nil {
+		if pctx.NodeId == "" {
+			return errors.New("pctx.NodeId not set")
+		}
+		if node.Id != pctx.NodeId {
+			return errors.Errorf("pctx.NodeId %s does not match node.Id %s", pctx.NodeId, node.Id)
+		}
+	} else if pctx.NodeId != "" {
+		return errors.New("pctx.NodeId is set, but no node was returned")
+	}
+	return nil
+}
+
+func (nodeDb *NodeDb) selectNodeForJobWithUrgencyPreemption(
+	txn *memdb.Txn,
+	jctx *schedulercontext.JobSchedulingContext,
+) (*Node, error) {
+	pctx := jctx.PodSchedulingContext
+	req := jctx.PodRequirements
+	numExcludedNodesByReason := pctx.NumExcludedNodesByReason
+	// TODO: This doesn't need to include the evictedPriority now.
+	for _, priority := range nodeDb.priorityClassPriorities {
 		if priority > req.Priority {
 			break
 		}
@@ -548,22 +680,13 @@ func (nodeDb *NodeDb) SelectNodeForJobWithTxn(txn *memdb.Txn, jctx *schedulercon
 		pctx.NumExcludedNodesByReason = maps.Clone(numExcludedNodesByReason)
 
 		// Try to find a node at this priority.
-		node, err := nodeDb.selectNodeForPodAtPriority(txn, pctx, priority, req)
-		if err != nil {
+		if node, err := nodeDb.selectNodeForPodAtPriority(txn, pctx, priority, req); err != nil {
 			return nil, err
-		}
-		if node != nil {
-			if pctx.NodeId == "" {
-				return nil, errors.New("pctx.NodeId not set")
-			}
-			if node.Id != pctx.NodeId {
-				return nil, errors.New("pctx.NodeId does not match that of the returned node")
-			}
+		} else if err := assertPodSchedulingContextNode(pctx, node); err != nil {
+			return nil, err
+		} else if node != nil {
 			return node, nil
 		}
-		if pctx.NodeId != "" {
-			return nil, errors.New("pctx.NodeId is set, but no node was returned")
-		}
 	}
 	return nil, nil
 }
@@ -667,6 +790,72 @@ func (nodeDb *NodeDb) selectNodeForPodWithIt(
 	return selectedNode, nil
 }
 
+// selectNodeForJobWithFairPreemption returns a node onto which the provided job could be scheduled, or nil if none can be found.
+// Specifically, it returns the node for which scheduling would result in the most "fair" preemptions.
+//
+// It does this by considering all evicted jobs in the reverse order they would be scheduled in and preventing
+// from being re-scheduled the jobs that would be scheduled last.
+func (nodeDb *NodeDb) selectNodeForJobWithFairPreemption(txn *memdb.Txn, jctx *schedulercontext.JobSchedulingContext) (*Node, error) {
+	pctx := jctx.PodSchedulingContext
+	var selectedNode *Node
+	nodesById := make(map[string]*Node)
+	evictedJobSchedulingContextsByNodeId := make(map[string][]*EvictedJobSchedulingContext)
+	it, err := txn.ReverseLowerBound("evictedJobs", "index", math.MaxInt)
+	if err != nil {
+		return nil, errors.WithStack(err)
+	}
+	for obj := it.Next(); obj != nil && selectedNode == nil; obj = it.Next() {
+		evictedJobSchedulingContext := obj.(*EvictedJobSchedulingContext)
+		evictedJctx := evictedJobSchedulingContext.JobSchedulingContext
+		evictedReq := evictedJctx.PodRequirements
+
+		nodeId, ok := evictedReq.NodeSelector[schedulerconfig.NodeIdLabel]
+		if !ok {
+			return nil, errors.Errorf("evicted job %s does not have a nodeIdLabel", evictedJctx.JobId)
+		}
+		node, ok := nodesById[nodeId]
+		if !ok {
+			node, err = nodeDb.GetNodeWithTxn(txn, nodeId)
+			if err != nil {
+				return nil, errors.WithStack(err)
+			}
+		}
+		node, err = UnbindJobFromNode(nodeDb.priorityClasses, evictedJctx.Job, node)
+		if err != nil {
+			return nil, err
+		}
+		nodesById[nodeId] = node
+		evictedJobSchedulingContextsByNodeId[nodeId] = append(evictedJobSchedulingContextsByNodeId[nodeId], evictedJobSchedulingContext)
+
+		matches, _, reason, err := schedulerobjects.PodRequirementsMet(
+			node.Taints,
+			node.Labels,
+			node.TotalResources,
+			node.AllocatableByPriority[evictedPriority],
+			jctx.PodRequirements,
+		)
+		if err != nil {
+			return nil, err
+		}
+		if matches {
+			selectedNode = node
+		} else {
+			s := nodeDb.stringFromPodRequirementsNotMetReason(reason)
+			pctx.NumExcludedNodesByReason[s] += 1
+		}
+	}
+	if selectedNode != nil {
+		pctx.NodeId = selectedNode.Id
+		pctx.ScheduledAtPriority = jctx.PodRequirements.Priority
+		for _, evictedJobSchedulingContext := range evictedJobSchedulingContextsByNodeId[selectedNode.Id] {
+			if err := txn.Delete("evictedJobs", evictedJobSchedulingContext); err != nil {
+				return nil, errors.WithStack(err)
+			}
+		}
+	}
+	return selectedNode, nil
+}
+
 // bindJobToNode returns a copy of node with job bound to it.
 func bindJobToNode(priorityClasses map[string]types.PriorityClass, job interfaces.LegacySchedulerJob, node *Node) (*Node, error) {
 	node = node.UnsafeCopy()
@@ -801,7 +990,8 @@ func unbindJobFromNodeInPlace(priorityClasses map[string]types.PriorityClass, jo
 	delete(node.EvictedJobRunIds, jobId)
 
 	if _, ok := node.AllocatedByJobId[jobId]; !ok {
-		return errors.Errorf("job %s has no resources allocated on node %s", jobId, node.Id)
+		// Job already unbound; nothing more to do.
+		return nil
 	} else {
 		delete(node.AllocatedByJobId, jobId)
 	}
@@ -878,8 +1068,8 @@ func (nodeDb *NodeDb) Upsert(node *Node) error {
 }
 
 func (nodeDb *NodeDb) UpsertWithTxn(txn *memdb.Txn, node *Node) error {
-	keys := make([][]byte, len(nodeDb.prioritiesToTryAssigningAt))
-	for i, p := range nodeDb.prioritiesToTryAssigningAt {
+	keys := make([][]byte, len(nodeDb.nodeDbPriorities))
+	for i, p := range nodeDb.nodeDbPriorities {
 		keys[i] = nodeDb.nodeDbKey(keys[i], node.NodeTypeId, node.AllocatableByPriority[p])
 	}
 	node.Keys = keys
@@ -902,7 +1092,7 @@ func (nodeDb *NodeDb) ClearAllocated() error {
 	for node := it.NextNode(); node != nil; node = it.NextNode() {
 		node = node.UnsafeCopy()
 		node.AllocatableByPriority = schedulerobjects.NewAllocatableByPriorityAndResourceType(
-			nodeDb.prioritiesToTryAssigningAt,
+			nodeDb.nodeDbPriorities,
 			node.TotalResources,
 		)
 		newNodes = append(newNodes, node)
@@ -914,8 +1104,31 @@ func (nodeDb *NodeDb) ClearAllocated() error {
 	return nil
 }
 
+func (nodeDb *NodeDb) AddEvictedJobSchedulingContextWithTxn(txn *memdb.Txn, index int, jctx *schedulercontext.JobSchedulingContext) error {
+	if it, err := txn.Get("evictedJobs", "id", jctx.JobId); err != nil {
+		return errors.WithStack(err)
+	} else if obj := it.Next(); obj != nil {
+		return errors.Errorf("tried to insert evicted job %s with duplicate index %d", jctx.JobId, index)
+	}
+	if err := txn.Insert("evictedJobs", &EvictedJobSchedulingContext{JobId: jctx.JobId, Index: index, JobSchedulingContext: jctx}); err != nil {
+		return errors.WithStack(err)
+	}
+	return nil
+}
+
 func nodeDbSchema(priorities []int32, resources []string) (*memdb.DBSchema, map[int32]string) {
-	indexes := make(map[string]*memdb.IndexSchema)
+	nodesTable, indexNameByPriority := nodesTableSchema(priorities, resources)
+	evictionsTable := evictionsTableSchema()
+	return &memdb.DBSchema{
+		Tables: map[string]*memdb.TableSchema{
+			nodesTable.Name:     nodesTable,
+			evictionsTable.Name: evictionsTable,
+		},
+	}, indexNameByPriority
+}
+
+func nodesTableSchema(priorities []int32, resources []string) (*memdb.TableSchema, map[int32]string) {
+	indexes := make(map[string]*memdb.IndexSchema, len(priorities)+1)
 	indexes["id"] = &memdb.IndexSchema{
 		Name:    "id",
 		Unique:  true,
@@ -931,14 +1144,28 @@ func nodeDbSchema(priorities []int32, resources []string) (*memdb.DBSchema, map[
 			Indexer: &NodeIndex{KeyIndex: i},
 		}
 	}
-	return &memdb.DBSchema{
-		Tables: map[string]*memdb.TableSchema{
-			"nodes": {
-				Name:    "nodes",
-				Indexes: indexes,
+	return &memdb.TableSchema{
+		Name:    "nodes",
+		Indexes: indexes,
+	}, indexNameByPriority
+}
+
+func evictionsTableSchema() *memdb.TableSchema {
+	return &memdb.TableSchema{
+		Name: "evictedJobs",
+		Indexes: map[string]*memdb.IndexSchema{
+			"id": {
+				Name:    "id",
+				Unique:  true,
+				Indexer: &memdb.StringFieldIndex{Field: "JobId"},
+			},
+			"index": {
+				Name:    "index",
+				Unique:  true,
+				Indexer: &memdb.IntFieldIndex{Field: "Index"},
 			},
 		},
-	}, indexNameByPriority
+	}
 }
 
 func nodeIndexName(keyIndex int) string {
diff --git a/internal/scheduler/preempting_queue_scheduler.go b/internal/scheduler/preempting_queue_scheduler.go
index a4a38bde..ba753af0 100644
--- a/internal/scheduler/preempting_queue_scheduler.go
+++ b/internal/scheduler/preempting_queue_scheduler.go
@@ -20,9 +20,11 @@ import (
 	schedulerconfig "github.com/armadaproject/armada/internal/scheduler/configuration"
 	schedulerconstraints "github.com/armadaproject/armada/internal/scheduler/constraints"
 	schedulercontext "github.com/armadaproject/armada/internal/scheduler/context"
+	"github.com/armadaproject/armada/internal/scheduler/fairness"
 	"github.com/armadaproject/armada/internal/scheduler/interfaces"
 	"github.com/armadaproject/armada/internal/scheduler/jobdb"
 	"github.com/armadaproject/armada/internal/scheduler/nodedb"
+	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
 )
 
 // PreemptingQueueScheduler is a scheduler that makes a unified decisions on which jobs to preempt and schedule.
@@ -47,6 +49,8 @@ type PreemptingQueueScheduler struct {
 	skipUnsuccessfulSchedulingKeyCheck bool
 	// If true, asserts that the nodeDb state is consistent with expected changes.
 	enableAssertions bool
+	// If true, a newer preemption strategy is used.
+	enableNewPreemptionStrategy bool
 }
 
 func NewPreemptingQueueScheduler(
@@ -96,29 +100,17 @@ func (sch *PreemptingQueueScheduler) SkipUnsuccessfulSchedulingKeyCheck() {
 	sch.skipUnsuccessfulSchedulingKeyCheck = true
 }
 
+func (sch *PreemptingQueueScheduler) EnableNewPreemptionStrategy() {
+	sch.enableNewPreemptionStrategy = true
+	sch.nodeDb.EnableNewPreemptionStrategy()
+}
+
 // Schedule
 // - preempts jobs belonging to queues with total allocation above their fair share and
 // - schedules new jobs belonging to queues with total allocation less than their fair share.
 func (sch *PreemptingQueueScheduler) Schedule(ctx context.Context) (*SchedulerResult, error) {
 	log := ctxlogrus.Extract(ctx)
 	log = log.WithField("service", "PreemptingQueueScheduler")
-	if sch.schedulingContext.TotalResources.AsWeightedMillis(sch.schedulingContext.ResourceScarcity) == 0 {
-		// This refers to resources available across all clusters, i.e.,
-		// it may include resources not currently considered for scheduling.
-		log.Infof(
-			"no resources with non-zero weight available for scheduling on any cluster: resource scarcity %v, total resources %v",
-			sch.schedulingContext.ResourceScarcity, sch.schedulingContext.TotalResources,
-		)
-		return &SchedulerResult{}, nil
-	}
-	if rl := sch.nodeDb.TotalResources(); rl.AsWeightedMillis(sch.schedulingContext.ResourceScarcity) == 0 {
-		// This refers to the resources currently considered for scheduling.
-		log.Infof(
-			"no resources with non-zero weight available for scheduling in NodeDb: resource scarcity %v, total resources %v",
-			sch.schedulingContext.ResourceScarcity, sch.nodeDb.TotalResources(),
-		)
-		return &SchedulerResult{}, nil
-	}
 	defer func() {
 		sch.schedulingContext.Finished = time.Now()
 	}()
@@ -154,7 +146,7 @@ func (sch *PreemptingQueueScheduler) Schedule(ctx context.Context) (*SchedulerRe
 				}
 				if qctx, ok := sch.schedulingContext.QueueSchedulingContexts[job.GetQueue()]; ok {
 					fairShare := qctx.Weight / sch.schedulingContext.WeightSum
-					actualShare := qctx.TotalCostForQueue() / totalCost
+					actualShare := sch.schedulingContext.FairnessCostProvider.CostFromQueue(qctx) / totalCost
 					fractionOfFairShare := actualShare / fairShare
 					if fractionOfFairShare <= sch.protectedFractionOfFairShare {
 						return false
@@ -342,6 +334,15 @@ func (sch *PreemptingQueueScheduler) evict(ctx context.Context, evictor *Evictor
 	inMemoryJobRepo := NewInMemoryJobRepository(sch.schedulingContext.PriorityClasses)
 	inMemoryJobRepo.EnqueueMany(evictedJobs)
 	txn.Commit()
+
+	if sch.enableNewPreemptionStrategy {
+		if err := sch.nodeDb.Reset(); err != nil {
+			return nil, nil, err
+		}
+		if err := addEvictedJobsToNodeDb(ctx, sch.schedulingContext, sch.nodeDb, inMemoryJobRepo); err != nil {
+			return nil, nil, err
+		}
+	}
 	return result, inMemoryJobRepo, nil
 }
 
@@ -479,6 +480,78 @@ func (sch *PreemptingQueueScheduler) evictionAssertions(evictedJobsById map[stri
 	return nil
 }
 
+type MinimalQueueRepository struct {
+	queues map[string]MinimalQueue
+}
+
+func (qr *MinimalQueueRepository) GetQueue(name string) (fairness.Queue, bool) {
+	queue, ok := qr.queues[name]
+	return queue, ok
+}
+
+func NewMinimalQueueRepositoryFromSchedulingContext(sctx *schedulercontext.SchedulingContext) *MinimalQueueRepository {
+	queues := make(map[string]MinimalQueue, len(sctx.QueueSchedulingContexts))
+	for name, qctx := range sctx.QueueSchedulingContexts {
+		queues[name] = MinimalQueue{allocation: qctx.Allocated.DeepCopy(), weight: qctx.Weight}
+	}
+	return &MinimalQueueRepository{queues: queues}
+}
+
+type MinimalQueue struct {
+	allocation schedulerobjects.ResourceList
+	weight     float64
+}
+
+func (q MinimalQueue) GetAllocation() schedulerobjects.ResourceList {
+	return q.allocation
+}
+
+func (q MinimalQueue) GetWeight() float64 {
+	return q.weight
+}
+
+// addEvictedJobsToNodeDb adds evicted jobs to the NodeDb.
+// Needed to enable the nodeDb accounting for these when preempting.
+func addEvictedJobsToNodeDb(ctx context.Context, sctx *schedulercontext.SchedulingContext, nodeDb *nodedb.NodeDb, inMemoryJobRepo *InMemoryJobRepository) error {
+	gangItByQueue := make(map[string]*QueuedGangIterator)
+	for _, qctx := range sctx.QueueSchedulingContexts {
+		jobIt, err := inMemoryJobRepo.GetJobIterator(ctx, qctx.Queue)
+		if err != nil {
+			return err
+		}
+		gangItByQueue[qctx.Queue] = NewQueuedGangIterator(sctx, jobIt, 0)
+	}
+	qr := NewMinimalQueueRepositoryFromSchedulingContext(sctx)
+	candidateGangIterator, err := NewCandidateGangIterator(qr, sctx.FairnessCostProvider, gangItByQueue)
+	if err != nil {
+		return err
+	}
+	txn := nodeDb.Txn(true)
+	defer txn.Abort()
+	i := 0
+	for {
+		if gctx, err := candidateGangIterator.Peek(); err != nil {
+			return err
+		} else if gctx == nil {
+			break
+		} else {
+			for _, jctx := range gctx.JobSchedulingContexts {
+				if err := nodeDb.AddEvictedJobSchedulingContextWithTxn(txn, i, jctx); err != nil {
+					return err
+				}
+				i++
+			}
+			q := qr.queues[gctx.Queue]
+			q.allocation.Add(gctx.TotalResourceRequests)
+		}
+		if err := candidateGangIterator.Clear(); err != nil {
+			return err
+		}
+	}
+	txn.Commit()
+	return nil
+}
+
 func (sch *PreemptingQueueScheduler) schedule(ctx context.Context, inMemoryJobRepo *InMemoryJobRepository, jobRepo JobRepository) (*SchedulerResult, error) {
 	jobIteratorByQueue := make(map[string]JobIterator)
 	for _, qctx := range sch.schedulingContext.QueueSchedulingContexts {
diff --git a/internal/scheduler/queue_scheduler.go b/internal/scheduler/queue_scheduler.go
index 97160396..ba6c223f 100644
--- a/internal/scheduler/queue_scheduler.go
+++ b/internal/scheduler/queue_scheduler.go
@@ -6,13 +6,13 @@ import (
 	"reflect"
 	"time"
 
-	"github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus"
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
 
 	"github.com/armadaproject/armada/internal/common/logging"
 	schedulerconstraints "github.com/armadaproject/armada/internal/scheduler/constraints"
 	schedulercontext "github.com/armadaproject/armada/internal/scheduler/context"
+	"github.com/armadaproject/armada/internal/scheduler/fairness"
 	"github.com/armadaproject/armada/internal/scheduler/interfaces"
 	"github.com/armadaproject/armada/internal/scheduler/nodedb"
 	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
@@ -45,7 +45,7 @@ func NewQueueScheduler(
 	for queue, it := range jobIteratorByQueue {
 		gangIteratorsByQueue[queue] = NewQueuedGangIterator(sctx, it, constraints.MaxQueueLookback)
 	}
-	candidateGangIterator, err := NewCandidateGangIterator(sctx, gangIteratorsByQueue)
+	candidateGangIterator, err := NewCandidateGangIterator(sctx, sctx.FairnessCostProvider, gangIteratorsByQueue)
 	if err != nil {
 		return nil, err
 	}
@@ -61,24 +61,6 @@ func (sch *QueueScheduler) SkipUnsuccessfulSchedulingKeyCheck() {
 }
 
 func (sch *QueueScheduler) Schedule(ctx context.Context) (*SchedulerResult, error) {
-	log := ctxlogrus.Extract(ctx)
-	if sch.schedulingContext.TotalResources.AsWeightedMillis(sch.schedulingContext.ResourceScarcity) == 0 {
-		// This refers to resources available across all clusters, i.e.,
-		// it may include resources not currently considered for scheduling.
-		log.Infof(
-			"no resources with non-zero weight available for scheduling on any cluster: resource scarcity %v, total resources %v",
-			sch.schedulingContext.ResourceScarcity, sch.schedulingContext.TotalResources,
-		)
-		return &SchedulerResult{}, nil
-	}
-	if rl := sch.gangScheduler.nodeDb.TotalResources(); rl.AsWeightedMillis(sch.schedulingContext.ResourceScarcity) == 0 {
-		// This refers to the resources currently considered for scheduling.
-		log.Infof(
-			"no resources with non-zero weight available for scheduling in NodeDb: resource scarcity %v, total resources %v",
-			sch.schedulingContext.ResourceScarcity, sch.gangScheduler.nodeDb.TotalResources(),
-		)
-		return &SchedulerResult{}, nil
-	}
 	nodeIdByJobId := make(map[string]string)
 	scheduledJobs := make([]interfaces.LegacySchedulerJob, 0)
 	for {
@@ -272,7 +254,8 @@ func (it *QueuedGangIterator) hitLookbackLimit() bool {
 // Specifically, it yields the next gang in the queue with smallest fraction of its fair share,
 // where the fraction of fair share computation includes the yielded gang.
 type CandidateGangIterator struct {
-	SchedulingContext *schedulercontext.SchedulingContext
+	queueProvier         fairness.QueueRepository
+	fairnessCostProvider fairness.FairnessCostProvider
 	// If true, this iterator only yields gangs where all jobs are evicted.
 	onlyYieldEvicted bool
 	// Reusable buffer to avoid allocations.
@@ -283,13 +266,15 @@ type CandidateGangIterator struct {
 }
 
 func NewCandidateGangIterator(
-	sctx *schedulercontext.SchedulingContext,
+	queueProvier fairness.QueueRepository,
+	fairnessCostProvider fairness.FairnessCostProvider,
 	iteratorsByQueue map[string]*QueuedGangIterator,
 ) (*CandidateGangIterator, error) {
 	it := &CandidateGangIterator{
-		SchedulingContext: sctx,
-		buffer:            schedulerobjects.NewResourceListWithDefaultSize(),
-		pq:                make(QueueCandidateGangIteratorPQ, 0, len(iteratorsByQueue)),
+		queueProvier:         queueProvier,
+		fairnessCostProvider: fairnessCostProvider,
+		buffer:               schedulerobjects.NewResourceListWithDefaultSize(),
+		pq:                   make(QueueCandidateGangIteratorPQ, 0, len(iteratorsByQueue)),
 	}
 	for queue, queueIt := range iteratorsByQueue {
 		if _, err := it.updateAndPushPQItem(it.newPQItem(queue, queueIt)); err != nil {
@@ -328,7 +313,7 @@ func (it *CandidateGangIterator) updateAndPushPQItem(item *QueueCandidateGangIte
 
 func (it *CandidateGangIterator) updatePQItem(item *QueueCandidateGangIteratorItem) error {
 	item.gctx = nil
-	item.fractionOfFairShare = 0
+	item.queueCost = 0
 	gctx, err := item.it.Peek()
 	if err != nil {
 		return err
@@ -340,17 +325,24 @@ func (it *CandidateGangIterator) updatePQItem(item *QueueCandidateGangIteratorIt
 		return errors.Errorf("mismatched queue %s and %s for gctx", gctx.Queue, item.queue)
 	}
 	item.gctx = gctx
-	item.fractionOfFairShare = it.fractionOfFairShareWithGctx(gctx)
+	cost, err := it.queueCostWithGctx(gctx)
+	if err != nil {
+		return err
+	}
+	item.queueCost = cost
 	return nil
 }
 
-// fractionOfFairShareWithGctx returns the fraction of its fair share this queue would have if the jobs in gctx were scheduled.
-func (it *CandidateGangIterator) fractionOfFairShareWithGctx(gctx *schedulercontext.GangSchedulingContext) float64 {
-	qctx := it.SchedulingContext.QueueSchedulingContexts[gctx.Queue]
+// queueCostWithGctx returns the cost associated with a queue if gctx were to be scheduled.
+func (it *CandidateGangIterator) queueCostWithGctx(gctx *schedulercontext.GangSchedulingContext) (float64, error) {
+	queue, ok := it.queueProvier.GetQueue(gctx.Queue)
+	if !ok {
+		return 0, errors.Errorf("unknown queue %s", gctx.Queue)
+	}
 	it.buffer.Zero()
-	it.buffer.Add(qctx.Allocated)
+	it.buffer.Add(queue.GetAllocation())
 	it.buffer.Add(gctx.TotalResourceRequests)
-	return qctx.TotalCostForQueueWithAllocation(it.buffer)
+	return it.fairnessCostProvider.CostFromAllocationAndWeight(it.buffer, queue.GetWeight()), nil
 }
 
 // Clear removes the first item in the iterator.
@@ -391,9 +383,9 @@ type QueueCandidateGangIteratorItem struct {
 	// Most recent value produced by the iterator.
 	// Cached here to avoid repeating scheduling checks unnecessarily.
 	gctx *schedulercontext.GangSchedulingContext
-	// Fraction of its fair share this queue would have
-	// if its next schedulable job were to be scheduled.
-	fractionOfFairShare float64
+	// Cost associated with the queue if the topmost gang in the queue were to be scheduled.
+	// Used to order queues fairly.
+	queueCost float64
 	// The index of the item in the heap.
 	// maintained by the heap.Interface methods.
 	index int
@@ -403,10 +395,10 @@ func (pq QueueCandidateGangIteratorPQ) Len() int { return len(pq) }
 
 func (pq QueueCandidateGangIteratorPQ) Less(i, j int) bool {
 	// Tie-break by queue name.
-	if pq[i].fractionOfFairShare == pq[j].fractionOfFairShare {
+	if pq[i].queueCost == pq[j].queueCost {
 		return pq[i].queue < pq[j].queue
 	}
-	return pq[i].fractionOfFairShare < pq[j].fractionOfFairShare
+	return pq[i].queueCost < pq[j].queueCost
 }
 
 func (pq QueueCandidateGangIteratorPQ) Swap(i, j int) {
diff --git a/internal/scheduler/scheduling_algo.go b/internal/scheduler/scheduling_algo.go
index fc3a7414..2c925d46 100644
--- a/internal/scheduler/scheduling_algo.go
+++ b/internal/scheduler/scheduling_algo.go
@@ -21,6 +21,7 @@ import (
 	schedulerconstraints "github.com/armadaproject/armada/internal/scheduler/constraints"
 	schedulercontext "github.com/armadaproject/armada/internal/scheduler/context"
 	"github.com/armadaproject/armada/internal/scheduler/database"
+	"github.com/armadaproject/armada/internal/scheduler/fairness"
 	"github.com/armadaproject/armada/internal/scheduler/interfaces"
 	"github.com/armadaproject/armada/internal/scheduler/jobdb"
 	"github.com/armadaproject/armada/internal/scheduler/nodedb"
@@ -349,17 +350,30 @@ func (l *FairSchedulingAlgo) scheduleOnExecutors(
 	if len(executors) == 1 {
 		executorId = executors[0].Id
 	}
+	totalResources := fsctx.totalCapacityByPool[pool]
+	var fairnessCostProvider fairness.FairnessCostProvider
+	if l.schedulingConfig.FairnessModel == configuration.DominantResourceFairness {
+		fairnessCostProvider, err = fairness.NewDominantResourceFairness(
+			totalResources,
+			l.schedulingConfig.DominantResourceFairnessResourcesToConsider,
+		)
+		if err != nil {
+			return nil, nil, err
+		}
+	} else {
+		fairnessCostProvider, err = fairness.NewAssetFairness(l.schedulingConfig.ResourceScarcity)
+		if err != nil {
+			return nil, nil, err
+		}
+	}
 	sctx := schedulercontext.NewSchedulingContext(
 		executorId,
 		pool,
 		l.schedulingConfig.Preemption.PriorityClasses,
 		l.schedulingConfig.Preemption.DefaultPriorityClass,
-		l.schedulingConfig.ResourceScarcity,
-		fsctx.totalCapacityByPool[pool],
+		fairnessCostProvider,
+		totalResources,
 	)
-	if l.schedulingConfig.FairnessModel == configuration.DominantResourceFairness {
-		sctx.EnableDominantResourceFairness(l.schedulingConfig.DominantResourceFairnessResourcesToConsider)
-	}
 	for queue, priorityFactor := range fsctx.priorityFactorByQueue {
 		if !fsctx.isActiveByQueueName[queue] {
 			// To ensure fair share is computed only from active queues, i.e., queues with jobs queued or running.
@@ -401,6 +415,9 @@ func (l *FairSchedulingAlgo) scheduleOnExecutors(
 	if l.schedulingConfig.EnableAssertions {
 		scheduler.EnableAssertions()
 	}
+	if l.schedulingConfig.EnableNewPreemptionStrategy {
+		scheduler.EnableNewPreemptionStrategy()
+	}
 	result, err := scheduler.Schedule(ctx)
 	if err != nil {
 		return nil, nil, err


Test patch:
diff --git a/internal/scheduler/context/context_test.go b/internal/scheduler/context/context_test.go
index 05e17c24..3b932f30 100644
--- a/internal/scheduler/context/context_test.go
+++ b/internal/scheduler/context/context_test.go
@@ -9,6 +9,7 @@ import (
 	"k8s.io/apimachinery/pkg/api/resource"
 
 	armadaslices "github.com/armadaproject/armada/internal/common/slices"
+	"github.com/armadaproject/armada/internal/scheduler/fairness"
 	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
 	"github.com/armadaproject/armada/internal/scheduler/testfixtures"
 )
@@ -33,13 +34,16 @@ func TestNewGangSchedulingContext(t *testing.T) {
 }
 
 func TestSchedulingContextAccounting(t *testing.T) {
+	totalResources := schedulerobjects.ResourceList{Resources: map[string]resource.Quantity{"cpu": resource.MustParse("1")}}
+	fairnessCostProvider, err := fairness.NewAssetFairness(map[string]float64{"cpu": 1})
+	require.NoError(t, err)
 	sctx := NewSchedulingContext(
 		"executor",
 		"pool",
 		testfixtures.TestPriorityClasses,
 		testfixtures.TestDefaultPriorityClass,
-		map[string]float64{"cpu": 1},
-		schedulerobjects.ResourceList{Resources: map[string]resource.Quantity{"cpu": resource.MustParse("1")}},
+		fairnessCostProvider,
+		totalResources,
 	)
 	priorityFactorByQueue := map[string]float64{"A": 1, "B": 1}
 	allocatedByQueueAndPriorityClass := map[string]schedulerobjects.QuantityByTAndResourceType[string]{
@@ -55,7 +59,7 @@ func TestSchedulingContextAccounting(t *testing.T) {
 	expected := sctx.AllocatedByQueueAndPriority()
 	jctxs := testNSmallCpuJobSchedulingContext("A", testfixtures.TestDefaultPriorityClass, 2)
 	gctx := NewGangSchedulingContext(jctxs)
-	_, err := sctx.AddGangSchedulingContext(gctx)
+	_, err = sctx.AddGangSchedulingContext(gctx)
 	require.NoError(t, err)
 	for _, jctx := range jctxs {
 		_, err := sctx.EvictJob(jctx.Job)
diff --git a/internal/scheduler/gang_scheduler_test.go b/internal/scheduler/gang_scheduler_test.go
index ea7153b0..bab895a0 100644
--- a/internal/scheduler/gang_scheduler_test.go
+++ b/internal/scheduler/gang_scheduler_test.go
@@ -12,6 +12,7 @@ import (
 	armadaslices "github.com/armadaproject/armada/internal/common/slices"
 	schedulerconstraints "github.com/armadaproject/armada/internal/scheduler/constraints"
 	schedulercontext "github.com/armadaproject/armada/internal/scheduler/context"
+	"github.com/armadaproject/armada/internal/scheduler/fairness"
 	"github.com/armadaproject/armada/internal/scheduler/jobdb"
 	"github.com/armadaproject/armada/internal/scheduler/nodedb"
 	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
@@ -327,12 +328,18 @@ func TestGangScheduler(t *testing.T) {
 					priorityFactorByQueue[job.GetQueue()] = 1
 				}
 			}
+
+			fairnessCostProvider, err := fairness.NewDominantResourceFairness(
+				tc.TotalResources,
+				tc.SchedulingConfig.DominantResourceFairnessResourcesToConsider,
+			)
+			require.NoError(t, err)
 			sctx := schedulercontext.NewSchedulingContext(
 				"executor",
 				"pool",
 				tc.SchedulingConfig.Preemption.PriorityClasses,
 				tc.SchedulingConfig.Preemption.DefaultPriorityClass,
-				tc.SchedulingConfig.ResourceScarcity,
+				fairnessCostProvider,
 				tc.TotalResources,
 			)
 			for queue, priorityFactor := range priorityFactorByQueue {
diff --git a/internal/scheduler/nodedb/nodedb_test.go b/internal/scheduler/nodedb/nodedb_test.go
index 78e0198e..50f7f9c5 100644
--- a/internal/scheduler/nodedb/nodedb_test.go
+++ b/internal/scheduler/nodedb/nodedb_test.go
@@ -152,7 +152,7 @@ func TestNodeBindingEvictionUnbinding(t *testing.T) {
 	require.Error(t, err)
 
 	_, err = UnbindJobFromNode(testfixtures.TestPriorityClasses, job, entry)
-	require.Error(t, err)
+	require.NoError(t, err)
 
 	_, err = bindJobToNode(testfixtures.TestPriorityClasses, job, boundNode)
 	require.Error(t, err)
diff --git a/internal/scheduler/nodedb/nodeiteration_test.go b/internal/scheduler/nodedb/nodeiteration_test.go
index 215f1719..54447741 100644
--- a/internal/scheduler/nodedb/nodeiteration_test.go
+++ b/internal/scheduler/nodedb/nodeiteration_test.go
@@ -432,7 +432,7 @@ func TestNodeTypeIterator(t *testing.T) {
 				indexedResourceRequests[i] = tc.resourceRequests.Get(t)
 			}
 			keyIndex := -1
-			for i, p := range nodeDb.prioritiesToTryAssigningAt {
+			for i, p := range nodeDb.nodeDbPriorities {
 				if p == tc.priority {
 					keyIndex = i
 				}
diff --git a/internal/scheduler/preempting_queue_scheduler_test.go b/internal/scheduler/preempting_queue_scheduler_test.go
index 59a82e89..923d7729 100644
--- a/internal/scheduler/preempting_queue_scheduler_test.go
+++ b/internal/scheduler/preempting_queue_scheduler_test.go
@@ -19,6 +19,7 @@ import (
 	armadaslices "github.com/armadaproject/armada/internal/common/slices"
 	schedulerconstraints "github.com/armadaproject/armada/internal/scheduler/constraints"
 	schedulercontext "github.com/armadaproject/armada/internal/scheduler/context"
+	"github.com/armadaproject/armada/internal/scheduler/fairness"
 	"github.com/armadaproject/armada/internal/scheduler/interfaces"
 	"github.com/armadaproject/armada/internal/scheduler/jobdb"
 	"github.com/armadaproject/armada/internal/scheduler/nodedb"
@@ -1355,17 +1356,19 @@ func TestPreemptingQueueScheduler(t *testing.T) {
 					tc.TotalResources = nodeDb.TotalResources()
 				}
 
+				fairnessCostProvider, err := fairness.NewDominantResourceFairness(
+					nodeDb.TotalResources(),
+					tc.SchedulingConfig.DominantResourceFairnessResourcesToConsider,
+				)
+				require.NoError(t, err)
 				sctx := schedulercontext.NewSchedulingContext(
 					"executor",
 					"pool",
 					tc.SchedulingConfig.Preemption.PriorityClasses,
 					tc.SchedulingConfig.Preemption.DefaultPriorityClass,
-					tc.SchedulingConfig.ResourceScarcity,
+					fairnessCostProvider,
 					tc.TotalResources,
 				)
-				if tc.SchedulingConfig.FairnessModel == configuration.DominantResourceFairness {
-					sctx.EnableDominantResourceFairness(tc.SchedulingConfig.DominantResourceFairnessResourcesToConsider)
-				}
 				for queue, priorityFactor := range tc.PriorityFactorByQueue {
 					weight := 1 / priorityFactor
 					err := sctx.AddQueueSchedulingContext(queue, weight, allocatedByQueueAndPriorityClass[queue])
@@ -1390,6 +1393,9 @@ func TestPreemptingQueueScheduler(t *testing.T) {
 					gangIdByJobId,
 				)
 				sch.EnableAssertions()
+				if tc.SchedulingConfig.EnableNewPreemptionStrategy {
+					sch.EnableNewPreemptionStrategy()
+				}
 				result, err := sch.Schedule(ctxlogrus.ToContext(context.Background(), log))
 				require.NoError(t, err)
 				jobIdsByGangId = sch.jobIdsByGangId
@@ -1628,12 +1634,17 @@ func BenchmarkPreemptingQueueScheduler(b *testing.B) {
 			}
 			jobRepo.EnqueueMany(jobs)
 
+			fairnessCostProvider, err := fairness.NewDominantResourceFairness(
+				nodeDb.TotalResources(),
+				tc.SchedulingConfig.DominantResourceFairnessResourcesToConsider,
+			)
+			require.NoError(b, err)
 			sctx := schedulercontext.NewSchedulingContext(
 				"executor",
 				"pool",
 				tc.SchedulingConfig.Preemption.PriorityClasses,
 				tc.SchedulingConfig.Preemption.DefaultPriorityClass,
-				tc.SchedulingConfig.ResourceScarcity,
+				fairnessCostProvider,
 				nodeDb.TotalResources(),
 			)
 			for queue, priorityFactor := range priorityFactorByQueue {
@@ -1694,7 +1705,7 @@ func BenchmarkPreemptingQueueScheduler(b *testing.B) {
 					"pool",
 					tc.SchedulingConfig.Preemption.PriorityClasses,
 					tc.SchedulingConfig.Preemption.DefaultPriorityClass,
-					tc.SchedulingConfig.ResourceScarcity,
+					fairnessCostProvider,
 					nodeDb.TotalResources(),
 				)
 				for queue, priorityFactor := range priorityFactorByQueue {
diff --git a/internal/scheduler/queue_scheduler_test.go b/internal/scheduler/queue_scheduler_test.go
index 84d42dd3..d24c6ed3 100644
--- a/internal/scheduler/queue_scheduler_test.go
+++ b/internal/scheduler/queue_scheduler_test.go
@@ -16,6 +16,7 @@ import (
 	"github.com/armadaproject/armada/internal/common/util"
 	schedulerconstraints "github.com/armadaproject/armada/internal/scheduler/constraints"
 	schedulercontext "github.com/armadaproject/armada/internal/scheduler/context"
+	"github.com/armadaproject/armada/internal/scheduler/fairness"
 	"github.com/armadaproject/armada/internal/scheduler/interfaces"
 	"github.com/armadaproject/armada/internal/scheduler/jobdb"
 	"github.com/armadaproject/armada/internal/scheduler/nodedb"
@@ -450,12 +451,17 @@ func TestQueueScheduler(t *testing.T) {
 			jobRepo := NewInMemoryJobRepository(tc.SchedulingConfig.Preemption.PriorityClasses)
 			jobRepo.EnqueueMany(legacySchedulerJobs)
 
+			fairnessCostProvider, err := fairness.NewDominantResourceFairness(
+				tc.TotalResources,
+				tc.SchedulingConfig.DominantResourceFairnessResourcesToConsider,
+			)
+			require.NoError(t, err)
 			sctx := schedulercontext.NewSchedulingContext(
 				"executor",
 				"pool",
 				tc.SchedulingConfig.Preemption.PriorityClasses,
 				tc.SchedulingConfig.Preemption.DefaultPriorityClass,
-				tc.SchedulingConfig.ResourceScarcity,
+				fairnessCostProvider,
 				tc.TotalResources,
 			)
 			for queue, priorityFactor := range tc.PriorityFactorByQueue {
diff --git a/internal/scheduler/simulator/testdata/diva-plat.yaml b/internal/scheduler/simulator/testdata/diva-plat.yaml
new file mode 100644
index 00000000..a4106287
--- /dev/null
+++ b/internal/scheduler/simulator/testdata/diva-plat.yaml
@@ -0,0 +1,30 @@
+name: "DIVA-plat"
+pools:
+  - name: "CPU"
+    executorGroups:
+      - executors:
+          - name: "Executor-CPU-1"
+            nodeTemplates:
+              - number: 1
+                totalResources:
+                  resources:
+                    cpu: "1"
+                    memory: "1Gi"
+          - name: "Executor-CPU-2"
+            nodeTemplates:
+              - number: 2
+                totalResources:
+                  resources:
+                    cpu: "1"
+                    memory: "1Gi"                    
+  - name: "GPU"
+    executorGroups:                    
+      - executors:
+          - name: "Executor-GPU"
+            nodeTemplates:
+              - number: 2
+                totalResources:
+                  resources:
+                    cpu: "1"
+                    memory: "1Gi"                    
+                    ndivia.com/gpu: "1"
\ No newline at end of file
diff --git a/internal/scheduler/testfixtures/testfixtures.go b/internal/scheduler/testfixtures/testfixtures.go
index bbd91d2e..3af40773 100644
--- a/internal/scheduler/testfixtures/testfixtures.go
+++ b/internal/scheduler/testfixtures/testfixtures.go
@@ -100,6 +100,7 @@ func TestSchedulingConfig() configuration.SchedulingConfig {
 		DominantResourceFairnessResourcesToConsider: TestResourceNames,
 		ExecutorTimeout:                  15 * time.Minute,
 		MaxUnacknowledgedJobsPerExecutor: math.MaxInt,
+		EnableNewPreemptionStrategy:      true,
 	}
 }