[Core] Hash LabelSelector's label values as the set operator== compares - #65740
[Core] Hash LabelSelector's label values as the set operator== compares#65740LuciferYang wants to merge 3 commits into
Conversation
AbslHashValue walked a constraint's values with an ordered H::combine while operator== compares them as a set, so two equal LabelSelectors could hash differently. Which slot a value takes in a flat_hash_set depends on the order the set was filled and on a salt absl derives from the address of the table's control bytes, so this is not hypothetical: with the old hash, building one label with the same 64 values 16 shuffled ways produced 16 distinct hashes. SchedulingClassDescriptor hashes a LabelSelector directly and again through FallbackOption, so a selector that hashes two ways registers under two scheduling class ids in sched_cls_to_id_, which is never pruned. The hash now sits on LabelConstraint, next to the operator== it has to agree with, and hands the values set to absl, which hashes its unordered containers order-independently. The two were added ten lines apart in ray-project#53578 and have disagreed ever since; putting them on one type is what makes the next reader check both. Fixes ray-project#65677 Signed-off-by: yangjie01 <yangjie01@baidu.com>
There was a problem hiding this comment.
Code Review
This pull request refactors the AbslHashValue implementation for LabelSelector and LabelConstraint to ensure that equal selectors hash equally regardless of the insertion order of their values. It also adds comprehensive unit tests to verify that hashing is order-independent and that differences in keys, operators, or values result in different hashes. There are no review comments, so I have no feedback to provide.
|
@ryanaoleary @rueian friendly ping — would either of you have time to take a look at this one? No urgency, and happy to adjust it if you'd rather it went a different way. |
|
@ryanaoleary Please take a look if you have time. |
|
Thank you @Kunchd |
|
|
||
| } // namespace | ||
|
|
||
| // A constraint holds its values in a flat_hash_set and operator== compares them as a |
There was a problem hiding this comment.
nit: could shorten some of these comments, I think they err on the verbose side currently
// LabelConstraint values form an unordered set, so their hash must be order-independent
// to match operator==. We shuffle inputs to force diverse internal memory layouts
// (caused by insertion order and internal salting) and verify hash stability.
There was a problem hiding this comment.
Took your wording in 85f926b.
One measurement worth passing on, since your text credits salting: I instrumented the 16 rounds and the per-table salt proxy took only 2 distinct values across them, because the allocator kept handing back the same address. So the layout diversity the sweep gets is almost entirely insertion order, with salting contributing close to nothing here. Your parenthetical is still true of absl in general, which is why I left it as you wrote it, but say the word and I will narrow it to insertion order.
I also dropped the older, longer version's mention of the salt coming from the address of the control bytes. That is specific to this absl version, so it would go stale on an upgrade.
| EXPECT_NE(absl::HashOf(RegionSelector({})), absl::HashOf(RegionSelector({"us-east"}))); | ||
| } | ||
|
|
||
| // The key and the operator need their own case: with only the cases above, dropping |
There was a problem hiding this comment.
nit: I think we could remove this note about the key and operator needing their own test case, if a comment is needed it could just describe the test:
// Verify that both the key and the operator are mixed into the hash.
There was a problem hiding this comment.
Done in 85f926b, with your line. I applied the same treatment to the test above it, which argued for its own existence in the same way, so the file does not end up half in each style.
|
|
||
| namespace { | ||
|
|
||
| LabelSelector OneConstraint(const std::string &key, |
There was a problem hiding this comment.
could use pass by value here and move to avoid deep copies every iteration in the test:
LabelSelector OneConstraint(std::string key,
LabelSelectorOperator op,
absl::flat_hash_set<std::string> values) {
LabelSelector selector;
selector.AddConstraint(LabelConstraint(std::move(key), op, std::move(values)));
return selector;
}
LabelSelector RegionSelector(absl::flat_hash_set<std::string> values) {
return OneConstraint("region", LabelSelectorOperator::LABEL_IN, std::move(values));
}
we'd also change the selector creation to this pattern:
const LabelSelector selector = RegionSelector(std::move(set));
There was a problem hiding this comment.
Done in 85f926b, and it turned out to be more than a copy saved. LabelConstraint already takes the set by value and moves it, so the old const & helper meant a copy landed on that parameter, and an absl set copy reinserts into a freshly sized table. The layouts the order sweep was visiting were therefore rehashed ones rather than the ones it built from each shuffle. Moving all the way through keeps the table the test made, which is what the sweep is about.
Re-verified after the change since it touches that mechanism: 10 tests green on 10 consecutive runs, and both failing directions still fail by exit code, with an order-dependent hash failing the sweep on 10 out of 10.
Left the four calls in SelectorsDifferingOnlyInKeyOrOperatorHashDifferently as they are. The copy count there is the same either way, since it used to happen on LabelConstraint's parameter and now happens at the call, and removing it would mean four separate sets or moving only on the last call.
|
I left some relatively minor comments on the test changes but the fix itself LGTM, thanks for making this change. Will approve once the comments are resolved |
…comments Taking the set by value and moving it also drops the rehash the copy caused, so the layouts the order sweep visits are the ones it built. Signed-off-by: yangjie01 <yangjie01@baidu.com>
Description
AbslHashValuewalked aLabelConstraint's values with an orderedH::combinewhileoperator==compares them as a set, so twoLabelSelectors that compare equal could hash differently. Which slot a value takes in aflat_hash_setdepends on the order the set was filled and on a salt absl derives from the address of the table's control bytes, so this is reachable rather than theoretical: with the old hash, building one label with the same 64 values 16 shuffled ways produced 16 distinct hashes.SchedulingClassDescriptorhashes aLabelSelectordirectly and again throughFallbackOption, andsched_cls_to_id_only ever getsfindand insert — no erase, no clear. A selector that hashes two ways therefore registers under two scheduling class ids, and the per-class queue and backlog metrics split with it.The hash now sits on
LabelConstraint, next to theoperator==it has to agree with, and hands the values set to absl, which hashes its unordered containers order-independently and mixes in the size. That also removes the hand-writtencombine_unorderedand the two explicitsize()terms, since absl's container hashes already carry them. The two were added ten lines apart in #53578 and have disagreed ever since; keeping them on one type is what makes the next reader check both.One consequence worth stating: if a future edit adds a field to
LabelConstraintand forgets the hash,H::combine(h, GetConstraints())fails to compile rather than silently falling back to a byte-wise hash.absl::hash_internal::is_uniquely_representedhas no trivially-copyable fallback, so there is no path that would quietly hash the set's heap pointer.Related issues
Fixes #65677
Not a duplicate: #65677 has no other linked PR, and no open PR modifies
src/ray/common/scheduling/label_selector.h(checked #64986 and #62041, the two open Core PRs that mention label selectors — neither touches the file).Additional information
Three tests, one per hashed field, each checked in the failing direction with one edit at a time:
EqualSelectorsHashEquallyWhateverTheValueOrderconstraint.GetLabelValues()SelectorsWithDifferentValuesHashDifferentlyconstraint.GetLabelKey()SelectorsDifferingOnlyInKeyOrOperatorHashDifferentlystatic_cast<int>(constraint.GetOperator())The order sweep builds one 64-value constraint 16 times with a fixed-seed shuffle and requires a single hash. It also asserts that more than one iteration order was actually observed: without that, the sweep would pass for an order-dependent hash whenever every round happened to lay the table out identically. An earlier revision used 4 values and swept reserved capacities instead, and missed the bug in 1 run out of 20 — reserving on the source set does nothing here, because
LabelConstrainttakes the set by value and absl normalizes the copy's capacity from its size.Commands, on macOS 15.5 / arm64 with Apple clang 21. The two extra flags work around that toolchain, not this change (a deprecated builtin in the pinned absl):
With the old hash restored, the order sweep failed on 20 runs out of 20; with this change all 10 tests passed on 20 runs out of 20.
pre-commit runpassesclang-formatandcpplinton both files.While here I swept every hash entry point under
src/for the same defect — an ordered combine over an unordered member whoseoperator==compares it as a container. There is no third instance:ResourceSetwas the other one and #64958 fixed it. Five hash functions are coarser than theiroperator==(rpc::AddressXORs four fields, soworker_idandnode_idswap to the same hash; likewisegcs/state_util.h,core_worker/common.h,scheduling_class_util.h's node-label branch, andFunctionDescriptor::Hash), but all of them err toward extra collisions, never toward equal objects hashing differently, so they are correct and out of scope here.Not addressed, and I think it deserves its own change:
LabelSelector::operator==comparesstd::vector<LabelConstraint>, so it is order-sensitive. The hash agrees with it, so there is no inconsistency, but two selectors that mean the same thing written with their keys in a different order still register as two scheduling classes. That is deterministic and bounded by how many orders users write, unlike the case fixed here. Filed as #65739.AI assistance
AI assistance was used for this change and for reviewing it. I have read every changed line and run the commands above locally.