Repository: armadaproject/armada
Pull request #2426 (2023-05-05 11:37:15)

Fix scheduling key hashing
Hashing benchmarks
Benchmark without affinity: 1.94e-06 s/op, i.e., 515463 op/s.
Benchmark with affinity: 4.2157e-05 s/op, i.e., 23720 op/s.


┆Issue is synchronized with this [Jira Task](https://gr-oss.atlassian.net/browse/BATCH-233) by [Unito](https://www.unito.io)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/armadaproject/armada/pull/2426?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject) Report
Patch coverage: **`73.24`**% and project coverage change: **`+0.03`** :tada:
> Comparison is base [(`e433417`)](https://app.codecov.io/gh/armadaproject/armada/commit/e4334172a65844b86d79b3b2f336e86a266b2dbe?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject) 58.55% compared to head [(`038048c`)](https://app.codecov.io/gh/armadaproject/armada/pull/2426?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject) 58.59%.

> :exclamation: Current head 038048c differs from pull request most recent head efc4285. Consider uploading reports for the commit efc4285 to get more accurate results

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


```diff
@@            Coverage Diff             @@
##           master    #2426      +/-   ##
==========================================
+ Coverage   58.55%   58.59%   +0.03%     
==========================================
  Files         231      231              
  Lines       28815    28893      +78     
==========================================
+ Hits        16874    16931      +57     
- Misses      10638    10665      +27     
+ Partials     1303     1297       -6     
```

| Flag | Coverage Δ | |
|---|---|---|
| armada-server | `58.59% <73.24%> (+0.03%)` | :arrow_up: |

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.

| [Impacted Files](https://app.codecov.io/gh/armadaproject/armada/pull/2426?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject) | Coverage Δ | |
|---|---|---|
| [internal/scheduler/context/context.go](https://app.codecov.io/gh/armadaproject/armada/pull/2426?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvc2NoZWR1bGVyL2NvbnRleHQvY29udGV4dC5nbw==) | `45.79% <ø> (ø)` | |
| [internal/scheduler/pool\_assigner.go](https://app.codecov.io/gh/armadaproject/armada/pull/2426?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvc2NoZWR1bGVyL3Bvb2xfYXNzaWduZXIuZ28=) | `72.72% <57.14%> (+0.27%)` | :arrow_up: |
| [internal/scheduler/schedulerobjects/podutils.go](https://app.codecov.io/gh/armadaproject/armada/pull/2426?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvc2NoZWR1bGVyL3NjaGVkdWxlcm9iamVjdHMvcG9kdXRpbHMuZ28=) | `63.15% <62.50%> (-6.85%)` | :arrow_down: |
| [internal/common/proto/protoutil.go](https://app.codecov.io/gh/armadaproject/armada/pull/2426?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvY29tbW9uL3Byb3RvL3Byb3RvdXRpbC5nbw==) | `60.00% <100.00%> (+0.81%)` | :arrow_up: |
| [internal/common/slices/slices.go](https://app.codecov.io/gh/armadaproject/armada/pull/2426?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvY29tbW9uL3NsaWNlcy9zbGljZXMuZ28=) | `93.61% <100.00%> (+0.75%)` | :arrow_up: |
| [internal/scheduler/nodedb/nodeiteration.go](https://app.codecov.io/gh/armadaproject/armada/pull/2426?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvc2NoZWR1bGVyL25vZGVkYi9ub2RlaXRlcmF0aW9uLmdv) | `72.46% <100.00%> (-1.58%)` | :arrow_down: |
| [internal/scheduler/preempting\_queue\_scheduler.go](https://app.codecov.io/gh/armadaproject/armada/pull/2426?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvc2NoZWR1bGVyL3ByZWVtcHRpbmdfcXVldWVfc2NoZWR1bGVyLmdv) | `65.21% <100.00%> (+0.05%)` | :arrow_up: |
| [internal/scheduler/scheduler.go](https://app.codecov.io/gh/armadaproject/armada/pull/2426?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvc2NoZWR1bGVyL3NjaGVkdWxlci5nbw==) | `77.72% <100.00%> (+0.33%)` | :arrow_up: |
| [internal/scheduler/schedulerobjects/quantity.go](https://app.codecov.io/gh/armadaproject/armada/pull/2426?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvc2NoZWR1bGVyL3NjaGVkdWxlcm9iamVjdHMvcXVhbnRpdHkuZ28=) | `100.00% <100.00%> (ø)` | |
| [internal/scheduler/submitcheck.go](https://app.codecov.io/gh/armadaproject/armada/pull/2426?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvc2NoZWR1bGVyL3N1Ym1pdGNoZWNrLmdv) | `78.10% <100.00%> (+1.09%)` | :arrow_up: |
| ... and [1 more](https://app.codecov.io/gh/armadaproject/armada/pull/2426?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/2426?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](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/common/hash/pod_requirements.go b/internal/common/hash/pod_requirements.go
deleted file mode 100644
index 10d9e0d1..00000000
--- a/internal/common/hash/pod_requirements.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package hash
-
-import (
-	"github.com/gogo/protobuf/proto"
-	"github.com/pkg/errors"
-
-	protoutil "github.com/armadaproject/armada/internal/common/proto"
-	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
-)
-
-func CalculatePodRequirementsHash(reqs *schedulerobjects.PodRequirements) ([]byte, error) {
-	if reqs == nil {
-		return []byte{}, errors.Errorf("cannot calculate hash of nil pod requirements")
-	}
-	podReqsCopy := proto.Clone(reqs).(*schedulerobjects.PodRequirements)
-	// Clear annotations, as otherwise it'll cause the hash to be different for equivalent requirements
-	// Ideally long term we'd remove annotations from the PodRequirements
-	podReqsCopy.Annotations = map[string]string{}
-	reqsHash, err := protoutil.Hash(podReqsCopy)
-	if err != nil {
-		return []byte{}, err
-	}
-	return reqsHash, nil
-}
diff --git a/internal/common/proto/protoutil.go b/internal/common/proto/protoutil.go
index 1a363bfc..5745327c 100644
--- a/internal/common/proto/protoutil.go
+++ b/internal/common/proto/protoutil.go
@@ -1,9 +1,6 @@
 package protoutil
 
 import (
-	"crypto/sha1"
-	"encoding/json"
-
 	"github.com/gogo/protobuf/proto"
 	"github.com/pkg/errors"
 
@@ -23,13 +20,13 @@ func DecompressAndUnmarshall[T proto.Message](buf []byte, msg T, decompressor co
 	if err != nil {
 		return msg, err
 	}
-	return Unmarshall[T](decompressed, msg)
+	return Unmarshall(decompressed, msg)
 }
 
 // MustUnmarshall unmarshalls a proto message and panics if the unmarshall fails.  The main use case here is for unit tests.
 // Think carefully if you intend to use this elsewhere
 func MustUnmarshall[T proto.Message](buf []byte, msg T) T {
-	msg, err := Unmarshall[T](buf, msg)
+	msg, err := Unmarshall(buf, msg)
 	if err != nil {
 		panic(errors.Wrap(err, "Error unmarshalling object"))
 	}
@@ -39,7 +36,7 @@ func MustUnmarshall[T proto.Message](buf []byte, msg T) T {
 // MustDecompressAndUnmarshall first decompressed the message and then unmarshalls.  If either of these steps fail then it will panic.
 // The main use case here is for unit tests. Think carefully if you intend to use this elsewhere
 func MustDecompressAndUnmarshall[T proto.Message](buf []byte, msg T, decompressor compress.Decompressor) T {
-	msg, err := DecompressAndUnmarshall[T](buf, msg, decompressor)
+	msg, err := DecompressAndUnmarshall(buf, msg, decompressor)
 	if err != nil {
 		panic(errors.Wrap(err, "Error unmarshalling object"))
 	}
@@ -75,24 +72,3 @@ func MustMarshallAndCompress(msg proto.Message, compressor compress.Compressor)
 	}
 	return b
 }
-
-// Hash produces a 160 bit hash of the supplied proto object
-func Hash(msg proto.Message) ([]byte, error) {
-	return HashMany([]proto.Message{msg})
-}
-
-// HashMany produces a 160 bit hash of the supplied proto objects
-func HashMany[T proto.Message](msgs []T) ([]byte, error) {
-	h := sha1.New()
-	for _, msg := range msgs {
-		b, err := json.Marshal(msg)
-		if err != nil {
-			return nil, err
-		}
-		_, err = h.Write(b)
-		if err != nil {
-			return nil, err
-		}
-	}
-	return h.Sum(nil), nil
-}
diff --git a/internal/common/slices/slices.go b/internal/common/slices/slices.go
index b1f315cd..0f527736 100644
--- a/internal/common/slices/slices.go
+++ b/internal/common/slices/slices.go
@@ -126,3 +126,16 @@ func Subtract[T comparable](list []T, toRemove []T) []T {
 	}
 	return out
 }
+
+func Filter[S ~[]E, E any](s S, predicate func(e E) bool) S {
+	if s == nil {
+		return nil
+	}
+	out := make(S, 0, len(s))
+	for _, e := range s {
+		if predicate(e) {
+			out = append(out, e)
+		}
+	}
+	return out
+}
diff --git a/internal/scheduler/context/context.go b/internal/scheduler/context/context.go
index 49190d76..2864acc3 100644
--- a/internal/scheduler/context/context.go
+++ b/internal/scheduler/context/context.go
@@ -122,7 +122,7 @@ func (sctx *SchedulingContext) AddGangSchedulingContext(gctx *GangSchedulingCont
 }
 
 // AddJobSchedulingContext adds a job scheduling context.
-// Automatically updates scheduled resources
+// Automatically updates scheduled resources.
 func (sctx *SchedulingContext) AddJobSchedulingContext(jctx *JobSchedulingContext) bool {
 	qctx, ok := sctx.QueueSchedulingContexts[jctx.Job.GetQueue()]
 	if !ok {
diff --git a/internal/scheduler/nodedb/nodeiteration.go b/internal/scheduler/nodedb/nodeiteration.go
index 556aab7e..87202668 100644
--- a/internal/scheduler/nodedb/nodeiteration.go
+++ b/internal/scheduler/nodedb/nodeiteration.go
@@ -3,7 +3,6 @@ package nodedb
 import (
 	"bytes"
 	"container/heap"
-	"encoding/binary"
 	"fmt"
 
 	"github.com/hashicorp/go-memdb"
@@ -147,7 +146,7 @@ func (index *NodeAvailableResourceIndex) FromArgs(args ...interface{}) ([]byte,
 	if !ok {
 		return nil, errors.Errorf("expected Quantity, but got %T", args[0])
 	}
-	return encodeQuantity(q), nil
+	return schedulerobjects.EncodeQuantity(q), nil
 }
 
 // FromObject extracts the index key from a *schedulerobjects.Node.
@@ -157,26 +156,7 @@ func (index *NodeAvailableResourceIndex) FromObject(raw interface{}) (bool, []by
 		return false, nil, errors.Errorf("expected *Node, but got %T", raw)
 	}
 	q := node.AvailableQuantityByPriorityAndResource(index.Priority, index.Resource)
-	return true, encodeQuantity(q), nil
-}
-
-func encodeQuantity(val resource.Quantity) []byte {
-	// We assume that any quantity we want to compare can be represented as an int64.
-	return encodeInt(val.MilliValue())
-}
-
-func encodeInt(val int64) []byte {
-	size := 8
-	buf := make([]byte, size)
-
-	// This bit flips the sign bit on any sized signed twos-complement integer,
-	// which when truncated to a uint of the same size will bias the value such
-	// that the maximum negative int becomes 0, and the maximum positive int
-	// becomes the maximum positive uint.
-	scaled := val ^ int64(-1<<(size*8-1))
-
-	binary.BigEndian.PutUint64(buf, uint64(scaled))
-	return buf
+	return true, schedulerobjects.EncodeQuantity(q), nil
 }
 
 // NodeTypesIterator is an iterator over all nodes of the given nodeTypes
diff --git a/internal/scheduler/pool_assigner.go b/internal/scheduler/pool_assigner.go
index 53067395..4e0faa16 100644
--- a/internal/scheduler/pool_assigner.go
+++ b/internal/scheduler/pool_assigner.go
@@ -98,10 +98,12 @@ func (p *DefaultPoolAssigner) AssignPool(j *jobdb.Job) (string, error) {
 	if !j.Queued() && j.HasRuns() {
 		return p.poolByExecutorId[j.LatestRun().Executor()], nil
 	}
-	// See if we have this set of reqs cached
-	cachedPool, ok := p.poolCache.Get(string(j.JobSchedulingInfo().PodRequirementsHash))
-	if ok {
-		return cachedPool.(string), nil
+
+	// See if we have this set of reqs cached.
+	if schedulingKey, ok := j.JobSchedulingInfo().SchedulingKey(); ok {
+		if cachedPool, ok := p.poolCache.Get(schedulingKey); ok {
+			return cachedPool.(string), nil
+		}
 	}
 
 	req := PodRequirementFromJobSchedulingInfo(j.JobSchedulingInfo())
@@ -122,7 +124,9 @@ func (p *DefaultPoolAssigner) AssignPool(j *jobdb.Job) (string, error) {
 					return "", errors.WithMessagef(err, "error selecting node for job %s", j.Id())
 				}
 				if report.Node != nil {
-					p.poolCache.Add(string(j.JobSchedulingInfo().GetPodRequirementsHash()), pool)
+					if schedulingKey, ok := j.JobSchedulingInfo().SchedulingKey(); ok {
+						p.poolCache.Add(schedulingKey, pool)
+					}
 					return pool, nil
 				}
 			}
diff --git a/internal/scheduler/preempting_queue_scheduler.go b/internal/scheduler/preempting_queue_scheduler.go
index e65299fa..7a6bcf2d 100644
--- a/internal/scheduler/preempting_queue_scheduler.go
+++ b/internal/scheduler/preempting_queue_scheduler.go
@@ -850,4 +850,7 @@ func defaultPostEvictFunc(ctx context.Context, job interfaces.LegacySchedulerJob
 	} else {
 		req.NodeSelector[schedulerconfig.NodeIdLabel] = node.Id
 	}
+
+	// We've changed the scheduling requirements and must clear any cached key.
+	req.ClearCachedSchedulingKey()
 }
diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go
index 4dba4cc9..7003c93e 100644
--- a/internal/scheduler/scheduler.go
+++ b/internal/scheduler/scheduler.go
@@ -13,7 +13,6 @@ import (
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/util/clock"
 
-	"github.com/armadaproject/armada/internal/common/hash"
 	"github.com/armadaproject/armada/internal/common/logging"
 	"github.com/armadaproject/armada/internal/common/stringinterner"
 	"github.com/armadaproject/armada/internal/scheduler/database"
@@ -333,11 +332,8 @@ func (s *Scheduler) createSchedulingInfoWithNodeAntiAffinityForAttemptedRuns(job
 		}
 	}
 	podSchedulingRequirement.Affinity = newAffinity
-	newRequirementsHash, err := hash.CalculatePodRequirementsHash(podSchedulingRequirement)
-	if err != nil {
-		return nil, errors.WithStack(err)
-	}
-	newSchedulingInfo.PodRequirementsHash = newRequirementsHash
+	podSchedulingRequirement.ClearCachedSchedulingKey()
+
 	return newSchedulingInfo, nil
 }
 
diff --git a/internal/scheduler/schedulerobjects/podutils.go b/internal/scheduler/schedulerobjects/podutils.go
index 164579c0..5669b8b3 100644
--- a/internal/scheduler/schedulerobjects/podutils.go
+++ b/internal/scheduler/schedulerobjects/podutils.go
@@ -1,6 +1,21 @@
 package schedulerobjects
 
-import v1 "k8s.io/api/core/v1"
+import (
+	"crypto/sha1"
+	"io"
+	"strconv"
+	"strings"
+
+	"github.com/gogo/protobuf/jsonpb"
+	"golang.org/x/exp/maps"
+	"golang.org/x/exp/slices"
+	v1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
+
+	armadaslices "github.com/armadaproject/armada/internal/common/slices"
+)
+
+const SchedulingKeySize = 20
 
 func (req *PodRequirements) GetAffinityNodeSelector() *v1.NodeSelector {
 	affinity := req.Affinity
@@ -13,3 +28,135 @@ func (req *PodRequirements) GetAffinityNodeSelector() *v1.NodeSelector {
 	}
 	return nodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution
 }
+
+// SchedulingKey returns the canonical hash of the scheduling requirements of a job.
+//
+// The hash is of size SchedulingKeySize and is guaranteed to always be the same for equivalent requirements,
+// unless Affinity is used, in which case they may differ as a result of unordered map keys.
+func (jobSchedulingInfo *JobSchedulingInfo) SchedulingKey() ([SchedulingKeySize]byte, bool) {
+	if jobSchedulingInfo == nil {
+		return [SchedulingKeySize]byte{}, false
+	}
+	for _, objReq := range jobSchedulingInfo.ObjectRequirements {
+		if req := objReq.GetPodRequirements(); req != nil {
+			return req.SchedulingKey(), true
+		}
+	}
+	return [SchedulingKeySize]byte{}, false
+}
+
+// SchedulingKey returns the canonical hash of the scheduling requirements of a pod.
+//
+// The hash is of size SchedulingKeySize and is guaranteed to always be the same for equivalent requirements,
+// unless Affinity is used, in which case they may differ as a result of unordered map keys.
+func (req *PodRequirements) SchedulingKey() [SchedulingKeySize]byte {
+	if req.CachedSchedulingKey == nil {
+		// Cache the key such that the next invocation returns a pre-computed key.
+		schedulingKey := schedulingKeyFromPodRequirements(req)
+		req.CachedSchedulingKey = schedulingKey[:]
+	}
+	return [SchedulingKeySize]byte(req.CachedSchedulingKey)
+}
+
+func schedulingKeyFromPodRequirements(req *PodRequirements) [SchedulingKeySize]byte {
+	// We separate taints/labels by $, labels and values by =, and and groups by &,
+	// since these characters are not allowed in taints and labels; see
+	// https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set
+	// https://man.archlinux.org/man/community/kubectl/kubectl-taint.1.en
+	h := sha1.New()
+
+	nodeSelectorKeys := maps.Keys(req.NodeSelector)
+	slices.Sort(nodeSelectorKeys)
+	for _, key := range nodeSelectorKeys {
+		value := req.NodeSelector[key]
+		_, _ = io.WriteString(h, key)
+		_, _ = io.WriteString(h, "=")
+		_, _ = io.WriteString(h, value)
+		_, _ = io.WriteString(h, "$")
+	}
+	_, _ = io.WriteString(h, "&")
+
+	if req.Affinity != nil {
+		_ = (&jsonpb.Marshaler{EnumsAsInts: true, OrigName: true}).Marshal(h, req.Affinity)
+		_, _ = io.WriteString(h, "&")
+	}
+
+	tolerations := slices.Clone(req.Tolerations)
+	slices.SortFunc(tolerations, lessToleration)
+	for _, toleration := range tolerations {
+		_, _ = io.WriteString(h, toleration.Key)
+		_, _ = io.WriteString(h, "=")
+		_, _ = io.WriteString(h, toleration.Value)
+		_, _ = io.WriteString(h, ":")
+		_, _ = io.WriteString(h, string(toleration.Operator))
+		_, _ = io.WriteString(h, ":")
+		_, _ = io.WriteString(h, string(toleration.Effect))
+		_, _ = io.WriteString(h, "$")
+	}
+	_, _ = io.WriteString(h, "&")
+
+	_, _ = io.WriteString(h, strconv.Itoa(int(req.Priority)))
+	_, _ = io.WriteString(h, "&")
+
+	requestKeys := maps.Keys(req.ResourceRequirements.Requests)
+	requestKeys = armadaslices.Filter(
+		requestKeys,
+		func(key v1.ResourceName) bool {
+			q := req.ResourceRequirements.Requests[key]
+			return q.Cmp(resource.Quantity{}) != 0
+		},
+	)
+	slices.Sort(requestKeys)
+	for _, key := range requestKeys {
+		value := req.ResourceRequirements.Requests[key]
+		_, _ = io.WriteString(h, string(key))
+		_, _ = io.WriteString(h, "=")
+		_, _ = h.Write(EncodeQuantity(value))
+		_, _ = io.WriteString(h, "$")
+	}
+
+	return [SchedulingKeySize]byte(h.Sum(nil))
+}
+
+// ClearCachedSchedulingKey clears any cached scheduling keys.
+// Necessary after changing scheduling requirements to avoid inconsistency.
+func (jobSchedulingInfo *JobSchedulingInfo) ClearCachedSchedulingKey() {
+	if jobSchedulingInfo == nil {
+		return
+	}
+	for _, objReq := range jobSchedulingInfo.ObjectRequirements {
+		if req := objReq.GetPodRequirements(); req != nil {
+			req.ClearCachedSchedulingKey()
+		}
+	}
+}
+
+// ClearCachedSchedulingKey clears any cached scheduling key.
+// Necessary after changing scheduling requirements to avoid inconsistency.
+func (req *PodRequirements) ClearCachedSchedulingKey() {
+	req.CachedSchedulingKey = nil
+}
+
+func lessToleration(a, b v1.Toleration) bool {
+	if cmp := strings.Compare(a.Key, b.Key); cmp == -1 {
+		return true
+	} else if cmp == 1 {
+		return false
+	}
+	if cmp := strings.Compare(a.Value, b.Value); cmp == -1 {
+		return true
+	} else if cmp == 1 {
+		return false
+	}
+	if cmp := strings.Compare(string(a.Operator), string(b.Operator)); cmp == -1 {
+		return true
+	} else if cmp == 1 {
+		return false
+	}
+	if cmp := strings.Compare(string(a.Effect), string(b.Effect)); cmp == -1 {
+		return true
+	} else if cmp == 1 {
+		return false
+	}
+	return true
+}
diff --git a/internal/scheduler/schedulerobjects/quantity.go b/internal/scheduler/schedulerobjects/quantity.go
new file mode 100644
index 00000000..fbbb39e7
--- /dev/null
+++ b/internal/scheduler/schedulerobjects/quantity.go
@@ -0,0 +1,28 @@
+package schedulerobjects
+
+import (
+	"encoding/binary"
+
+	"k8s.io/apimachinery/pkg/api/resource"
+)
+
+// EncodeQuantity returns the canonical byte representation of a Kubernetes resource.Quantity{}
+// used throughout the scheduler.
+func EncodeQuantity(val resource.Quantity) []byte {
+	// We assume that any quantity we want to compare can be represented as an int64.
+	return encodeInt(val.MilliValue())
+}
+
+func encodeInt(val int64) []byte {
+	size := 8
+	buf := make([]byte, size)
+
+	// This bit flips the sign bit on any sized signed twos-complement integer,
+	// which when truncated to a uint of the same size will bias the value such
+	// that the maximum negative int becomes 0, and the maximum positive int
+	// becomes the maximum positive uint.
+	scaled := val ^ int64(-1<<(size*8-1))
+
+	binary.BigEndian.PutUint64(buf, uint64(scaled))
+	return buf
+}
diff --git a/internal/scheduler/schedulerobjects/schedulerobjects.pb.go b/internal/scheduler/schedulerobjects/schedulerobjects.pb.go
index ad3f8d71..50df8d0f 100644
--- a/internal/scheduler/schedulerobjects/schedulerobjects.pb.go
+++ b/internal/scheduler/schedulerobjects/schedulerobjects.pb.go
@@ -592,9 +592,8 @@ type JobSchedulingInfo struct {
 	// In-queue priority. I.e., not the priority class priority.
 	Priority uint32 `protobuf:"varint,8,opt,name=priority,proto3" json:"priority,omitempty"`
 	// Kubernetes objects that make up this job and their respective scheduling requirements.
-	ObjectRequirements  []*ObjectRequirements `protobuf:"bytes,5,rep,name=object_requirements,json=objectRequirements,proto3" json:"objectRequirements,omitempty"`
-	Version             uint32                `protobuf:"varint,9,opt,name=version,proto3" json:"version,omitempty"`
-	PodRequirementsHash []byte                `protobuf:"bytes,10,opt,name=podRequirementsHash,proto3" json:"podRequirementsHash,omitempty"`
+	ObjectRequirements []*ObjectRequirements `protobuf:"bytes,5,rep,name=object_requirements,json=objectRequirements,proto3" json:"objectRequirements,omitempty"`
+	Version            uint32                `protobuf:"varint,9,opt,name=version,proto3" json:"version,omitempty"`
 }
 
 func (m *JobSchedulingInfo) Reset()         { *m = JobSchedulingInfo{} }
@@ -607,16 +606,12 @@ func (m *JobSchedulingInfo) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
 }
 func (m *JobSchedulingInfo) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
-	if deterministic {
-		return xxx_messageInfo_JobSchedulingInfo.Marshal(b, m, deterministic)
-	} else {
-		b = b[:cap(b)]
-		n, err := m.MarshalToSizedBuffer(b)
-		if err != nil {
-			return nil, err
-		}
-		return b[:n], nil
+	b = b[:cap(b)]
+	n, err := m.MarshalToSizedBuffer(b)
+	if err != nil {
+		return nil, err
 	}
+	return b[:n], nil
 }
 func (m *JobSchedulingInfo) XXX_Merge(src proto.Message) {
 	xxx_messageInfo_JobSchedulingInfo.Merge(m, src)
@@ -693,13 +688,6 @@ func (m *JobSchedulingInfo) GetVersion() uint32 {
 	return 0
 }
 
-func (m *JobSchedulingInfo) GetPodRequirementsHash() []byte {
-	if m != nil {
-		return m.PodRequirementsHash
-	}
-	return nil
-}
-
 // Message capturing the scheduling requirements of a particular Kubernetes object.
 type ObjectRequirements struct {
 	// Types that are valid to be assigned to Requirements:
@@ -790,6 +778,10 @@ type PodRequirements struct {
 	PreemptionPolicy string `protobuf:"bytes,5,opt,name=preemptionPolicy,proto3" json:"preemptionPolicy,omitempty"`
 	// Sum of the resource requirements for all containers that make up this pod.
 	ResourceRequirements v1.ResourceRequirements `protobuf:"bytes,6,opt,name=resourceRequirements,proto3" json:"resourceRequirements"`
+	// This key is a hash of the scheduling requirements.
+	// The hash is 20 bytes in size and is guaranteed to be the same for equivalent PodRequirements,
+	// unless Affinity is used, in which case they may differ as a result of unordered map keys.
+	CachedSchedulingKey []byte `protobuf:"bytes,8,opt,name=cachedSchedulingKey,proto3" json:"cachedSchedulingKey,omitempty"`
 }
 
 func (m *PodRequirements) Reset()         { *m = PodRequirements{} }
@@ -874,6 +866,13 @@ func (m *PodRequirements) GetResourceRequirements() v1.ResourceRequirements {
 	return v1.ResourceRequirements{}
 }
 
+func (m *PodRequirements) GetCachedSchedulingKey() []byte {
+	if m != nil {
+		return m.CachedSchedulingKey
+	}
+	return nil
+}
+
 // Used to store details about pulsar scheduler jobs in Redis
 // Can be removed once we deprecate the legacy scheduler
 type PulsarSchedulerJobDetails struct {
@@ -969,133 +968,133 @@ func init() {
 }
 
 var fileDescriptor_97dadc5fbd620721 = []byte{
-	// 2002 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x19, 0xdf, 0x6f, 0x23, 0x47,
-	0x39, 0x9b, 0x9f, 0xf6, 0xe7, 0x5c, 0xe2, 0x4c, 0x92, 0xeb, 0xc6, 0x77, 0x67, 0xa7, 0x69, 0xa9,
-	0x52, 0x7a, 0xb5, 0xd5, 0x14, 0x89, 0xd3, 0xc1, 0x4b, 0x9c, 0x04, 0xce, 0xe1, 0xea, 0xe4, 0xd6,
-	0x09, 0x08, 0x24, 0xba, 0x1a, 0x7b, 0x27, 0xbe, 0x6d, 0xd6, 0x33, 0xee, 0xee, 0x6c, 0xa8, 0xfb,
-	0x0c, 0x48, 0x08, 0xa9, 0xaa, 0x00, 0x21, 0x24, 0x24, 0x10, 0x6f, 0xfc, 0x05, 0xbc, 0xf0, 0x0f,
-	0xf4, 0xb1, 0x8f, 0x3c, 0x19, 0x74, 0xf7, 0x66, 0x89, 0x17, 0xfe, 0x02, 0xb4, 0x33, 0xbb, 0xde,
-	0xf1, 0xae, 0x1d, 0xe7, 0x0a, 0xd7, 0x7b, 0xf2, 0xce, 0xf7, 0x7b, 0xbe, 0x1f, 0xf3, 0x7d, 0x33,
-	0x86, 0x87, 0x36, 0xe5, 0xc4, 0xa5, 0xd8, 0xa9, 0x78, 0xad, 0xa7, 0xc4, 0xf2, 0x1d, 0xe2, 0xc6,
-	0x5f, 0xac, 0xf9, 0x11, 0x69, 0x71, 0x2f, 0x05, 0x28, 0x77, 0x5d, 0xc6, 0x19, 0xca, 0x27, 0xe1,
-	0x85, 0x52, 0x9b, 0xb1, 0xb6, 0x43, 0x2a, 0x02, 0xdf, 0xf4, 0x2f, 0x2a, 0xdc, 0xee, 0x10, 0x8f,
-	0xe3, 0x4e, 0x57, 0xb2, 0x14, 0x76, 0x2e, 0x1f, 0x78, 0x65, 0x9b, 0x55, 0x70, 0xd7, 0xae, 0xb4,
-	0x98, 0x4b, 0x2a, 0x57, 0xef, 0x55, 0xda, 0x84, 0x12, 0x17, 0x73, 0x62, 0x85, 0x34, 0xdf, 0x8a,
-	0x69, 0x3a, 0xb8, 0xf5, 0xd4, 0xa6, 0xc4, 0xed, 0x55, 0xba, 0x97, 0x6d, 0xc1, 0xe4, 0x12, 0x8f,
-	0xf9, 0x6e, 0x8b, 0xa4, 0xb8, 0xde, 0x6d, 0xdb, 0xfc, 0xa9, 0xdf, 0x2c, 0xb7, 0x58, 0xa7, 0xd2,
-	0x66, 0x6d, 0x16, 0xdb, 0x10, 0xac, 0xc4, 0x42, 0x7c, 0x49, 0xf2, 0x9d, 0xbf, 0xce, 0x41, 0xe6,
-	0xe8, 0x13, 0xd2, 0xf2, 0x39, 0x73, 0xd1, 0x36, 0xcc, 0xda, 0x96, 0xae, 0x6d, 0x6b, 0xbb, 0xd9,
-	0x6a, 0x7e, 0xd0, 0x2f, 0x2d, 0xdb, 0xd6, 0x7d, 0xd6, 0xb1, 0x39, 0xe9, 0x74, 0x79, 0xcf, 0x98,
-	0xb5, 0x2d, 0xf4, 0x16, 0xcc, 0x77, 0x19, 0x73, 0xf4, 0x59, 0x41, 0x83, 0x06, 0xfd, 0xd2, 0x4a,
-	0xb0, 0x56, 0xa8, 0x04, 0x1e, 0xed, 0xc3, 0x02, 0x65, 0x16, 0xf1, 0xf4, 0xb9, 0xed, 0xb9, 0xdd,
-	0xdc, 0xde, 0xed, 0x72, 0xca, 0x75, 0x75, 0x66, 0x91, 0xea, 0xfa, 0xa0, 0x5f, 0x5a, 0x15, 0x84,
-	0x8a, 0x04, 0xc9, 0x89, 0x3e, 0x84, 0x95, 0x8e, 0x4d, 0xed, 0x8e, 0xdf, 0x39, 0x66, 0xcd, 0x86,
-	0xfd, 0x29, 0xd1, 0xe7, 0xb7, 0xb5, 0xdd, 0xdc, 0x5e, 0x31, 0x2d, 0xcb, 0x08, 0x9d, 0xf1, 0xd8,
-	0xf6, 0x78, 0xf5, 0xf6, 0x17, 0xfd, 0xd2, 0x4c, 0x60, 0xd8, 0x28, 0xb7, 0x91, 0x58, 0x07, 0xf2,
-	0x1d, 0xec, 0xf1, 0xf3, 0xae, 0x85, 0x39, 0x39, 0xb3, 0x3b, 0x44, 0x5f, 0x10, 0xf2, 0x0b, 0x65,
-	0x19, 0xbc, 0x72, 0xe4, 0xb8, 0xf2, 0x59, 0x14, 0xbc, 0x6a, 0x21, 0x92, 0x3d, 0xca, 0xf9, 0xf9,
-	0x3f, 0x4b, 0x9a, 0x91, 0x80, 0xa1, 0x13, 0x58, 0xf7, 0x29, 0xf6, 0x3c, 0xbb, 0x4d, 0x89, 0x65,
-	0x7e, 0xc4, 0x9a, 0xa6, 0xeb, 0x53, 0x4f, 0xcf, 0x6e, 0xcf, 0xed, 0x66, 0xab, 0xa5, 0x41, 0xbf,
-	0x74, 0x27, 0x46, 0x1f, 0xb3, 0xa6, 0xe1, 0x53, 0xd5, 0x09, 0x6b, 0x29, 0xe4, 0xce, 0x2f, 0x11,
-	0xcc, 0x07, 0x5e, 0xbb, 0x59, 0x98, 0x28, 0xee, 0x10, 0x7d, 0x39, 0x0e, 0x53, 0xb0, 0x56, 0xc3,
-	0x14, 0xac, 0xd1, 0x07, 0x90, 0x0d, 0xac, 0x36, 0x3d, 0x42, 0xa8, 0x88, 0xe9, 0xf5, 0xdb, 0xdf,
-	0x08, 0xb7, 0x9f, 0x09, 0x98, 0x1a, 0x84, 0x50, 0xb1, 0xf1, 0xe1, 0x0a, 0x9d, 0x40, 0x36, 0x88,
-	0x9d, 0xc9, 0x7b, 0x5d, 0xa2, 0xcf, 0x85, 0xe2, 0xc6, 0x46, 0xfe, 0xac, 0xd7, 0x25, 0xd5, 0xdb,
-	0x83, 0x7e, 0x09, 0xd1, 0x70, 0xa5, 0xd8, 0x96, 0x89, 0x60, 0xe8, 0x21, 0x2c, 0x0f, 0x05, 0x9a,
-	0xb6, 0x25, 0x32, 0x20, 0x5b, 0xd5, 0x07, 0xfd, 0xd2, 0x46, 0x44, 0x53, 0x53, 0xf7, 0x0e, 0x31,
-	0x14, 0xed, 0xc3, 0x22, 0xc7, 0x36, 0xe5, 0x9e, 0xbe, 0x20, 0x72, 0x70, 0xab, 0x2c, 0xeb, 0xa9,
-	0x8c, 0xbb, 0x76, 0x39, 0xa8, 0xb9, 0xf2, 0xd5, 0x7b, 0xe5, 0xb3, 0x80, 0xa2, 0xba, 0x12, 0xee,
-	0x2b, 0x64, 0x30, 0xc2, 0x5f, 0x74, 0x0a, 0x8b, 0x0e, 0x6e, 0x12, 0xc7, 0xd3, 0x17, 0x85, 0x88,
-	0x9d, 0xf1, 0x9b, 0x29, 0x3f, 0x16, 0x44, 0x47, 0x94, 0xbb, 0xbd, 0xea, 0xc6, 0xa0, 0x5f, 0xca,
-	0x4b, 0x2e, 0xc5, 0xb0, 0x50, 0x0e, 0x32, 0x61, 0x95, 0x33, 0x8e, 0x1d, 0x33, 0xaa, 0x5f, 0x4f,
-	0x5f, 0x7a, 0xb1, 0xac, 0x16, 0xec, 0x11, 0xca, 0x33, 0x12, 0x6b, 0xf4, 0x37, 0x0d, 0xde, 0xc4,
-	0x8e, 0xc3, 0x5a, 0x98, 0xe3, 0xa6, 0x43, 0xcc, 0x66, 0xcf, 0xec, 0xba, 0x36, 0x73, 0x6d, 0xde,
-	0x33, 0x31, 0xb5, 0x86, 0x7a, 0xf5, 0x8c, 0xd8, 0xd1, 0x77, 0x27, 0xec, 0x68, 0x3f, 0x16, 0x51,
-	0xed, 0x9d, 0x86, 0x02, 0xf6, 0xa9, 0x15, 0x29, 0x92, 0x7b, 0xdd, 0x0d, 0x8d, 0xda, 0xc6, 0x53,
-	0xc8, 0x8d, 0xa9, 0x14, 0xc8, 0x85, 0x75, 0x8f, 0x63, 0x2e, 0x2c, 0x0e, 0x8b, 0x25, 0x88, 0x78,
-	0x56, 0x98, 0xf9, 0xce, 0x04, 0x33, 0x1b, 0x01, 0x47, 0xb5, 0x27, 0x2b, 0xa4, 0x66, 0x49, 0xab,
-	0x5e, 0x0b, 0xad, 0x5a, 0xf5, 0x46, 0xb1, 0x46, 0x12, 0x80, 0x7c, 0x58, 0x0f, 0xed, 0x22, 0x56,
-	0xa4, 0xd7, 0xb6, 0x74, 0x10, 0x3a, 0xef, 0x5f, 0xef, 0x1a, 0x62, 0x09, 0x41, 0x91, 0x52, 0x3d,
-	0x54, 0x9a, 0xc7, 0x09, 0xb4, 0x91, 0x82, 0x20, 0x0e, 0x68, 0x44, 0xed, 0xc7, 0x3e, 0xf1, 0x89,
-	0x9e, 0xbb, 0xa9, 0xd6, 0x27, 0x01, 0xf9, 0x64, 0xad, 0x02, 0x6d, 0xa4, 0x20, 0xc1, 0x66, 0xc9,
-	0x95, 0xdd, 0xe2, 0xf1, 0x61, 0x64, 0xda, 0x96, 0xa7, 0xaf, 0x5c, 0xab, 0xf6, 0x48, 0x72, 0x44,
-	0x1e, 0xf3, 0x12, 0x6a, 0x49, 0x02, 0x6d, 0xa4, 0x20, 0xe8, 0x2f, 0x1a, 0x14, 0x29, 0xa3, 0x26,
-	0x76, 0x3b, 0xd8, 0xc2, 0x66, 0xbc, 0xf1, 0xb8, 0x02, 0x6e, 0x09, 0x13, 0xbe, 0x3d, 0xc1, 0x84,
-	0x3a, 0xa3, 0xfb, 0x82, 0x77, 0xe8, 0x82, 0x61, 0xb6, 0x4b, 0x6b, 0xde, 0x08, 0xad, 0xb9, 0x43,
-	0x27, 0x53, 0x1a, 0xd7, 0x21, 0x0b, 0x18, 0x72, 0x4a, 0x09, 0xa3, 0x37, 0x60, 0xee, 0x92, 0xf4,
-	0xc2, 0x03, 0x76, 0x6d, 0xd0, 0x2f, 0xdd, 0xba, 0x24, 0x3d, 0xa5, 0x98, 0x03, 0x2c, 0x7a, 0x1b,
-	0x16, 0xae, 0xb0, 0xe3, 0x93, 0xb0, 0x15, 0x8a, 0x4e, 0x26, 0x00, 0x6a, 0x27, 0x13, 0x80, 0x87,
-	0xb3, 0x0f, 0xb4, 0xc2, 0x1f, 0x35, 0xf8, 0xc6, 0x8d, 0x8a, 0x4a, 0xd5, 0xbe, 0x30, 0x51, 0x7b,
-	0x4d, 0xd5, 0x3e, 0xfd, 0xf4, 0x98, 0x66, 0xdd, 0xaf, 0x34, 0xd8, 0x18, 0x57, 0x4b, 0x37, 0x73,
-	0xc5, 0x23, 0xd5, 0x98, 0x95, 0xbd, 0x7b, 0x69, 0x63, 0xa4, 0x50, 0xa9, 0x61, 0x9a, 0x2d, 0xbf,
-	0xd6, 0x60, 0x73, 0x6c, 0x8d, 0xdd, 0xcc, 0x98, 0xff, 0xb3, 0x67, 0x12, 0xd6, 0xc4, 0xb5, 0xf7,
-	0x4a, 0xac, 0xb9, 0x84, 0xcd, 0xb1, 0x15, 0xf9, 0x15, 0x52, 0x36, 0x33, 0x55, 0xd9, 0xef, 0x35,
-	0xd8, 0x9e, 0x56, 0x7c, 0xaf, 0x22, 0x5b, 0x77, 0xfe, 0x34, 0x0f, 0x99, 0x68, 0x88, 0x08, 0x86,
-	0xa1, 0xda, 0xc8, 0x30, 0x34, 0x32, 0x10, 0xcc, 0x8e, 0x0c, 0x02, 0xb3, 0x5f, 0x75, 0x10, 0x38,
-	0x1b, 0x0e, 0x02, 0x72, 0x9e, 0x7d, 0x6b, 0xf2, 0x54, 0xf3, 0x02, 0xc3, 0xc0, 0xcf, 0x35, 0x40,
-	0x3e, 0xf5, 0x08, 0xaf, 0x51, 0x8b, 0x7c, 0x42, 0x2c, 0xc9, 0xa9, 0xcf, 0x0b, 0x15, 0x7b, 0xd7,
-	0xa8, 0x38, 0x4f, 0x31, 0x49, 0x75, 0xdb, 0x83, 0x7e, 0xe9, 0x6e, 0x5a, 0xa2, 0xa2, 0x7a, 0x8c,
-	0xbe, 0xaf, 0xe3, 0xf4, 0xeb, 0xc0, 0x6b, 0x13, 0x6c, 0x7e, 0x19, 0xea, 0x76, 0xfe, 0x33, 0x07,
-	0x5b, 0xa2, 0x52, 0x0f, 0x1c, 0xdf, 0xe3, 0xc4, 0x8d, 0xb2, 0xeb, 0xdc, 0xc3, 0x6d, 0x82, 0x6a,
-	0xb0, 0xd4, 0x72, 0x49, 0x90, 0xcb, 0x42, 0xeb, 0xf5, 0x23, 0xef, 0x7a, 0x98, 0x11, 0x11, 0x8b,
-	0x98, 0x78, 0xa3, 0x45, 0x60, 0x97, 0x6c, 0xde, 0x8a, 0x5d, 0x02, 0xa0, 0xda, 0x25, 0x00, 0xe8,
-	0x01, 0x00, 0x09, 0xef, 0x59, 0x35, 0x4b, 0x0c, 0xc7, 0xe1, 0x20, 0x1b, 0x43, 0xd5, 0x41, 0x36,
-	0x86, 0xa2, 0xdf, 0x6a, 0xb0, 0x3e, 0x6c, 0x96, 0x71, 0xe3, 0x08, 0xf3, 0xe4, 0x30, 0x9d, 0x27,
-	0x13, 0xb7, 0x3e, 0x2c, 0x33, 0x45, 0x8c, 0xcc, 0x9c, 0x3b, 0xe1, 0x36, 0xc7, 0x29, 0x32, 0xc6,
-	0x01, 0x0b, 0x9f, 0x69, 0xa0, 0x4f, 0x12, 0xf7, 0x4a, 0x4e, 0x85, 0xbf, 0xcf, 0x41, 0x61, 0xdc,
-	0xa6, 0x0d, 0xd2, 0x65, 0x2e, 0x1f, 0xde, 0x5c, 0xb5, 0x29, 0x37, 0x57, 0x25, 0x3b, 0x66, 0xff,
-	0xc7, 0xec, 0xf8, 0x4c, 0x83, 0xbc, 0xe2, 0x3a, 0x11, 0x96, 0xf0, 0x00, 0xa9, 0xa6, 0x37, 0x3b,
-	0xd9, 0x76, 0x35, 0x6c, 0xca, 0xf0, 0x57, 0x1c, 0xf4, 0x4b, 0x85, 0xa4, 0x7c, 0x65, 0x3f, 0x29,
-	0xdd, 0x85, 0x3f, 0x68, 0xb0, 0x39, 0x56, 0xd6, 0xcd, 0xaa, 0xf0, 0x87, 0xa3, 0x01, 0x7b, 0xe7,
-	0x05, 0x32, 0x6f, 0x6a, 0xf4, 0x7e, 0x31, 0x0b, 0xcb, 0x6a, 0xb8, 0xd1, 0x87, 0x90, 0x8d, 0x27,
-	0x44, 0x4d, 0x38, 0xed, 0xdd, 0xeb, 0x33, 0xa4, 0x9c, 0x98, 0x0b, 0xd7, 0xc2, 0xe0, 0xc4, 0x72,
-	0x8c, 0xf8, 0xb3, 0xf0, 0x3b, 0x0d, 0x56, 0x26, 0xf7, 0xb2, 0xc9, 0x4e, 0xf8, 0xf1, 0xa8, 0x13,
-	0xca, 0x4a, 0x33, 0x19, 0xbe, 0xd2, 0x94, 0xbb, 0x97, 0x6d, 0xd1, 0x5d, 0x22, 0x75, 0xe5, 0x27,
-	0x3e, 0xa6, 0xdc, 0xe6, 0xbd, 0xa9, 0x7e, 0x78, 0xbe, 0x00, 0x6b, 0xc7, 0xac, 0xd9, 0x90, 0x1b,
-	0xb5, 0x69, 0xbb, 0x46, 0x2f, 0x18, 0xda, 0x83, 0x8c, 0x63, 0x5f, 0x10, 0x6e, 0x77, 0x88, 0x30,
-	0xef, 0x96, 0xbc, 0x3b, 0x47, 0x30, 0xf5, 0xee, 0x1c, 0xc1, 0x82, 0xbb, 0x33, 0xe6, 0x66, 0x87,
-	0x79, 0xdc, 0x64, 0xb4, 0x15, 0x35, 0x7d, 0x71, 0xe4, 0x60, 0xfe, 0x01, 0xf3, 0xf8, 0x09, 0x6d,
-	0xa9, 0x9c, 0x10, 0x43, 0xd1, 0x77, 0x20, 0xd7, 0x75, 0x49, 0x00, 0xb7, 0x9b, 0x8e, 0xbc, 0xca,
-	0x67, 0xaa, 0x5b, 0x83, 0x7e, 0x69, 0x53, 0x01, 0x2b, 0xbc, 0x2a, 0x35, 0x7a, 0x04, 0xf9, 0x16,
-	0xa3, 0x2d, 0xdf, 0x75, 0x09, 0x6d, 0xf5, 0x4c, 0x0f, 0x5f, 0xc8, 0xa7, 0x9b, 0x4c, 0xf5, 0xde,
-	0xa0, 0x5f, 0xda, 0x52, 0x70, 0x0d, 0x7c, 0xa1, 0x4a, 0x59, 0x4d, 0xa0, 0xd0, 0x09, 0xac, 0x0f,
-	0x2f, 0xaf, 0x2d, 0x07, 0x7b, 0x9e, 0x29, 0x5e, 0x35, 0x16, 0x45, 0x80, 0xc4, 0x13, 0x4a, 0x84,
-	0x3e, 0x08, 0xb0, 0xf5, 0xd1, 0x27, 0x8e, 0xb5, 0x14, 0x12, 0x35, 0x20, 0xe7, 0xf9, 0xcd, 0x8e,
-	0xcd, 0x4d, 0xe1, 0xca, 0xa5, 0xa9, 0x05, 0x1e, 0x5d, 0xbb, 0x41, 0xb2, 0x0d, 0x1f, 0x7b, 0x94,
-	0x75, 0x10, 0x9c, 0x48, 0x93, 0x9e, 0x89, 0x83, 0x13, 0xc1, 0xd4, 0xe0, 0x44, 0x30, 0xf4, 0x33,
-	0x58, 0x97, 0x29, 0x6c, 0xba, 0xe4, 0x63, 0xdf, 0x76, 0x49, 0x87, 0xc4, 0x2f, 0x15, 0x6f, 0xa6,
-	0xf3, 0xfc, 0x44, 0xfc, 0x1a, 0x0a, 0xad, 0x6c, 0xf6, 0x2c, 0x05, 0x57, 0x9b, 0x7d, 0x1a, 0x8b,
-	0x2a, 0xb0, 0x74, 0x45, 0x5c, 0xcf, 0x66, 0x54, 0xcf, 0x0a, 0x5b, 0x37, 0x07, 0xfd, 0xd2, 0x5a,
-	0x08, 0x52, 0x78, 0x23, 0x2a, 0xd4, 0x80, 0xf5, 0x2e, 0xb3, 0x54, 0x19, 0x8f, 0xb0, 0xf7, 0x54,
-	0x87, 0x6d, 0x6d, 0x77, 0xb9, 0xfa, 0xfa, 0xa0, 0x5f, 0xba, 0x37, 0x06, 0xad, 0x08, 0x1a, 0xc7,
-	0xbd, 0xf3, 0x1b, 0x0d, 0x50, 0x7a, 0x4b, 0xc8, 0x81, 0xd5, 0x04, 0x75, 0xd8, 0xa1, 0x5f, 0x4f,
-	0x7b, 0xe4, 0x74, 0x94, 0x50, 0xe6, 0x56, 0x82, 0x3b, 0x36, 0xe3, 0xd1, 0x8c, 0x91, 0x14, 0x5d,
-	0x5d, 0x81, 0x65, 0xd5, 0xf9, 0x3b, 0xff, 0x5e, 0x84, 0xd5, 0x84, 0x54, 0xe4, 0xc9, 0x07, 0xa8,
-	0x06, 0x71, 0x48, 0x8b, 0x33, 0x37, 0x3c, 0x88, 0xde, 0x9f, 0x6a, 0x8e, 0x98, 0xd5, 0x22, 0x2e,
-	0x79, 0x1c, 0x15, 0x06, 0xfd, 0xd2, 0x6d, 0x55, 0x98, 0xe2, 0xa4, 0x11, 0x25, 0xe8, 0x14, 0x32,
-	0xf8, 0xe2, 0xc2, 0xa6, 0x41, 0x42, 0xc9, 0x53, 0xe6, 0xee, 0xb8, 0x91, 0x75, 0x3f, 0xa4, 0x91,
-	0xe9, 0x16, 0x71, 0xa8, 0xe9, 0x16, 0xc1, 0xd0, 0x39, 0xe4, 0x38, 0x73, 0x88, 0x8b, 0xb9, 0xcd,
-	0x68, 0x34, 0xc4, 0x16, 0xc7, 0xce, 0xc1, 0x43, 0xb2, 0x61, 0x73, 0x53, 0x59, 0x0d, 0x75, 0x81,
-	0x18, 0xe4, 0x30, 0xa5, 0x8c, 0x87, 0x62, 0x97, 0x26, 0x0d, 0xae, 0x49, 0xe7, 0xec, 0xc7, 0x4c,
-	0xd2, 0x37, 0xe2, 0x68, 0x51, 0x44, 0xa9, 0x47, 0x8b, 0x02, 0x1e, 0x29, 0xb5, 0x79, 0x31, 0x5c,
-	0x4c, 0x2f, 0xb5, 0x63, 0xc8, 0x47, 0xa7, 0x13, 0xa3, 0xa7, 0xcc, 0xb1, 0x5b, 0x3d, 0xf1, 0xd2,
-	0x9b, 0x95, 0x0d, 0x34, 0x89, 0x53, 0x1b, 0x68, 0x12, 0x87, 0x3e, 0x85, 0x8d, 0xe8, 0x48, 0x1f,
-	0xc9, 0xd2, 0x45, 0x11, 0xa5, 0xdd, 0x71, 0x0e, 0x35, 0xc6, 0xd0, 0x57, 0xef, 0x86, 0xae, 0x1d,
-	0x2b, 0xcd, 0x18, 0x0b, 0x2d, 0xb4, 0x61, 0x2d, 0x95, 0x54, 0x2f, 0x65, 0x58, 0xbf, 0x80, 0x7c,
-	0x32, 0x40, 0x2f, 0x65, 0x4a, 0xff, 0xb3, 0x06, 0x5b, 0xa7, 0xbe, 0xe3, 0x61, 0xb7, 0x11, 0x25,
-	0xcc, 0x31, 0x6b, 0x1e, 0x12, 0x8e, 0x6d, 0xc7, 0x0b, 0x84, 0x89, 0xab, 0x7f, 0xa8, 0x53, 0x08,
-	0x13, 0x00, 0x55, 0x98, 0x7c, 0x4f, 0x7b, 0x1b, 0x16, 0x9e, 0x24, 0xa7, 0xf0, 0xe4, 0x30, 0x24,
-	0x29, 0xd0, 0x7d, 0x58, 0x0c, 0xba, 0x2b, 0xe1, 0xe1, 0x04, 0x2e, 0x2e, 0x68, 0x12, 0xa2, 0x5e,
-	0xd0, 0x24, 0xe4, 0x9b, 0x27, 0x90, 0x53, 0x5e, 0x2e, 0x50, 0x0e, 0x96, 0xce, 0xeb, 0x3f, 0xa8,
-	0x9f, 0xfc, 0xa8, 0x9e, 0x9f, 0x09, 0x16, 0xa7, 0x47, 0xf5, 0xc3, 0x5a, 0xfd, 0xfb, 0x79, 0x2d,
-	0x58, 0x18, 0xe7, 0xf5, 0x7a, 0xb0, 0x98, 0x45, 0xb7, 0x20, 0xdb, 0x38, 0x3f, 0x38, 0x38, 0x3a,
-	0x3a, 0x3c, 0x3a, 0xcc, 0xcf, 0x21, 0x80, 0xc5, 0xef, 0xed, 0xd7, 0x1e, 0x1f, 0x1d, 0xe6, 0xe7,
-	0xab, 0x3f, 0xfd, 0xe2, 0x59, 0x51, 0xfb, 0xf2, 0x59, 0x51, 0xfb, 0xd7, 0xb3, 0xa2, 0xf6, 0xf9,
-	0xf3, 0xe2, 0xcc, 0x97, 0xcf, 0x8b, 0x33, 0xff, 0x78, 0x5e, 0x9c, 0xf9, 0xc9, 0x81, 0xf2, 0xb7,
-	0x8d, 0x7c, 0x2a, 0xeb, 0xba, 0x2c, 0xa8, 0x9e, 0x70, 0x55, 0xb9, 0xc1, 0xff, 0x53, 0xcd, 0x45,
-	0xd1, 0xc1, 0xde, 0xff, 0x6f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x35, 0xc0, 0xd1, 0x85, 0xcd, 0x1a,
-	0x00, 0x00,
+	// 2007 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x19, 0x4d, 0x6f, 0xe3, 0xc6,
+	0xd5, 0xf4, 0xa7, 0xf4, 0xe4, 0xb5, 0xe5, 0xf1, 0x7a, 0x43, 0x6b, 0x77, 0x45, 0xc7, 0x49, 0x03,
+	0xa7, 0xd9, 0x48, 0x88, 0x53, 0xa0, 0x8b, 0x6d, 0x2f, 0x96, 0xed, 0x76, 0xe5, 0x6c, 0x64, 0x2f,
+	0x65, 0xb7, 0x68, 0x81, 0x86, 0x18, 0x91, 0x63, 0x2d, 0x63, 0x8a, 0xa3, 0x90, 0x43, 0x37, 0xca,
+	0xb9, 0x2d, 0x50, 0x14, 0x08, 0x82, 0x36, 0x28, 0x02, 0x14, 0x68, 0x91, 0x5b, 0x7f, 0x41, 0x2f,
+	0xbd, 0xf5, 0x94, 0x63, 0x8e, 0x3d, 0xa9, 0xc5, 0xee, 0x4d, 0xc7, 0xfe, 0x82, 0x82, 0x33, 0xa4,
+	0x38, 0x22, 0x25, 0xcb, 0x9b, 0x76, 0xb3, 0x27, 0x6b, 0xde, 0xf7, 0xe7, 0xbc, 0xc7, 0x31, 0x3c,
+	0xb0, 0x5d, 0x46, 0x3c, 0x17, 0x3b, 0x55, 0xdf, 0x7c, 0x42, 0xac, 0xc0, 0x21, 0x5e, 0xf2, 0x8b,
+	0xb6, 0x3e, 0x24, 0x26, 0xf3, 0x33, 0x80, 0x4a, 0xd7, 0xa3, 0x8c, 0xa2, 0x62, 0x1a, 0x5e, 0xd2,
+	0xda, 0x94, 0xb6, 0x1d, 0x52, 0xe5, 0xf8, 0x56, 0x70, 0x5e, 0x65, 0x76, 0x87, 0xf8, 0x0c, 0x77,
+	0xba, 0x82, 0xa5, 0xb4, 0x7d, 0x71, 0xdf, 0xaf, 0xd8, 0xb4, 0x8a, 0xbb, 0x76, 0xd5, 0xa4, 0x1e,
+	0xa9, 0x5e, 0xbe, 0x53, 0x6d, 0x13, 0x97, 0x78, 0x98, 0x11, 0x2b, 0xa2, 0xf9, 0x5e, 0x42, 0xd3,
+	0xc1, 0xe6, 0x13, 0xdb, 0x25, 0x5e, 0xaf, 0xda, 0xbd, 0x68, 0x73, 0x26, 0x8f, 0xf8, 0x34, 0xf0,
+	0x4c, 0x92, 0xe1, 0x7a, 0xbb, 0x6d, 0xb3, 0x27, 0x41, 0xab, 0x62, 0xd2, 0x4e, 0xb5, 0x4d, 0xdb,
+	0x34, 0xb1, 0x21, 0x3c, 0xf1, 0x03, 0xff, 0x25, 0xc8, 0xb7, 0xff, 0x3a, 0x07, 0xb9, 0xc3, 0x8f,
+	0x89, 0x19, 0x30, 0xea, 0xa1, 0x2d, 0x98, 0xb5, 0x2d, 0x55, 0xd9, 0x52, 0x76, 0xf2, 0xb5, 0xe2,
+	0xa0, 0xaf, 0x2d, 0xdb, 0xd6, 0x3d, 0xda, 0xb1, 0x19, 0xe9, 0x74, 0x59, 0x4f, 0x9f, 0xb5, 0x2d,
+	0xf4, 0x06, 0xcc, 0x77, 0x29, 0x75, 0xd4, 0x59, 0x4e, 0x83, 0x06, 0x7d, 0x6d, 0x25, 0x3c, 0x4b,
+	0x54, 0x1c, 0x8f, 0xf6, 0x60, 0xc1, 0xa5, 0x16, 0xf1, 0xd5, 0xb9, 0xad, 0xb9, 0x9d, 0xc2, 0xee,
+	0xad, 0x4a, 0x26, 0x74, 0x0d, 0x6a, 0x91, 0xda, 0xfa, 0xa0, 0xaf, 0xad, 0x72, 0x42, 0x49, 0x82,
+	0xe0, 0x44, 0x1f, 0xc0, 0x4a, 0xc7, 0x76, 0xed, 0x4e, 0xd0, 0x39, 0xa2, 0xad, 0xa6, 0xfd, 0x09,
+	0x51, 0xe7, 0xb7, 0x94, 0x9d, 0xc2, 0x6e, 0x39, 0x2b, 0x4b, 0x8f, 0x82, 0xf1, 0xc8, 0xf6, 0x59,
+	0xed, 0xd6, 0x57, 0x7d, 0x6d, 0x26, 0x34, 0x6c, 0x94, 0x5b, 0x4f, 0x9d, 0x43, 0xf9, 0x0e, 0xf6,
+	0xd9, 0x59, 0xd7, 0xc2, 0x8c, 0x9c, 0xda, 0x1d, 0xa2, 0x2e, 0x70, 0xf9, 0xa5, 0x8a, 0x48, 0x5e,
+	0x25, 0x0e, 0x5c, 0xe5, 0x34, 0x4e, 0x5e, 0xad, 0x14, 0xcb, 0x1e, 0xe5, 0xfc, 0xec, 0x5f, 0x9a,
+	0xa2, 0xa7, 0x60, 0xe8, 0x18, 0xd6, 0x03, 0x17, 0xfb, 0xbe, 0xdd, 0x76, 0x89, 0x65, 0x7c, 0x48,
+	0x5b, 0x86, 0x17, 0xb8, 0xbe, 0x9a, 0xdf, 0x9a, 0xdb, 0xc9, 0xd7, 0xb4, 0x41, 0x5f, 0xbb, 0x9d,
+	0xa0, 0x8f, 0x68, 0x4b, 0x0f, 0x5c, 0x39, 0x08, 0x6b, 0x19, 0xe4, 0xf6, 0x6f, 0x10, 0xcc, 0x87,
+	0x51, 0xbb, 0x5e, 0x9a, 0x5c, 0xdc, 0x21, 0xea, 0x72, 0x92, 0xa6, 0xf0, 0x2c, 0xa7, 0x29, 0x3c,
+	0xa3, 0xf7, 0x21, 0x1f, 0x5a, 0x6d, 0xf8, 0x84, 0xb8, 0x3c, 0xa7, 0x57, 0xbb, 0x7f, 0x33, 0x72,
+	0x3f, 0x17, 0x32, 0x35, 0x09, 0x71, 0xb9, 0xe3, 0xc3, 0x13, 0x3a, 0x86, 0x7c, 0x98, 0x3b, 0x83,
+	0xf5, 0xba, 0x44, 0x9d, 0x8b, 0xc4, 0x8d, 0xcd, 0xfc, 0x69, 0xaf, 0x4b, 0x6a, 0xb7, 0x06, 0x7d,
+	0x0d, 0xb9, 0xd1, 0x49, 0xb2, 0x2d, 0x17, 0xc3, 0xd0, 0x03, 0x58, 0x1e, 0x0a, 0x34, 0x6c, 0x8b,
+	0x57, 0x40, 0xbe, 0xa6, 0x0e, 0xfa, 0xda, 0xcd, 0x98, 0xa6, 0x2e, 0xfb, 0x0e, 0x09, 0x14, 0xed,
+	0xc1, 0x22, 0xc3, 0xb6, 0xcb, 0x7c, 0x75, 0x81, 0xd7, 0xe0, 0x66, 0x45, 0xf4, 0x53, 0x05, 0x77,
+	0xed, 0x4a, 0xd8, 0x73, 0x95, 0xcb, 0x77, 0x2a, 0xa7, 0x21, 0x45, 0x6d, 0x25, 0xf2, 0x2b, 0x62,
+	0xd0, 0xa3, 0xbf, 0xe8, 0x04, 0x16, 0x1d, 0xdc, 0x22, 0x8e, 0xaf, 0x2e, 0x72, 0x11, 0xdb, 0xe3,
+	0x9d, 0xa9, 0x3c, 0xe2, 0x44, 0x87, 0x2e, 0xf3, 0x7a, 0xb5, 0x9b, 0x83, 0xbe, 0x56, 0x14, 0x5c,
+	0x92, 0x61, 0x91, 0x1c, 0x64, 0xc0, 0x2a, 0xa3, 0x0c, 0x3b, 0x46, 0xdc, 0xbf, 0xbe, 0xba, 0xf4,
+	0x7c, 0x55, 0xcd, 0xd9, 0x63, 0x94, 0xaf, 0xa7, 0xce, 0xe8, 0x6f, 0x0a, 0xbc, 0x8e, 0x1d, 0x87,
+	0x9a, 0x98, 0xe1, 0x96, 0x43, 0x8c, 0x56, 0xcf, 0xe8, 0x7a, 0x36, 0xf5, 0x6c, 0xd6, 0x33, 0xb0,
+	0x6b, 0x0d, 0xf5, 0xaa, 0x39, 0xee, 0xd1, 0x0f, 0x27, 0x78, 0xb4, 0x97, 0x88, 0xa8, 0xf5, 0x4e,
+	0x22, 0x01, 0x7b, 0xae, 0x15, 0x2b, 0x12, 0xbe, 0xee, 0x44, 0x46, 0x6d, 0xe1, 0x29, 0xe4, 0xfa,
+	0x54, 0x0a, 0xe4, 0xc1, 0xba, 0xcf, 0x30, 0xe3, 0x16, 0x47, 0xcd, 0x12, 0x66, 0x3c, 0xcf, 0xcd,
+	0x7c, 0x6b, 0x82, 0x99, 0xcd, 0x90, 0xa3, 0xd6, 0x13, 0x1d, 0x52, 0xb7, 0x84, 0x55, 0xaf, 0x44,
+	0x56, 0xad, 0xfa, 0xa3, 0x58, 0x3d, 0x0d, 0x40, 0x01, 0xac, 0x47, 0x76, 0x11, 0x2b, 0xd6, 0x6b,
+	0x5b, 0x2a, 0x70, 0x9d, 0xf7, 0xae, 0x0e, 0x0d, 0xb1, 0xb8, 0xa0, 0x58, 0xa9, 0x1a, 0x29, 0x2d,
+	0xe2, 0x14, 0x5a, 0xcf, 0x40, 0x10, 0x03, 0x34, 0xa2, 0xf6, 0xa3, 0x80, 0x04, 0x44, 0x2d, 0x5c,
+	0x57, 0xeb, 0xe3, 0x90, 0x7c, 0xb2, 0x56, 0x8e, 0xd6, 0x33, 0x90, 0xd0, 0x59, 0x72, 0x69, 0x9b,
+	0x2c, 0xb9, 0x8c, 0x0c, 0xdb, 0xf2, 0xd5, 0x95, 0x2b, 0xd5, 0x1e, 0x0a, 0x8e, 0x38, 0x62, 0x7e,
+	0x4a, 0x2d, 0x49, 0xa1, 0xf5, 0x0c, 0x04, 0x7d, 0xa9, 0x40, 0xd9, 0xa5, 0xae, 0x81, 0xbd, 0x0e,
+	0xb6, 0xb0, 0x91, 0x38, 0x9e, 0x74, 0xc0, 0x0d, 0x6e, 0xc2, 0xf7, 0x27, 0x98, 0xd0, 0xa0, 0xee,
+	0x1e, 0xe7, 0x1d, 0x86, 0x60, 0x58, 0xed, 0xc2, 0x9a, 0xd7, 0x22, 0x6b, 0x6e, 0xbb, 0x93, 0x29,
+	0xf5, 0xab, 0x90, 0x25, 0x0c, 0x05, 0xa9, 0x85, 0xd1, 0x6b, 0x30, 0x77, 0x41, 0x7a, 0xd1, 0x05,
+	0xbb, 0x36, 0xe8, 0x6b, 0x37, 0x2e, 0x48, 0x4f, 0x6a, 0xe6, 0x10, 0x8b, 0xde, 0x84, 0x85, 0x4b,
+	0xec, 0x04, 0x24, 0x1a, 0x85, 0x7c, 0x92, 0x71, 0x80, 0x3c, 0xc9, 0x38, 0xe0, 0xc1, 0xec, 0x7d,
+	0xa5, 0xf4, 0x27, 0x05, 0xbe, 0x73, 0xad, 0xa6, 0x92, 0xb5, 0x2f, 0x4c, 0xd4, 0x5e, 0x97, 0xb5,
+	0x4f, 0xbf, 0x3d, 0xa6, 0x59, 0xf7, 0x5b, 0x05, 0x6e, 0x8e, 0xeb, 0xa5, 0xeb, 0x85, 0xe2, 0xa1,
+	0x6c, 0xcc, 0xca, 0xee, 0xdd, 0xac, 0x31, 0x42, 0xa8, 0xd0, 0x30, 0xcd, 0x96, 0xdf, 0x29, 0xb0,
+	0x31, 0xb6, 0xc7, 0xae, 0x67, 0xcc, 0xff, 0x39, 0x32, 0x29, 0x6b, 0x92, 0xde, 0x7b, 0x29, 0xd6,
+	0x5c, 0xc0, 0xc6, 0xd8, 0x8e, 0xfc, 0x06, 0x25, 0x9b, 0x9b, 0xaa, 0xec, 0x8f, 0x0a, 0x6c, 0x4d,
+	0x6b, 0xbe, 0x97, 0x51, 0xad, 0xdb, 0x7f, 0x9e, 0x87, 0x5c, 0xbc, 0x44, 0x84, 0xcb, 0x50, 0x7d,
+	0x64, 0x19, 0x1a, 0x59, 0x08, 0x66, 0x47, 0x16, 0x81, 0xd9, 0x6f, 0xba, 0x08, 0x9c, 0x0e, 0x17,
+	0x01, 0xb1, 0xcf, 0xbe, 0x31, 0x79, 0xab, 0x79, 0x8e, 0x65, 0xe0, 0x57, 0x0a, 0xa0, 0xc0, 0xf5,
+	0x09, 0xab, 0xbb, 0x16, 0xf9, 0x98, 0x58, 0x82, 0x53, 0x9d, 0xe7, 0x2a, 0x76, 0xaf, 0x50, 0x71,
+	0x96, 0x61, 0x12, 0xea, 0xb6, 0x06, 0x7d, 0xed, 0x4e, 0x56, 0xa2, 0xa4, 0x7a, 0x8c, 0xbe, 0x6f,
+	0xe3, 0xf6, 0xeb, 0xc0, 0x2b, 0x13, 0x6c, 0x7e, 0x11, 0xea, 0xb6, 0xff, 0x33, 0x07, 0x9b, 0xbc,
+	0x53, 0xf7, 0x9d, 0xc0, 0x67, 0xc4, 0x8b, 0xab, 0xeb, 0xcc, 0xc7, 0x6d, 0x82, 0xea, 0xb0, 0x64,
+	0x7a, 0x24, 0xac, 0x65, 0xae, 0xf5, 0xea, 0x95, 0x77, 0x3d, 0xaa, 0x88, 0x98, 0x85, 0x6f, 0xbc,
+	0xf1, 0x21, 0xb4, 0x4b, 0x0c, 0x6f, 0xc9, 0x2e, 0x0e, 0x90, 0xed, 0xe2, 0x00, 0x74, 0x1f, 0x80,
+	0x44, 0xdf, 0x59, 0x75, 0x8b, 0x2f, 0xc7, 0xd1, 0x22, 0x9b, 0x40, 0xe5, 0x45, 0x36, 0x81, 0xa2,
+	0x3f, 0x28, 0xb0, 0x3e, 0x1c, 0x96, 0xc9, 0xe0, 0x88, 0xea, 0xe4, 0x20, 0x5b, 0x27, 0x13, 0x5d,
+	0x1f, 0xb6, 0x99, 0x24, 0x46, 0x54, 0xce, 0xed, 0xc8, 0xcd, 0x71, 0x8a, 0xf4, 0x71, 0xc0, 0xd2,
+	0xa7, 0x0a, 0xa8, 0x93, 0xc4, 0xbd, 0x94, 0x5b, 0xe1, 0xef, 0x73, 0x50, 0x1a, 0xe7, 0xb4, 0x4e,
+	0xba, 0xd4, 0x63, 0xc3, 0x2f, 0x57, 0x65, 0xca, 0x97, 0xab, 0x54, 0x1d, 0xb3, 0xff, 0x63, 0x75,
+	0x7c, 0xaa, 0x40, 0x51, 0x0a, 0x1d, 0x4f, 0x4b, 0x74, 0x81, 0xd4, 0xb2, 0xce, 0x4e, 0xb6, 0x5d,
+	0x4e, 0x9b, 0xb4, 0xfc, 0x95, 0x07, 0x7d, 0xad, 0x94, 0x96, 0x2f, 0xf9, 0x93, 0xd1, 0x5d, 0xfa,
+	0x42, 0x81, 0x8d, 0xb1, 0xb2, 0xae, 0xd7, 0x85, 0x3f, 0x19, 0x4d, 0xd8, 0x5b, 0xcf, 0x51, 0x79,
+	0x53, 0xb3, 0xf7, 0xeb, 0x59, 0x58, 0x96, 0xd3, 0x8d, 0x3e, 0x80, 0x7c, 0xb2, 0x21, 0x2a, 0x3c,
+	0x68, 0x6f, 0x5f, 0x5d, 0x21, 0x95, 0xd4, 0x5e, 0xb8, 0x16, 0x25, 0x27, 0x91, 0xa3, 0x27, 0x3f,
+	0x4b, 0x9f, 0x2b, 0xb0, 0x32, 0x79, 0x96, 0x4d, 0x0e, 0xc2, 0xcf, 0x46, 0x83, 0x50, 0x91, 0x86,
+	0xc9, 0xf0, 0x95, 0xa6, 0xd2, 0xbd, 0x68, 0xf3, 0xe9, 0x12, 0xab, 0xab, 0x3c, 0x0e, 0xb0, 0xcb,
+	0x6c, 0xd6, 0x9b, 0x1a, 0x87, 0xcf, 0x17, 0x60, 0xed, 0x88, 0xb6, 0x9a, 0xc2, 0x51, 0xdb, 0x6d,
+	0xd7, 0xdd, 0x73, 0x8a, 0x76, 0x21, 0xe7, 0xd8, 0xe7, 0x84, 0xd9, 0x1d, 0xc2, 0xcd, 0xbb, 0x21,
+	0xbe, 0x9d, 0x63, 0x98, 0xfc, 0xed, 0x1c, 0xc3, 0xc2, 0x6f, 0x67, 0xcc, 0x8c, 0x0e, 0xf5, 0x99,
+	0x41, 0x5d, 0x33, 0x1e, 0xfa, 0xfc, 0xca, 0xc1, 0xec, 0x7d, 0xea, 0xb3, 0x63, 0xd7, 0x94, 0x39,
+	0x21, 0x81, 0xa2, 0x1f, 0x40, 0xa1, 0xeb, 0x91, 0x10, 0x6e, 0xb7, 0x1c, 0xf1, 0x29, 0x9f, 0xab,
+	0x6d, 0x0e, 0xfa, 0xda, 0x86, 0x04, 0x96, 0x78, 0x65, 0x6a, 0xf4, 0x10, 0x8a, 0x26, 0x75, 0xcd,
+	0xc0, 0xf3, 0x88, 0x6b, 0xf6, 0x0c, 0x1f, 0x9f, 0x8b, 0xa7, 0x9b, 0x5c, 0xed, 0xee, 0xa0, 0xaf,
+	0x6d, 0x4a, 0xb8, 0x26, 0x3e, 0x97, 0xa5, 0xac, 0xa6, 0x50, 0xe8, 0x18, 0xd6, 0x87, 0x1f, 0xaf,
+	0xa6, 0x83, 0x7d, 0xdf, 0xe0, 0xaf, 0x1a, 0x8b, 0x3c, 0x41, 0xfc, 0x09, 0x25, 0x46, 0xef, 0x87,
+	0xd8, 0xc6, 0xe8, 0x13, 0xc7, 0x5a, 0x06, 0x89, 0x9a, 0x50, 0xf0, 0x83, 0x56, 0xc7, 0x66, 0x06,
+	0x0f, 0xe5, 0xd2, 0xd4, 0x06, 0x8f, 0x3f, 0xbb, 0x41, 0xb0, 0x0d, 0x1f, 0x7b, 0xa4, 0x73, 0x98,
+	0x9c, 0x58, 0x93, 0x9a, 0x4b, 0x92, 0x13, 0xc3, 0xe4, 0xe4, 0xc4, 0x30, 0xf4, 0x4b, 0x58, 0x17,
+	0x25, 0x6c, 0x78, 0xe4, 0xa3, 0xc0, 0xf6, 0x48, 0x87, 0x24, 0x2f, 0x15, 0xaf, 0x67, 0xeb, 0xfc,
+	0x98, 0xff, 0xd5, 0x25, 0x5a, 0x31, 0xec, 0x69, 0x06, 0x2e, 0x0f, 0xfb, 0x2c, 0x16, 0x55, 0x61,
+	0xe9, 0x92, 0x78, 0xbe, 0x4d, 0x5d, 0x35, 0xcf, 0x6d, 0xdd, 0x18, 0xf4, 0xb5, 0xb5, 0x08, 0x24,
+	0xf1, 0xc6, 0x54, 0x0f, 0xe6, 0xbf, 0xf8, 0x52, 0x53, 0xb6, 0x7f, 0xaf, 0x00, 0xca, 0xda, 0x80,
+	0x1c, 0x58, 0xed, 0x52, 0x4b, 0x06, 0x45, 0x23, 0xf5, 0xd5, 0xac, 0x0b, 0x27, 0xa3, 0x84, 0xa2,
+	0x18, 0x52, 0xdc, 0x89, 0x01, 0x0f, 0x67, 0xf4, 0xb4, 0xe8, 0xda, 0x0a, 0x2c, 0xcb, 0xd1, 0xda,
+	0xfe, 0xc7, 0x12, 0xac, 0xa6, 0xa4, 0x22, 0x5f, 0xbc, 0x18, 0x35, 0x89, 0x43, 0x4c, 0x46, 0xbd,
+	0xe8, 0xe6, 0x78, 0x77, 0xaa, 0x39, 0x7c, 0xb9, 0x8a, 0xb9, 0xc4, 0xfd, 0x51, 0x1a, 0xf4, 0xb5,
+	0x5b, 0xb2, 0x30, 0x29, 0x3c, 0x23, 0x4a, 0xd0, 0x09, 0xe4, 0xf0, 0xf9, 0xb9, 0xed, 0x86, 0x15,
+	0x20, 0xae, 0x85, 0x3b, 0xe3, 0x76, 0xcc, 0xbd, 0x88, 0x46, 0xd4, 0x47, 0xcc, 0x21, 0xd7, 0x47,
+	0x0c, 0x43, 0x67, 0x50, 0x60, 0xd4, 0x21, 0x1e, 0x66, 0x36, 0x75, 0xe3, 0xad, 0xb3, 0x3c, 0x76,
+	0x71, 0x1d, 0x92, 0x0d, 0xa7, 0x91, 0xcc, 0xaa, 0xcb, 0x07, 0x44, 0xa1, 0x80, 0x5d, 0x97, 0xb2,
+	0x48, 0xec, 0xd2, 0xa4, 0x4d, 0x33, 0x1d, 0x9c, 0xbd, 0x84, 0x49, 0xc4, 0x86, 0xdf, 0x05, 0x92,
+	0x28, 0xf9, 0x2e, 0x90, 0xc0, 0x23, 0xbd, 0x31, 0xcf, 0xb7, 0x81, 0xe9, 0xbd, 0x71, 0x04, 0xc5,
+	0xf8, 0x3a, 0xa1, 0xee, 0x09, 0x75, 0x6c, 0xb3, 0xc7, 0x9f, 0x66, 0xf3, 0x62, 0xe2, 0xa5, 0x71,
+	0xf2, 0xc4, 0x4b, 0xe3, 0xd0, 0x27, 0x70, 0x33, 0xbe, 0x83, 0x47, 0xaa, 0x74, 0x91, 0x67, 0x69,
+	0x67, 0x5c, 0x40, 0xf5, 0x31, 0xf4, 0xb5, 0x3b, 0x51, 0x68, 0xc7, 0x4a, 0xd3, 0xc7, 0x42, 0x51,
+	0x13, 0xd6, 0x4d, 0x1c, 0x46, 0x36, 0xb9, 0xcc, 0xdf, 0x23, 0xe2, 0x8a, 0x58, 0xae, 0xbd, 0x3a,
+	0xe8, 0x6b, 0x77, 0xc7, 0xa0, 0x25, 0x6f, 0xc6, 0x71, 0x97, 0xda, 0xb0, 0x96, 0xa9, 0xd4, 0x17,
+	0xb2, 0xb2, 0x9f, 0x43, 0x31, 0x9d, 0xf5, 0x17, 0xb2, 0xab, 0xff, 0x45, 0x81, 0xcd, 0x93, 0xc0,
+	0xf1, 0xb1, 0xd7, 0x8c, 0xab, 0xf0, 0x88, 0xb6, 0x0e, 0x08, 0xc3, 0xb6, 0xe3, 0x87, 0xc2, 0xf8,
+	0x03, 0x40, 0xa4, 0x93, 0x0b, 0xe3, 0x00, 0x59, 0x98, 0x78, 0x55, 0x7b, 0x13, 0x16, 0x1e, 0xa7,
+	0x77, 0xf1, 0xf4, 0x4a, 0x24, 0x28, 0xd0, 0x3d, 0x58, 0x0c, 0x67, 0x2c, 0x61, 0xd1, 0x1e, 0xce,
+	0x3f, 0xd3, 0x04, 0x44, 0xfe, 0x4c, 0x13, 0x90, 0xef, 0x1e, 0x43, 0x41, 0x7a, 0xbf, 0x40, 0x05,
+	0x58, 0x3a, 0x6b, 0xbc, 0xd7, 0x38, 0xfe, 0x69, 0xa3, 0x38, 0x13, 0x1e, 0x4e, 0x0e, 0x1b, 0x07,
+	0xf5, 0xc6, 0x8f, 0x8b, 0x4a, 0x78, 0xd0, 0xcf, 0x1a, 0x8d, 0xf0, 0x30, 0x8b, 0x6e, 0x40, 0xbe,
+	0x79, 0xb6, 0xbf, 0x7f, 0x78, 0x78, 0x70, 0x78, 0x50, 0x9c, 0x43, 0x00, 0x8b, 0x3f, 0xda, 0xab,
+	0x3f, 0x3a, 0x3c, 0x28, 0xce, 0xd7, 0x7e, 0xf1, 0xd5, 0xd3, 0xb2, 0xf2, 0xf5, 0xd3, 0xb2, 0xf2,
+	0xef, 0xa7, 0x65, 0xe5, 0xb3, 0x67, 0xe5, 0x99, 0xaf, 0x9f, 0x95, 0x67, 0xfe, 0xf9, 0xac, 0x3c,
+	0xf3, 0xf3, 0x7d, 0xe9, 0x9f, 0x37, 0xe2, 0xc1, 0xac, 0xeb, 0xd1, 0xb0, 0x25, 0xa3, 0x53, 0xf5,
+	0x1a, 0xff, 0xa5, 0x6a, 0x2d, 0xf2, 0x39, 0xf6, 0xee, 0x7f, 0x03, 0x00, 0x00, 0xff, 0xff, 0xe8,
+	0x6a, 0xf7, 0x41, 0xd3, 0x1a, 0x00, 0x00,
 }
 
 func (m *Executor) Marshal() (dAtA []byte, err error) {
@@ -1694,13 +1693,6 @@ func (m *JobSchedulingInfo) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	_ = i
 	var l int
 	_ = l
-	if len(m.PodRequirementsHash) > 0 {
-		i -= len(m.PodRequirementsHash)
-		copy(dAtA[i:], m.PodRequirementsHash)
-		i = encodeVarintSchedulerobjects(dAtA, i, uint64(len(m.PodRequirementsHash)))
-		i--
-		dAtA[i] = 0x52
-	}
 	if m.Version != 0 {
 		i = encodeVarintSchedulerobjects(dAtA, i, uint64(m.Version))
 		i--
@@ -1851,6 +1843,13 @@ func (m *PodRequirements) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	_ = i
 	var l int
 	_ = l
+	if len(m.CachedSchedulingKey) > 0 {
+		i -= len(m.CachedSchedulingKey)
+		copy(dAtA[i:], m.CachedSchedulingKey)
+		i = encodeVarintSchedulerobjects(dAtA, i, uint64(len(m.CachedSchedulingKey)))
+		i--
+		dAtA[i] = 0x42
+	}
 	if len(m.Annotations) > 0 {
 		for k := range m.Annotations {
 			v := m.Annotations[k]
@@ -2268,10 +2267,6 @@ func (m *JobSchedulingInfo) Size() (n int) {
 	if m.Version != 0 {
 		n += 1 + sovSchedulerobjects(uint64(m.Version))
 	}
-	l = len(m.PodRequirementsHash)
-	if l > 0 {
-		n += 1 + l + sovSchedulerobjects(uint64(l))
-	}
 	return n
 }
 
@@ -2340,6 +2335,10 @@ func (m *PodRequirements) Size() (n int) {
 			n += mapEntrySize + 1 + sovSchedulerobjects(uint64(mapEntrySize))
 		}
 	}
+	l = len(m.CachedSchedulingKey)
+	if l > 0 {
+		n += 1 + l + sovSchedulerobjects(uint64(l))
+	}
 	return n
 }
 
@@ -5041,40 +5040,6 @@ func (m *JobSchedulingInfo) Unmarshal(dAtA []byte) error {
 					break
 				}
 			}
-		case 10:
-			if wireType != 2 {
-				return fmt.Errorf("proto: wrong wireType = %d for field PodRequirementsHash", wireType)
-			}
-			var byteLen int
-			for shift := uint(0); ; shift += 7 {
-				if shift >= 64 {
-					return ErrIntOverflowSchedulerobjects
-				}
-				if iNdEx >= l {
-					return io.ErrUnexpectedEOF
-				}
-				b := dAtA[iNdEx]
-				iNdEx++
-				byteLen |= int(b&0x7F) << shift
-				if b < 0x80 {
-					break
-				}
-			}
-			if byteLen < 0 {
-				return ErrInvalidLengthSchedulerobjects
-			}
-			postIndex := iNdEx + byteLen
-			if postIndex < 0 {
-				return ErrInvalidLengthSchedulerobjects
-			}
-			if postIndex > l {
-				return io.ErrUnexpectedEOF
-			}
-			m.PodRequirementsHash = append(m.PodRequirementsHash[:0], dAtA[iNdEx:postIndex]...)
-			if m.PodRequirementsHash == nil {
-				m.PodRequirementsHash = []byte{}
-			}
-			iNdEx = postIndex
 		default:
 			iNdEx = preIndex
 			skippy, err := skipSchedulerobjects(dAtA[iNdEx:])
@@ -5618,6 +5583,40 @@ func (m *PodRequirements) Unmarshal(dAtA []byte) error {
 			}
 			m.Annotations[mapkey] = mapvalue
 			iNdEx = postIndex
+		case 8:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field CachedSchedulingKey", wireType)
+			}
+			var byteLen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowSchedulerobjects
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				byteLen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if byteLen < 0 {
+				return ErrInvalidLengthSchedulerobjects
+			}
+			postIndex := iNdEx + byteLen
+			if postIndex < 0 {
+				return ErrInvalidLengthSchedulerobjects
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.CachedSchedulingKey = append(m.CachedSchedulingKey[:0], dAtA[iNdEx:postIndex]...)
+			if m.CachedSchedulingKey == nil {
+				m.CachedSchedulingKey = []byte{}
+			}
+			iNdEx = postIndex
 		default:
 			iNdEx = preIndex
 			skippy, err := skipSchedulerobjects(dAtA[iNdEx:])
diff --git a/internal/scheduler/schedulerobjects/schedulerobjects.proto b/internal/scheduler/schedulerobjects/schedulerobjects.proto
index e232e288..a24ac284 100644
--- a/internal/scheduler/schedulerobjects/schedulerobjects.proto
+++ b/internal/scheduler/schedulerobjects/schedulerobjects.proto
@@ -7,6 +7,8 @@ import "k8s.io/api/core/v1/generated.proto";
 import "k8s.io/apimachinery/pkg/api/resource/generated.proto";
 import "github.com/gogo/protobuf/gogoproto/gogo.proto";
 
+// option (gogoproto.stable_marshaler) = true;
+
 // Executor represents an executor running on a worker cluster
 message Executor {
     // Name of the executor.
@@ -112,6 +114,7 @@ message ResourceList {
 
 // Minimal job representation used by the scheduler.
 message JobSchedulingInfo {
+    option (gogoproto.stable_marshaler) = true;
     uint32 lifetime = 1;
     bool at_most_once = 2;
     bool preemptible = 3;
@@ -123,7 +126,6 @@ message JobSchedulingInfo {
     // Kubernetes objects that make up this job and their respective scheduling requirements.
     repeated ObjectRequirements object_requirements = 5;
     uint32 version = 9;
-    bytes podRequirementsHash = 10;
 }
 
 // Message capturing the scheduling requirements of a particular Kubernetes object.
@@ -150,6 +152,10 @@ message PodRequirements {
     string preemptionPolicy = 5;
     // Sum of the resource requirements for all containers that make up this pod.
     k8s.io.api.core.v1.ResourceRequirements resourceRequirements = 6 [(gogoproto.nullable) = false];
+    // This key is a hash of the scheduling requirements.
+    // The hash is 20 bytes in size and is guaranteed to be the same for equivalent PodRequirements,
+    // unless Affinity is used, in which case they may differ as a result of unordered map keys.
+    bytes cachedSchedulingKey = 8;
 }
 
 // Used to store details about pulsar scheduler jobs in Redis
diff --git a/internal/scheduler/submitcheck.go b/internal/scheduler/submitcheck.go
index 129d2006..9befd9fc 100644
--- a/internal/scheduler/submitcheck.go
+++ b/internal/scheduler/submitcheck.go
@@ -14,7 +14,6 @@ import (
 	"k8s.io/apimachinery/pkg/util/clock"
 
 	"github.com/armadaproject/armada/internal/armada/configuration"
-	protoutil "github.com/armadaproject/armada/internal/common/proto"
 	"github.com/armadaproject/armada/internal/scheduler/database"
 	"github.com/armadaproject/armada/internal/scheduler/nodedb"
 	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
@@ -163,31 +162,20 @@ func GroupJobsByAnnotation(annotation string, jobs []*api.Job) map[string][]*api
 }
 
 func (srv *SubmitChecker) getSchedulingResult(reqs []*schedulerobjects.PodRequirements) schedulingResult {
-	overwriteAnnotations(reqs)
-	reqsHash, err := protoutil.HashMany(reqs)
-	if err != nil {
-		return schedulingResult{isSchedulable: false, reason: err.Error()}
-	}
-	cachedResult, cacheExists := srv.jobSchedulingResultsCache.Get(string(reqsHash))
-	result, castSuccess := cachedResult.(schedulingResult)
-
-	if !cacheExists || !castSuccess {
-		result = srv.check(reqs)
-		srv.jobSchedulingResultsCache.Add(string(reqsHash), result)
-	}
-
-	return result
-}
-
-// overwriteAnnotations This sets all annotations to a constant value
-// This is needed to reduce the cardinality of PodRequirements - so they hash more consistently
-// To allow our caching to work effectively
-func overwriteAnnotations(reqs []*schedulerobjects.PodRequirements) {
 	for _, req := range reqs {
-		for key := range req.GetAnnotations() {
-			req.Annotations[key] = "submission-check"
+		schedulingKey := req.SchedulingKey()
+		var result schedulingResult
+		if obj, ok := srv.jobSchedulingResultsCache.Get(schedulingKey); ok {
+			result = obj.(schedulingResult)
+		} else {
+			result = srv.check(reqs)
+			srv.jobSchedulingResultsCache.Add(schedulingKey, result)
+		}
+		if !result.isSchedulable {
+			return result
 		}
 	}
+	return schedulingResult{isSchedulable: true}
 }
 
 // Check if a set of pods can be scheduled onto some cluster.
diff --git a/internal/scheduleringester/instructions.go b/internal/scheduleringester/instructions.go
index 4a91015e..6dd89c44 100644
--- a/internal/scheduleringester/instructions.go
+++ b/internal/scheduleringester/instructions.go
@@ -10,7 +10,6 @@ import (
 
 	"github.com/armadaproject/armada/internal/armada/configuration"
 	"github.com/armadaproject/armada/internal/common/compress"
-	"github.com/armadaproject/armada/internal/common/hash"
 	"github.com/armadaproject/armada/internal/common/ingest"
 	"github.com/armadaproject/armada/internal/common/ingest/metrics"
 	protoutil "github.com/armadaproject/armada/internal/common/proto"
@@ -366,13 +365,12 @@ func (c *InstructionConverter) schedulingInfoFromSubmitJob(submitJob *armadaeven
 			schedulingInfo.ObjectRequirements,
 			&schedulerobjects.ObjectRequirements{Requirements: requirements},
 		)
-		podRequirementsHash, err := hash.CalculatePodRequirementsHash(requirements.PodRequirements)
-		if err != nil {
-			return nil, err
-		}
-		schedulingInfo.PodRequirementsHash = podRequirementsHash
 	default:
 		return nil, errors.Errorf("unsupported object type %T", object)
 	}
+
+	// Call SchedulingKey() to trigger computing and caching the key.
+	_, _ = schedulingInfo.SchedulingKey()
+
 	return schedulingInfo, nil
 }
diff --git a/pkg/api/util.go b/pkg/api/util.go
index 903e82e0..8ea63eda 100644
--- a/pkg/api/util.go
+++ b/pkg/api/util.go
@@ -107,12 +107,15 @@ func (job *Job) GetRequirements(priorityClasses map[string]configuration.Priorit
 		}
 		objectRequirements[i] = &schedulerobjects.ObjectRequirements{
 			Requirements: &schedulerobjects.ObjectRequirements_PodRequirements{
-				PodRequirements: adapters.PodRequirementsFromPod(&v1.Pod{
-					ObjectMeta: metav1.ObjectMeta{
-						Annotations: job.Annotations,
+				PodRequirements: adapters.PodRequirementsFromPod(
+					&v1.Pod{
+						ObjectMeta: metav1.ObjectMeta{
+							Annotations: job.Annotations,
+						},
+						Spec: *podSpec,
 					},
-					Spec: *podSpec,
-				}, priorityClasses),
+					priorityClasses,
+				),
 			},
 		}
 	}


Test patch:
diff --git a/internal/common/hash/pod_requirements_test.go b/internal/common/hash/pod_requirements_test.go
deleted file mode 100644
index 2a3dbf74..00000000
--- a/internal/common/hash/pod_requirements_test.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package hash
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-
-	protoutil "github.com/armadaproject/armada/internal/common/proto"
-	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
-)
-
-func TestCalculatePodRequirementsHash(t *testing.T) {
-	podReqs := &schedulerobjects.PodRequirements{
-		Priority: 1,
-	}
-	expectedHash, err := protoutil.Hash(podReqs)
-	assert.NoError(t, err)
-
-	result, err := CalculatePodRequirementsHash(podReqs)
-	assert.NoError(t, err)
-	assert.Equal(t, expectedHash, result)
-
-	podReqs.Annotations = map[string]string{"test": "value"}
-	result, err = CalculatePodRequirementsHash(podReqs)
-	assert.NoError(t, err)
-	// The hash should be calculated ignoring annotations and therefore same as the original objects hash
-	assert.Equal(t, expectedHash, result)
-}
-
-func TestCalculatePodRequirementsHash_NilInput(t *testing.T) {
-	result, err := CalculatePodRequirementsHash(nil)
-
-	assert.Error(t, err)
-	assert.Equal(t, []byte{}, result)
-}
diff --git a/internal/common/proto/protoutil_test.go b/internal/common/proto/protoutil_test.go
index efc639da..3d717e9e 100644
--- a/internal/common/proto/protoutil_test.go
+++ b/internal/common/proto/protoutil_test.go
@@ -2,19 +2,13 @@ package protoutil
 
 import (
 	"testing"
-	"time"
 
 	"github.com/gogo/protobuf/proto"
 	"github.com/google/uuid"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
-	v1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/api/resource"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/utils/pointer"
 
 	"github.com/armadaproject/armada/internal/common/compress"
-	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
 	"github.com/armadaproject/armada/pkg/armadaevents"
 )
 
@@ -69,152 +63,3 @@ func TestMustMarshallAndCompress(t *testing.T) {
 	bytes := MustMarshallAndCompress(msg, compressor)
 	assert.Equal(t, compressedMsg, bytes)
 }
-
-func TestHashMany(t *testing.T) {
-	input1 := &schedulerobjects.JobSchedulingInfo{Lifetime: 1}
-	input2 := &schedulerobjects.JobSchedulingInfo{Lifetime: 2}
-	input3 := &schedulerobjects.JobSchedulingInfo{Lifetime: 3}
-	hash1, err := HashMany([]*schedulerobjects.JobSchedulingInfo{input1, input2})
-	require.NoError(t, err)
-	require.Equal(t, 20, len(hash1))
-	hash2, err := HashMany([]*schedulerobjects.JobSchedulingInfo{input1, input2})
-	require.NoError(t, err)
-	require.Equal(t, 20, len(hash2))
-	hash3, err := HashMany([]*schedulerobjects.JobSchedulingInfo{input1, input3})
-	require.NoError(t, err)
-	require.Equal(t, 20, len(hash3))
-	assert.Equal(t, hash1, hash2)
-	assert.NotEqual(t, hash1, hash3)
-}
-
-func TestHash_NoCollisions(t *testing.T) {
-	hashes := make(map[string]bool)
-	for i := 0; i < 100000; i++ {
-		intialHash, err := Hash(&schedulerobjects.JobSchedulingInfo{Lifetime: uint32(i)})
-		require.NoError(t, err)
-		hashStr := string(intialHash)
-		_, ok := hashes[hashStr]
-		require.False(t, ok, "Hash collision detected for %d", i)
-		hashes[hashStr] = true
-	}
-}
-
-func TestHash_TestConsistent(t *testing.T) {
-	schedulingInfo := &schedulerobjects.JobSchedulingInfo{
-		Lifetime:          1,
-		AtMostOnce:        true,
-		Preemptible:       true,
-		ConcurrencySafe:   true,
-		PriorityClassName: "armada-default",
-		SubmitTime:        time.Now(),
-		Priority:          10,
-		ObjectRequirements: []*schedulerobjects.ObjectRequirements{
-			{
-				Requirements: &schedulerobjects.ObjectRequirements_PodRequirements{
-					PodRequirements: &schedulerobjects.PodRequirements{
-						NodeSelector: map[string]string{
-							"property1": "value1",
-							"property3": "value3",
-						},
-						Affinity: &v1.Affinity{
-							NodeAffinity: &v1.NodeAffinity{
-								RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{
-									NodeSelectorTerms: []v1.NodeSelectorTerm{
-										{
-											MatchExpressions: []v1.NodeSelectorRequirement{
-												{
-													Key:      "k1",
-													Operator: "o1",
-													Values:   []string{"v1", "v2"},
-												},
-											},
-											MatchFields: []v1.NodeSelectorRequirement{
-												{
-													Key:      "k2",
-													Operator: "o2",
-													Values:   []string{"v10", "v20"},
-												},
-											},
-										},
-									},
-								},
-							},
-							PodAffinity: &v1.PodAffinity{
-								RequiredDuringSchedulingIgnoredDuringExecution: []v1.PodAffinityTerm{
-									{
-										LabelSelector: &metav1.LabelSelector{
-											MatchLabels: map[string]string{
-												"label1": "labelval1",
-												"label2": "labelval2",
-												"label3": "labelval3",
-											},
-											MatchExpressions: []metav1.LabelSelectorRequirement{
-												{
-													Key:      "k1",
-													Operator: "o1",
-													Values:   []string{"v1", "v2", "v3"},
-												},
-											},
-										},
-										Namespaces:  []string{"n1, n2, n3"},
-										TopologyKey: "topkey1",
-										NamespaceSelector: &metav1.LabelSelector{
-											MatchLabels: map[string]string{
-												"label10": "labelval1",
-												"label20": "labelval2",
-												"label30": "labelval3",
-											},
-											MatchExpressions: []metav1.LabelSelectorRequirement{
-												{
-													Key:      "k10",
-													Operator: "o10",
-													Values:   []string{"v10", "v20", "v30"},
-												},
-											},
-										},
-									},
-								},
-							},
-							PodAntiAffinity: nil,
-						},
-						Tolerations: []v1.Toleration{{
-							Key:               "a",
-							Operator:          "b",
-							Value:             "b",
-							Effect:            "d",
-							TolerationSeconds: pointer.Int64(1),
-						}},
-						Annotations: map[string]string{
-							"foo":  "bar",
-							"fish": "chips",
-							"salt": "pepper",
-						},
-						Priority:         1,
-						PreemptionPolicy: "abc",
-						ResourceRequirements: v1.ResourceRequirements{
-							Limits: map[v1.ResourceName]resource.Quantity{
-								"cpu":    resource.MustParse("1"),
-								"memory": resource.MustParse("2"),
-								"gpu":    resource.MustParse("3"),
-							},
-							Requests: map[v1.ResourceName]resource.Quantity{
-								"cpu":    resource.MustParse("2"),
-								"memory": resource.MustParse("2"),
-								"gpu":    resource.MustParse("2"),
-							},
-						},
-					},
-				},
-			},
-		},
-	}
-
-	intialHash, err := Hash(schedulingInfo)
-	require.NoError(t, err)
-
-	for i := 0; i < 1000; i++ {
-		hash, err := Hash(schedulingInfo)
-		require.NoError(t, err)
-		assert.Equal(t, intialHash, hash)
-	}
-}
diff --git a/internal/common/slices/slices_test.go b/internal/common/slices/slices_test.go
index 266b4dfe..3f1644d1 100644
--- a/internal/common/slices/slices_test.go
+++ b/internal/common/slices/slices_test.go
@@ -197,3 +197,47 @@ func TestUnique(t *testing.T) {
 		})
 	}
 }
+
+func TestFilter(t *testing.T) {
+	includeOver5 := func(val int) bool { return val > 5 }
+	input := []int{1, 3, 5, 7, 9}
+	expectedOutput := []int{7, 9}
+
+	output := Filter(input, includeOver5)
+	assert.Equal(t, expectedOutput, output)
+}
+
+func TestFilter_ExcludeAllFilter(t *testing.T) {
+	excludeAll := func(val int) bool { return false }
+	input := []int{1, 3, 5, 7, 9}
+	expectedOutput := []int{}
+	output := Filter(input, excludeAll)
+	assert.Equal(t, expectedOutput, output)
+}
+
+func TestFilter_IncludeAllFilter(t *testing.T) {
+	includeAll := func(val int) bool { return true }
+	input := []int{1, 3, 5, 7, 9}
+	expectedOutput := input
+
+	output := Filter(input, includeAll)
+	assert.Equal(t, expectedOutput, output)
+}
+
+func TestFilter_Empty(t *testing.T) {
+	includeAll := func(val int) bool { return true }
+	input := []int{}
+	expectedOutput := input
+
+	output := Filter(input, includeAll)
+	assert.Equal(t, expectedOutput, output)
+}
+
+func TestFilter_Nil(t *testing.T) {
+	includeAll := func(val int) bool { return true }
+	var input []int = nil
+	expectedOutput := input
+
+	output := Filter(input, includeAll)
+	assert.Equal(t, expectedOutput, output)
+}
diff --git a/internal/scheduler/nodedb/nodeiteration_test.go b/internal/scheduler/nodedb/nodeiteration_test.go
index 9ad2bbd4..7080f3a4 100644
--- a/internal/scheduler/nodedb/nodeiteration_test.go
+++ b/internal/scheduler/nodedb/nodeiteration_test.go
@@ -1,7 +1,6 @@
 package nodedb
 
 import (
-	"bytes"
 	"fmt"
 	"testing"
 
@@ -108,95 +107,6 @@ func TestNodePairIterator(t *testing.T) {
 	assert.Equal(t, expected, actual)
 }
 
-// The memdb internally uses bytes.Compare to compare keys.
-// Here, we test that byte representation comparison of quantities works as expected.
-func TestQuantityIndexComparison(t *testing.T) {
-	tests := map[string]struct {
-		A resource.Quantity
-		B resource.Quantity
-	}{
-		"10Mi 10Mi": {
-			A: resource.MustParse("10Mi"),
-			B: resource.MustParse("10Mi"),
-		},
-		"5Mi 10Mi": {
-			A: resource.MustParse("5Mi"),
-			B: resource.MustParse("10Mi"),
-		},
-		"10Gi 10Gi": {
-			A: resource.MustParse("10Gi"),
-			B: resource.MustParse("10Gi"),
-		},
-		"5Gi 10Gi": {
-			A: resource.MustParse("5Gi"),
-			B: resource.MustParse("10Gi"),
-		},
-		"1 1": {
-			A: resource.MustParse("1"),
-			B: resource.MustParse("1"),
-		},
-		"1 2": {
-			A: resource.MustParse("1"),
-			B: resource.MustParse("2"),
-		},
-		"-1 1": {
-			A: resource.MustParse("-1"),
-			B: resource.MustParse("1"),
-		},
-		"100m 100m": {
-			A: resource.MustParse("100M"),
-			B: resource.MustParse("100M"),
-		},
-		"100m 200m": {
-			A: resource.MustParse("100M"),
-			B: resource.MustParse("200M"),
-		},
-		"54870m 54871m": {
-			A: resource.MustParse("54870m"),
-			B: resource.MustParse("54871m"),
-		},
-		"1000Ti 1001Ti": {
-			A: resource.MustParse("1000Ti"),
-			B: resource.MustParse("1001Ti"),
-		},
-		"1000Pi 1001Pi": {
-			A: resource.MustParse("1000Pi"),
-			B: resource.MustParse("1001Pi"),
-		},
-		"1 1001m": {
-			A: resource.MustParse("1"),
-			B: resource.MustParse("1001m"),
-		},
-		"1 1000m": {
-			A: resource.MustParse("1"),
-			B: resource.MustParse("1000m"),
-		},
-		"1Gi 1001Mi": {
-			A: resource.MustParse("1Gi"),
-			B: resource.MustParse("1001Mi"),
-		},
-		"1Gi 1000Mi": {
-			A: resource.MustParse("1Gi"),
-			B: resource.MustParse("1000Mi"),
-		},
-		"5188205838208Ki 5188205838209Ki": {
-			A: resource.MustParse("5188205838208Ki"),
-			B: resource.MustParse("5188205838209Ki"),
-		},
-	}
-	for name, tc := range tests {
-		t.Run(name, func(t *testing.T) {
-			expected := tc.A.Cmp(tc.B)
-			actual := bytes.Compare(encodeQuantity(tc.A), encodeQuantity(tc.B))
-			assert.Equal(t, expected, actual)
-
-			expected = tc.B.Cmp(tc.A)
-			actual = bytes.Compare(encodeQuantity(tc.B), encodeQuantity(tc.A))
-			assert.Equal(t, expected, actual)
-		})
-	}
-}
-
 func TestNodeTypeIterator(t *testing.T) {
 	tests := map[string]struct {
 		nodes            []*schedulerobjects.Node
diff --git a/internal/scheduler/scheduler_test.go b/internal/scheduler/scheduler_test.go
index c1581680..e6116d53 100644
--- a/internal/scheduler/scheduler_test.go
+++ b/internal/scheduler/scheduler_test.go
@@ -14,7 +14,6 @@ import (
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/util/clock"
 
-	"github.com/armadaproject/armada/internal/common/hash"
 	protoutil "github.com/armadaproject/armada/internal/common/proto"
 	"github.com/armadaproject/armada/internal/common/stringinterner"
 	"github.com/armadaproject/armada/internal/common/util"
@@ -63,21 +62,6 @@ var (
 	updatedSchedulingInfoBytes = protoutil.MustMarshall(updatedSchedulingInfo)
 )
 
-func init() {
-	podRequirementsHash, err := hash.CalculatePodRequirementsHash(schedulingInfo.GetObjectRequirements()[0].GetPodRequirements())
-	if err != nil {
-		panic(err)
-	}
-	schedulingInfo.PodRequirementsHash = podRequirementsHash
-	schedulingInfoBytes = protoutil.MustMarshall(schedulingInfo)
-	updatedPodRequirementsHash, err := hash.CalculatePodRequirementsHash(updatedSchedulingInfo.GetObjectRequirements()[0].GetPodRequirements())
-	if err != nil {
-		panic(err)
-	}
-	updatedSchedulingInfo.PodRequirementsHash = updatedPodRequirementsHash
-	updatedSchedulingInfoBytes = protoutil.MustMarshall(updatedSchedulingInfo)
-}
-
 var queuedJob = jobdb.NewJob(
 	util.NewULID(),
 	"testJobset",
@@ -529,9 +513,6 @@ func TestScheduler_TestCycle(t *testing.T) {
 				}
 				podRequirements := PodRequirementFromJobSchedulingInfo(job.JobSchedulingInfo())
 				assert.NotNil(t, podRequirements)
-				expectedPodRequirementsHash, err := hash.CalculatePodRequirementsHash(podRequirements)
-				assert.NoError(t, err)
-				assert.Equal(t, expectedPodRequirementsHash, job.JobSchedulingInfo().PodRequirementsHash)
 
 				expectedQueuedVersion := int32(1)
 				if tc.expectedQueuedVersion != 0 {
diff --git a/internal/scheduler/schedulerobjects/podutils_test.go b/internal/scheduler/schedulerobjects/podutils_test.go
new file mode 100644
index 00000000..19afff0e
--- /dev/null
+++ b/internal/scheduler/schedulerobjects/podutils_test.go
@@ -0,0 +1,1008 @@
+package schedulerobjects
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	v1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/utils/pointer"
+)
+
+func TestSchedulingKey(t *testing.T) {
+	defaultN := 10 // Run the check several times to check for consistency.
+	tests := map[string]struct {
+		a     *PodRequirements
+		b     *PodRequirements
+		equal bool
+		n     int
+	}{
+		"annontations does not affect key": {
+			a: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property1": "value1",
+					"property3": "value3",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a",
+					Operator:          "b",
+					Value:             "b",
+					Effect:            "d",
+					TolerationSeconds: pointer.Int64(1),
+				}},
+				Annotations: map[string]string{
+					"foo":  "bar",
+					"fish": "chips",
+					"salt": "pepper",
+				},
+				Priority:         1,
+				PreemptionPolicy: "abc",
+				ResourceRequirements: v1.ResourceRequirements{
+					Limits: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("1"),
+						"memory": resource.MustParse("2"),
+						"gpu":    resource.MustParse("3"),
+					},
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("2"),
+						"memory": resource.MustParse("2"),
+						"gpu":    resource.MustParse("2"),
+					},
+				},
+			},
+			b: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property1": "value1",
+					"property3": "value3",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a",
+					Operator:          "b",
+					Value:             "b",
+					Effect:            "d",
+					TolerationSeconds: pointer.Int64(1),
+				}},
+				Annotations: map[string]string{
+					"foo":  "bar",
+					"fish": "chips",
+				},
+				Priority:         1,
+				PreemptionPolicy: "abc",
+				ResourceRequirements: v1.ResourceRequirements{
+					Limits: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("1"),
+						"memory": resource.MustParse("2"),
+						"gpu":    resource.MustParse("3"),
+					},
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("2"),
+						"memory": resource.MustParse("2"),
+						"gpu":    resource.MustParse("2"),
+					},
+				},
+			},
+			equal: true,
+		},
+		"preemptionPolicy does not affect key": {
+			a: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property1": "value1",
+					"property3": "value3",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a",
+					Operator:          "b",
+					Value:             "b",
+					Effect:            "d",
+					TolerationSeconds: pointer.Int64(1),
+				}},
+				Priority:         1,
+				PreemptionPolicy: "abc",
+				ResourceRequirements: v1.ResourceRequirements{
+					Limits: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("1"),
+						"memory": resource.MustParse("2"),
+						"gpu":    resource.MustParse("3"),
+					},
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("2"),
+						"memory": resource.MustParse("2"),
+						"gpu":    resource.MustParse("2"),
+					},
+				},
+			},
+			b: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property1": "value1",
+					"property3": "value3",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a",
+					Operator:          "b",
+					Value:             "b",
+					Effect:            "d",
+					TolerationSeconds: pointer.Int64(1),
+				}},
+				Priority:         1,
+				PreemptionPolicy: "abcdef",
+				ResourceRequirements: v1.ResourceRequirements{
+					Limits: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("1"),
+						"memory": resource.MustParse("2"),
+						"gpu":    resource.MustParse("3"),
+					},
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("2"),
+						"memory": resource.MustParse("2"),
+						"gpu":    resource.MustParse("2"),
+					},
+				},
+			},
+			equal: true,
+		},
+		"limits does not affect key": {
+			a: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property1": "value1",
+					"property3": "value3",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a",
+					Operator:          "b",
+					Value:             "b",
+					Effect:            "d",
+					TolerationSeconds: pointer.Int64(1),
+				}},
+				Priority:         1,
+				PreemptionPolicy: "abc",
+				ResourceRequirements: v1.ResourceRequirements{
+					Limits: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("1"),
+						"memory": resource.MustParse("2"),
+						"gpu":    resource.MustParse("3"),
+					},
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("4"),
+						"memory": resource.MustParse("5"),
+						"gpu":    resource.MustParse("6"),
+					},
+				},
+			},
+			b: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property1": "value1",
+					"property3": "value3",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a",
+					Operator:          "b",
+					Value:             "b",
+					Effect:            "d",
+					TolerationSeconds: pointer.Int64(1),
+				}},
+				Priority:         1,
+				PreemptionPolicy: "abcdef",
+				ResourceRequirements: v1.ResourceRequirements{
+					Limits: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("1"),
+						"memory": resource.MustParse("2"),
+						"gpu":    resource.MustParse("4"),
+					},
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("4"),
+						"memory": resource.MustParse("5"),
+						"gpu":    resource.MustParse("6"),
+					},
+				},
+			},
+			equal: true,
+		},
+		"priority": {
+			a: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property1": "value1",
+					"property3": "value3",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a",
+					Operator:          "b",
+					Value:             "b",
+					Effect:            "d",
+					TolerationSeconds: pointer.Int64(1),
+				}},
+				Priority: 1,
+				ResourceRequirements: v1.ResourceRequirements{
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("4"),
+						"memory": resource.MustParse("5"),
+						"gpu":    resource.MustParse("6"),
+					},
+				},
+			},
+			b: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property1": "value1",
+					"property3": "value3",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a",
+					Operator:          "b",
+					Value:             "b",
+					Effect:            "d",
+					TolerationSeconds: pointer.Int64(1),
+				}},
+				Priority: 2,
+				ResourceRequirements: v1.ResourceRequirements{
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("4"),
+						"memory": resource.MustParse("5"),
+						"gpu":    resource.MustParse("6"),
+					},
+				},
+			},
+		},
+		"zero request does not affect key": {
+			a: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property1": "value1",
+					"property3": "value3",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a",
+					Operator:          "b",
+					Value:             "b",
+					Effect:            "d",
+					TolerationSeconds: pointer.Int64(1),
+				}},
+				Priority: 1,
+				ResourceRequirements: v1.ResourceRequirements{
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("4"),
+						"memory": resource.MustParse("5"),
+						"gpu":    resource.MustParse("6"),
+					},
+				},
+			},
+			b: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property3": "value3",
+					"property1": "value1",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a",
+					Operator:          "b",
+					Value:             "b",
+					Effect:            "d",
+					TolerationSeconds: pointer.Int64(1),
+				}},
+				Priority: 1,
+				ResourceRequirements: v1.ResourceRequirements{
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("4"),
+						"memory": resource.MustParse("5"),
+						"gpu":    resource.MustParse("6"),
+						"foo":    resource.MustParse("0"),
+					},
+				},
+			},
+			equal: true,
+		},
+		"nodeSelector key": {
+			a: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property1": "value1",
+					"property3": "value3",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a",
+					Operator:          "b",
+					Value:             "b",
+					Effect:            "d",
+					TolerationSeconds: pointer.Int64(1),
+				}},
+				Priority: 1,
+				ResourceRequirements: v1.ResourceRequirements{
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("4"),
+						"memory": resource.MustParse("5"),
+						"gpu":    resource.MustParse("6"),
+					},
+				},
+			},
+			b: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property3": "value3",
+					"property1": "value1",
+					"property2": "value2",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a",
+					Operator:          "b",
+					Value:             "b",
+					Effect:            "d",
+					TolerationSeconds: pointer.Int64(1),
+				}},
+				Priority: 1,
+				ResourceRequirements: v1.ResourceRequirements{
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("4"),
+						"memory": resource.MustParse("5"),
+						"gpu":    resource.MustParse("6"),
+					},
+				},
+			},
+		},
+		"nodeSelector value": {
+			a: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property1": "value1",
+					"property3": "value3",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a",
+					Operator:          "b",
+					Value:             "b",
+					Effect:            "d",
+					TolerationSeconds: pointer.Int64(1),
+				}},
+				Priority: 1,
+				ResourceRequirements: v1.ResourceRequirements{
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("4"),
+						"memory": resource.MustParse("5"),
+						"gpu":    resource.MustParse("6"),
+					},
+				},
+			},
+			b: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property3": "value3",
+					"property1": "value1-2",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a",
+					Operator:          "b",
+					Value:             "b",
+					Effect:            "d",
+					TolerationSeconds: pointer.Int64(1),
+				}},
+				Priority: 1,
+				ResourceRequirements: v1.ResourceRequirements{
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("4"),
+						"memory": resource.MustParse("5"),
+						"gpu":    resource.MustParse("6"),
+					},
+				},
+			},
+		},
+		"toleration key": {
+			a: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property1": "value1",
+					"property3": "value3",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a",
+					Operator:          "b",
+					Value:             "b",
+					Effect:            "d",
+					TolerationSeconds: pointer.Int64(1),
+				}},
+				Priority: 1,
+				ResourceRequirements: v1.ResourceRequirements{
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("4"),
+						"memory": resource.MustParse("5"),
+						"gpu":    resource.MustParse("6"),
+					},
+				},
+			},
+			b: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property3": "value3",
+					"property1": "value1",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a-2",
+					Operator:          "b",
+					Value:             "b",
+					Effect:            "d",
+					TolerationSeconds: pointer.Int64(1),
+				}},
+				Priority: 1,
+				ResourceRequirements: v1.ResourceRequirements{
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("4"),
+						"memory": resource.MustParse("5"),
+						"gpu":    resource.MustParse("6"),
+					},
+				},
+			},
+		},
+		"toleration operator": {
+			a: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property1": "value1",
+					"property3": "value3",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a",
+					Operator:          "b",
+					Value:             "b",
+					Effect:            "d",
+					TolerationSeconds: pointer.Int64(1),
+				}},
+				Priority: 1,
+				ResourceRequirements: v1.ResourceRequirements{
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("4"),
+						"memory": resource.MustParse("5"),
+						"gpu":    resource.MustParse("6"),
+					},
+				},
+			},
+			b: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property3": "value3",
+					"property1": "value1",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a",
+					Operator:          "b-2",
+					Value:             "b",
+					Effect:            "d",
+					TolerationSeconds: pointer.Int64(1),
+				}},
+				Priority: 1,
+				ResourceRequirements: v1.ResourceRequirements{
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("4"),
+						"memory": resource.MustParse("5"),
+						"gpu":    resource.MustParse("6"),
+					},
+				},
+			},
+		},
+		"toleration value": {
+			a: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property1": "value1",
+					"property3": "value3",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a",
+					Operator:          "b",
+					Value:             "b",
+					Effect:            "d",
+					TolerationSeconds: pointer.Int64(1),
+				}},
+				Priority: 1,
+				ResourceRequirements: v1.ResourceRequirements{
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("4"),
+						"memory": resource.MustParse("5"),
+						"gpu":    resource.MustParse("6"),
+					},
+				},
+			},
+			b: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property3": "value3",
+					"property1": "value1",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a",
+					Operator:          "b",
+					Value:             "b-2",
+					Effect:            "d",
+					TolerationSeconds: pointer.Int64(1),
+				}},
+				Priority: 1,
+				ResourceRequirements: v1.ResourceRequirements{
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("4"),
+						"memory": resource.MustParse("5"),
+						"gpu":    resource.MustParse("6"),
+					},
+				},
+			},
+		},
+		"toleration effect": {
+			a: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property1": "value1",
+					"property3": "value3",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a",
+					Operator:          "b",
+					Value:             "b",
+					Effect:            "d",
+					TolerationSeconds: pointer.Int64(1),
+				}},
+				Priority: 1,
+				ResourceRequirements: v1.ResourceRequirements{
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("4"),
+						"memory": resource.MustParse("5"),
+						"gpu":    resource.MustParse("6"),
+					},
+				},
+			},
+			b: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property3": "value3",
+					"property1": "value1",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a",
+					Operator:          "b",
+					Value:             "b",
+					Effect:            "d-2",
+					TolerationSeconds: pointer.Int64(1),
+				}},
+				Priority: 1,
+				ResourceRequirements: v1.ResourceRequirements{
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("4"),
+						"memory": resource.MustParse("5"),
+						"gpu":    resource.MustParse("6"),
+					},
+				},
+			},
+		},
+		"toleration tolerationSeconds": {
+			a: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property1": "value1",
+					"property3": "value3",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a",
+					Operator:          "b",
+					Value:             "b",
+					Effect:            "d",
+					TolerationSeconds: pointer.Int64(1),
+				}},
+				Priority: 1,
+				ResourceRequirements: v1.ResourceRequirements{
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("4"),
+						"memory": resource.MustParse("5"),
+						"gpu":    resource.MustParse("6"),
+					},
+				},
+			},
+			b: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property3": "value3",
+					"property1": "value1",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a",
+					Operator:          "b",
+					Value:             "b",
+					Effect:            "d",
+					TolerationSeconds: pointer.Int64(2),
+				}},
+				Priority: 1,
+				ResourceRequirements: v1.ResourceRequirements{
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("4"),
+						"memory": resource.MustParse("5"),
+						"gpu":    resource.MustParse("6"),
+					},
+				},
+			},
+			equal: true,
+		},
+		"key ordering": {
+			a: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property1": "value1",
+					"property3": "value3",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a",
+					Operator:          "b",
+					Value:             "b",
+					Effect:            "d",
+					TolerationSeconds: pointer.Int64(1),
+				}},
+				Priority: 1,
+				ResourceRequirements: v1.ResourceRequirements{
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("4"),
+						"memory": resource.MustParse("5"),
+						"gpu":    resource.MustParse("6"),
+					},
+				},
+			},
+			b: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property3": "value3",
+					"property1": "value1",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a",
+					Operator:          "b",
+					Value:             "b",
+					Effect:            "d",
+					TolerationSeconds: pointer.Int64(1),
+				}},
+				Priority: 2,
+				ResourceRequirements: v1.ResourceRequirements{
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"memory": resource.MustParse("5"),
+						"gpu":    resource.MustParse("6"),
+						"cpu":    resource.MustParse("4"),
+					},
+				},
+			},
+		},
+		"affinity MatchLabels": {
+			a: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property1": "value1",
+					"property3": "value3",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a",
+					Operator:          "b",
+					Value:             "b",
+					Effect:            "d",
+					TolerationSeconds: pointer.Int64(1),
+				}},
+				Priority: 1,
+				ResourceRequirements: v1.ResourceRequirements{
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("4"),
+						"memory": resource.MustParse("5"),
+						"gpu":    resource.MustParse("6"),
+					},
+				},
+				Affinity: &v1.Affinity{
+					NodeAffinity: &v1.NodeAffinity{
+						RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{
+							NodeSelectorTerms: []v1.NodeSelectorTerm{
+								{
+									MatchExpressions: []v1.NodeSelectorRequirement{
+										{
+											Key:      "k1",
+											Operator: "o1",
+											Values:   []string{"v1", "v2"},
+										},
+									},
+									MatchFields: []v1.NodeSelectorRequirement{
+										{
+											Key:      "k2",
+											Operator: "o2",
+											Values:   []string{"v10", "v20"},
+										},
+									},
+								},
+							},
+						},
+					},
+					PodAffinity: &v1.PodAffinity{
+						RequiredDuringSchedulingIgnoredDuringExecution: []v1.PodAffinityTerm{
+							{
+								LabelSelector: &metav1.LabelSelector{
+									MatchLabels: map[string]string{
+										"label1": "labelval1",
+										"label2": "labelval2",
+										"label3": "labelval3",
+									},
+									MatchExpressions: []metav1.LabelSelectorRequirement{
+										{
+											Key:      "k1",
+											Operator: "o1",
+											Values:   []string{"v1", "v2", "v3"},
+										},
+									},
+								},
+								Namespaces:  []string{"n1, n2, n3"},
+								TopologyKey: "topkey1",
+								NamespaceSelector: &metav1.LabelSelector{
+									MatchLabels: map[string]string{
+										"label10": "labelval1",
+										"label20": "labelval2",
+										"label30": "labelval3",
+									},
+									MatchExpressions: []metav1.LabelSelectorRequirement{
+										{
+											Key:      "k10",
+											Operator: "o10",
+											Values:   []string{"v10", "v20", "v30"},
+										},
+									},
+								},
+							},
+						},
+					},
+					PodAntiAffinity: nil,
+				},
+			},
+			b: &PodRequirements{
+				NodeSelector: map[string]string{
+					"property3": "value3",
+					"property1": "value1",
+				},
+				Tolerations: []v1.Toleration{{
+					Key:               "a",
+					Operator:          "b",
+					Value:             "b",
+					Effect:            "d",
+					TolerationSeconds: pointer.Int64(1),
+				}},
+				Priority: 1,
+				ResourceRequirements: v1.ResourceRequirements{
+					Requests: map[v1.ResourceName]resource.Quantity{
+						"cpu":    resource.MustParse("4"),
+						"memory": resource.MustParse("5"),
+						"gpu":    resource.MustParse("6"),
+					},
+				},
+				Affinity: &v1.Affinity{
+					NodeAffinity: &v1.NodeAffinity{
+						RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{
+							NodeSelectorTerms: []v1.NodeSelectorTerm{
+								{
+									MatchExpressions: []v1.NodeSelectorRequirement{
+										{
+											Key:      "k1",
+											Operator: "o1",
+											Values:   []string{"v1", "v2"},
+										},
+									},
+									MatchFields: []v1.NodeSelectorRequirement{
+										{
+											Key:      "k2",
+											Operator: "o2",
+											Values:   []string{"v10", "v20"},
+										},
+									},
+								},
+							},
+						},
+					},
+					PodAffinity: &v1.PodAffinity{
+						RequiredDuringSchedulingIgnoredDuringExecution: []v1.PodAffinityTerm{
+							{
+								LabelSelector: &metav1.LabelSelector{
+									MatchLabels: map[string]string{
+										"label1": "labelval1-2",
+										"label2": "labelval2",
+										"label3": "labelval3",
+									},
+									MatchExpressions: []metav1.LabelSelectorRequirement{
+										{
+											Key:      "k1",
+											Operator: "o1",
+											Values:   []string{"v1", "v2", "v3"},
+										},
+									},
+								},
+								Namespaces:  []string{"n1, n2, n3"},
+								TopologyKey: "topkey1",
+								NamespaceSelector: &metav1.LabelSelector{
+									MatchLabels: map[string]string{
+										"label10": "labelval1",
+										"label20": "labelval2",
+										"label30": "labelval3",
+									},
+									MatchExpressions: []metav1.LabelSelectorRequirement{
+										{
+											Key:      "k10",
+											Operator: "o10",
+											Values:   []string{"v10", "v20", "v30"},
+										},
+									},
+								},
+							},
+						},
+					},
+					PodAntiAffinity: nil,
+				},
+			},
+		},
+	}
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			schedulingKeyA := tc.a.SchedulingKey()
+			schedulingKeyB := tc.b.SchedulingKey()
+			var prevSchedulingKeyA [SchedulingKeySize]byte
+			var prevSchedulingKeyB [SchedulingKeySize]byte
+			n := defaultN
+			if tc.n != 0 {
+				n = tc.n
+			}
+			for i := 0; i < n; i++ {
+				tc.a.ClearCachedSchedulingKey()
+				tc.b.ClearCachedSchedulingKey()
+				if tc.equal {
+					assert.Equal(t, schedulingKeyA, schedulingKeyB)
+				} else {
+					assert.NotEqual(t, schedulingKeyA, schedulingKeyB)
+				}
+				if i != 0 {
+					assert.Equal(t, prevSchedulingKeyA, schedulingKeyA)
+					assert.Equal(t, prevSchedulingKeyB, schedulingKeyB)
+				}
+				prevSchedulingKeyA = schedulingKeyA
+				prevSchedulingKeyB = schedulingKeyB
+			}
+		})
+	}
+}
+
+func BenchmarkSchedulingKey(b *testing.B) {
+	jobSchedulingInfo := &JobSchedulingInfo{
+		Lifetime:          1,
+		AtMostOnce:        true,
+		Preemptible:       true,
+		ConcurrencySafe:   true,
+		PriorityClassName: "armada-default",
+		Priority:          10,
+		ObjectRequirements: []*ObjectRequirements{
+			{
+				Requirements: &ObjectRequirements_PodRequirements{
+					PodRequirements: &PodRequirements{
+						NodeSelector: map[string]string{
+							"property1": "value1",
+							"property3": "value3",
+						},
+						Tolerations: []v1.Toleration{{
+							Key:               "a",
+							Operator:          "b",
+							Value:             "b",
+							Effect:            "d",
+							TolerationSeconds: pointer.Int64(1),
+						}},
+						Annotations: map[string]string{
+							"foo":  "bar",
+							"fish": "chips",
+							"salt": "pepper",
+						},
+						Priority:         1,
+						PreemptionPolicy: "abc",
+						ResourceRequirements: v1.ResourceRequirements{
+							Limits: map[v1.ResourceName]resource.Quantity{
+								"cpu":    resource.MustParse("1"),
+								"memory": resource.MustParse("2"),
+								"gpu":    resource.MustParse("3"),
+							},
+							Requests: map[v1.ResourceName]resource.Quantity{
+								"cpu":    resource.MustParse("2"),
+								"memory": resource.MustParse("2"),
+								"gpu":    resource.MustParse("2"),
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+	b.ResetTimer()
+	for n := 0; n < b.N; n++ {
+		jobSchedulingInfo.ClearCachedSchedulingKey()
+		jobSchedulingInfo.SchedulingKey()
+	}
+}
+
+func BenchmarkSchedulingKey_Affinity(b *testing.B) {
+	jobSchedulingInfo := &JobSchedulingInfo{
+		Lifetime:          1,
+		AtMostOnce:        true,
+		Preemptible:       true,
+		ConcurrencySafe:   true,
+		PriorityClassName: "armada-default",
+		Priority:          10,
+		ObjectRequirements: []*ObjectRequirements{
+			{
+				Requirements: &ObjectRequirements_PodRequirements{
+					PodRequirements: &PodRequirements{
+						NodeSelector: map[string]string{
+							"property1": "value1",
+							"property3": "value3",
+						},
+						Affinity: &v1.Affinity{
+							NodeAffinity: &v1.NodeAffinity{
+								RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{
+									NodeSelectorTerms: []v1.NodeSelectorTerm{
+										{
+											MatchExpressions: []v1.NodeSelectorRequirement{
+												{
+													Key:      "k1",
+													Operator: "o1",
+													Values:   []string{"v1", "v2"},
+												},
+											},
+											MatchFields: []v1.NodeSelectorRequirement{
+												{
+													Key:      "k2",
+													Operator: "o2",
+													Values:   []string{"v10", "v20"},
+												},
+											},
+										},
+									},
+								},
+							},
+							PodAffinity: &v1.PodAffinity{
+								RequiredDuringSchedulingIgnoredDuringExecution: []v1.PodAffinityTerm{
+									{
+										LabelSelector: &metav1.LabelSelector{
+											MatchLabels: map[string]string{
+												"label1": "labelval1",
+												"label2": "labelval2",
+												"label3": "labelval3",
+											},
+											MatchExpressions: []metav1.LabelSelectorRequirement{
+												{
+													Key:      "k1",
+													Operator: "o1",
+													Values:   []string{"v1", "v2", "v3"},
+												},
+											},
+										},
+										Namespaces:  []string{"n1, n2, n3"},
+										TopologyKey: "topkey1",
+										NamespaceSelector: &metav1.LabelSelector{
+											MatchLabels: map[string]string{
+												"label10": "labelval1",
+												"label20": "labelval2",
+												"label30": "labelval3",
+											},
+											MatchExpressions: []metav1.LabelSelectorRequirement{
+												{
+													Key:      "k10",
+													Operator: "o10",
+													Values:   []string{"v10", "v20", "v30"},
+												},
+											},
+										},
+									},
+								},
+							},
+							PodAntiAffinity: nil,
+						},
+						Tolerations: []v1.Toleration{{
+							Key:               "a",
+							Operator:          "b",
+							Value:             "b",
+							Effect:            "d",
+							TolerationSeconds: pointer.Int64(1),
+						}},
+						Annotations: map[string]string{
+							"foo":  "bar",
+							"fish": "chips",
+							"salt": "pepper",
+						},
+						Priority:         1,
+						PreemptionPolicy: "abc",
+						ResourceRequirements: v1.ResourceRequirements{
+							Limits: map[v1.ResourceName]resource.Quantity{
+								"cpu":    resource.MustParse("1"),
+								"memory": resource.MustParse("2"),
+								"gpu":    resource.MustParse("3"),
+							},
+							Requests: map[v1.ResourceName]resource.Quantity{
+								"cpu":    resource.MustParse("2"),
+								"memory": resource.MustParse("2"),
+								"gpu":    resource.MustParse("2"),
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+	b.ResetTimer()
+	for n := 0; n < b.N; n++ {
+		jobSchedulingInfo.ClearCachedSchedulingKey()
+		jobSchedulingInfo.SchedulingKey()
+	}
+}
diff --git a/internal/scheduler/schedulerobjects/quantity_test.go b/internal/scheduler/schedulerobjects/quantity_test.go
new file mode 100644
index 00000000..5fa46b2a
--- /dev/null
+++ b/internal/scheduler/schedulerobjects/quantity_test.go
@@ -0,0 +1,98 @@
+package schedulerobjects
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"k8s.io/apimachinery/pkg/api/resource"
+)
+
+// The memdb internally uses bytes.Compare to compare keys.
+// Here, we test that byte representation comparison of quantities works as expected.
+func TestQuantityIndexComparison(t *testing.T) {
+	tests := map[string]struct {
+		A resource.Quantity
+		B resource.Quantity
+	}{
+		"10Mi 10Mi": {
+			A: resource.MustParse("10Mi"),
+			B: resource.MustParse("10Mi"),
+		},
+		"5Mi 10Mi": {
+			A: resource.MustParse("5Mi"),
+			B: resource.MustParse("10Mi"),
+		},
+		"10Gi 10Gi": {
+			A: resource.MustParse("10Gi"),
+			B: resource.MustParse("10Gi"),
+		},
+		"5Gi 10Gi": {
+			A: resource.MustParse("5Gi"),
+			B: resource.MustParse("10Gi"),
+		},
+		"1 1": {
+			A: resource.MustParse("1"),
+			B: resource.MustParse("1"),
+		},
+		"1 2": {
+			A: resource.MustParse("1"),
+			B: resource.MustParse("2"),
+		},
+		"-1 1": {
+			A: resource.MustParse("-1"),
+			B: resource.MustParse("1"),
+		},
+		"100m 100m": {
+			A: resource.MustParse("100M"),
+			B: resource.MustParse("100M"),
+		},
+		"100m 200m": {
+			A: resource.MustParse("100M"),
+			B: resource.MustParse("200M"),
+		},
+		"54870m 54871m": {
+			A: resource.MustParse("54870m"),
+			B: resource.MustParse("54871m"),
+		},
+		"1000Ti 1001Ti": {
+			A: resource.MustParse("1000Ti"),
+			B: resource.MustParse("1001Ti"),
+		},
+		"1000Pi 1001Pi": {
+			A: resource.MustParse("1000Pi"),
+			B: resource.MustParse("1001Pi"),
+		},
+		"1 1001m": {
+			A: resource.MustParse("1"),
+			B: resource.MustParse("1001m"),
+		},
+		"1 1000m": {
+			A: resource.MustParse("1"),
+			B: resource.MustParse("1000m"),
+		},
+		"1Gi 1001Mi": {
+			A: resource.MustParse("1Gi"),
+			B: resource.MustParse("1001Mi"),
+		},
+		"1Gi 1000Mi": {
+			A: resource.MustParse("1Gi"),
+			B: resource.MustParse("1000Mi"),
+		},
+		"5188205838208Ki 5188205838209Ki": {
+			A: resource.MustParse("5188205838208Ki"),
+			B: resource.MustParse("5188205838209Ki"),
+		},
+	}
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			expected := tc.A.Cmp(tc.B)
+			actual := bytes.Compare(EncodeQuantity(tc.A), EncodeQuantity(tc.B))
+			assert.Equal(t, expected, actual)
+
+			expected = tc.B.Cmp(tc.A)
+			actual = bytes.Compare(EncodeQuantity(tc.B), EncodeQuantity(tc.A))
+			assert.Equal(t, expected, actual)
+		})
+	}
+}
diff --git a/internal/scheduleringester/instructions_test.go b/internal/scheduleringester/instructions_test.go
index de6e9adc..a0815080 100644
--- a/internal/scheduleringester/instructions_test.go
+++ b/internal/scheduleringester/instructions_test.go
@@ -10,11 +10,9 @@ import (
 	"k8s.io/apimachinery/pkg/api/resource"
 
 	"github.com/armadaproject/armada/internal/common/compress"
-	"github.com/armadaproject/armada/internal/common/hash"
 	"github.com/armadaproject/armada/internal/common/ingest/metrics"
 	f "github.com/armadaproject/armada/internal/common/ingest/testfixtures"
 	protoutil "github.com/armadaproject/armada/internal/common/proto"
-	"github.com/armadaproject/armada/internal/scheduler"
 	schedulerdb "github.com/armadaproject/armada/internal/scheduler/database"
 	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
 	"github.com/armadaproject/armada/pkg/armadaevents"
@@ -254,14 +252,6 @@ func assertErrorMessagesEqual(t *testing.T, expectedBytes []byte, actualBytes []
 	assert.Equal(t, expectedError, actualError)
 }
 
-func mustCalculatePodRequirementsHash(t *testing.T, schedulingInfo *schedulerobjects.JobSchedulingInfo) []byte {
-	podRequirements := scheduler.PodRequirementFromJobSchedulingInfo(schedulingInfo)
-	require.NotNil(t, podRequirements)
-	podRequirementsHash, err := hash.CalculatePodRequirementsHash(podRequirements)
-	require.NoError(t, err)
-	return podRequirementsHash
-}
-
 func getExpectedSubmitMessageSchedulingInfo(t *testing.T) *schedulerobjects.JobSchedulingInfo {
 	expectedSubmitSchedulingInfo := &schedulerobjects.JobSchedulingInfo{
 		Lifetime:        0,
@@ -292,8 +282,6 @@ func getExpectedSubmitMessageSchedulingInfo(t *testing.T) *schedulerobjects.JobS
 			},
 		},
 	}
-
-	expectedRequirementsHash := mustCalculatePodRequirementsHash(t, expectedSubmitSchedulingInfo)
-	expectedSubmitSchedulingInfo.PodRequirementsHash = expectedRequirementsHash
+	_, _ = expectedSubmitSchedulingInfo.SchedulingKey()
 	return expectedSubmitSchedulingInfo
 }