Skip to content

Commit 422eec4

Browse files
authored
Fix seeding for multi-rule systems (#653)
## Changes * Resolves #642. * Fixes seeding in multi-rule `WolframModel`. ## Comments * Adding matches for different rules in parallel caused non-determinism, which broke consistency with specified seeds. * In the case of multiple rules, the matches are now added to intermediate storage (similar to `EventDeduplication::SameInputSetIsomorphicOutputs`), and then sorted before being added to the main storage. ## Examples ```wl In[] := Counts @ Table[ BlockRandom[ WolframModel[<|"PatternRules" -> {{{1, 2}} -> {}, {{2, 3}} -> {}}|>, {{1, 2}, {2, 3}}, <|"MaxEvents" -> 1|>, "EventOrderingFunction" -> "Random"]["FinalState"], RandomSeeding -> 123], 100] Out[] = <|{{2, 3}} -> 100|> ```
1 parent 28f28ee commit 422eec4

2 files changed

Lines changed: 68 additions & 28 deletions

File tree

libSetReplace/HypergraphMatcher.cpp

Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,9 @@ class HypergraphMatcher::Implementation {
164164
MatchPtr nextMatch_;
165165

166166
const EventDeduplication eventDeduplication_;
167-
// Newly created matches for which the advanced deduplication algorithm has not yet been run.
168-
// We sort them by sets they match to, and then by the chosen ordering function,
167+
// Newly created matches that have not yet been added to matchQueue_, allMatches_, etc.
168+
// This is needed either for event deduplication or to keep the order in which matches are added deterministic.
169+
// We sort them for event deduplication purposes by sets they match to, and then by the chosen ordering function,
169170
// so that each batch with identical inputs can be processed together, and it's obvious which copy should be retained.
170171
std::set<MatchPtr, MatchComparator> newMatches_;
171172

@@ -215,15 +216,18 @@ class HypergraphMatcher::Implementation {
215216
return getCurrentError() != None || abortRequested();
216217
};
217218

219+
MatchStorage matchStorage;
218220
{
219221
// Only create threads if there is more than one rule
220222
const auto threadAcquisitionToken =
221223
Parallelism::acquire(Parallelism::HardwareType::StdCpu, static_cast<int>(rules_.size()));
222224
const int& numThreadsToUse = threadAcquisitionToken->numThreads();
225+
matchStorage = numThreadsToUse == 0 && eventDeduplication_ == EventDeduplication::None ? MatchStorage::Main
226+
: MatchStorage::NewMatches;
223227

224228
auto addMatchesForRuleRange = [=](RuleID start) {
225229
for (RuleID i = start; i < static_cast<RuleID>(rules_.size()); i += numThreadsToUse) {
226-
addMatchesForRule(tokenIDs, i, shouldAbort);
230+
addMatchesForRule(tokenIDs, i, shouldAbort, matchStorage);
227231
}
228232
};
229233

@@ -239,7 +243,7 @@ class HypergraphMatcher::Implementation {
239243
} else {
240244
// Single-threaded path
241245
for (RuleID i = 0; i < static_cast<RuleID>(rules_.size()); ++i) {
242-
addMatchesForRule(tokenIDs, i, shouldAbort);
246+
addMatchesForRule(tokenIDs, i, shouldAbort, matchStorage);
243247
}
244248
}
245249
}
@@ -254,6 +258,9 @@ class HypergraphMatcher::Implementation {
254258
removeIdenticalMatches(abortRequested);
255259
}
256260

261+
if (matchStorage == MatchStorage::NewMatches) {
262+
insertNewMatches();
263+
}
257264
chooseNextMatch();
258265
}
259266

@@ -324,14 +331,17 @@ class HypergraphMatcher::Implementation {
324331
}
325332

326333
private:
334+
enum class MatchStorage { Main, NewMatches };
335+
327336
void addMatchesForRule(const std::vector<TokenID>& tokenIDs,
328337
const RuleID& ruleID,
329-
const std::function<bool()>& shouldAbort) {
338+
const std::function<bool()>& shouldAbort,
339+
const MatchStorage matchStorage) {
330340
const auto& ruleInputTokens = rules_[ruleID].inputs;
331341
for (size_t i = 0; i < ruleInputTokens.size(); ++i) {
332342
const Match emptyMatch{ruleID, std::vector<TokenID>(ruleInputTokens.size(), -1)};
333343
completeMatchesStartingWithInput(
334-
emptyMatch, ruleInputTokens, rules_[ruleID].eventSelectionFunction, i, tokenIDs, shouldAbort);
344+
emptyMatch, ruleInputTokens, rules_[ruleID].eventSelectionFunction, i, tokenIDs, shouldAbort, matchStorage);
335345
}
336346
}
337347

@@ -340,14 +350,20 @@ class HypergraphMatcher::Implementation {
340350
const EventSelectionFunction eventSelectionFunction,
341351
const size_t nextInputIdx,
342352
const std::vector<TokenID>& potentialTokenIDs,
343-
const std::function<bool()>& shouldAbort) {
353+
const std::function<bool()>& shouldAbort,
354+
const MatchStorage matchStorage) {
344355
for (const auto tokenID : potentialTokenIDs) {
345356
if (getCurrentError() != None) {
346357
return;
347358
}
348359
if (isTokenUnused(incompleteMatch, tokenID)) {
349-
attemptMatchTokenToInput(
350-
incompleteMatch, partiallyMatchedInputs, eventSelectionFunction, nextInputIdx, tokenID, shouldAbort);
360+
attemptMatchTokenToInput(incompleteMatch,
361+
partiallyMatchedInputs,
362+
eventSelectionFunction,
363+
nextInputIdx,
364+
tokenID,
365+
shouldAbort,
366+
matchStorage);
351367
}
352368
}
353369
}
@@ -373,7 +389,8 @@ class HypergraphMatcher::Implementation {
373389
const EventSelectionFunction eventSelectionFunction,
374390
const size_t nextInputIdx,
375391
const TokenID potentialTokenID,
376-
const std::function<bool()>& shouldAbort) {
392+
const std::function<bool()>& shouldAbort,
393+
const MatchStorage matchStorage) {
377394
// If WL wants to abort, abort
378395
if (shouldAbort()) {
379396
setCurrentErrorIfNone(Error::Aborted);
@@ -401,7 +418,12 @@ class HypergraphMatcher::Implementation {
401418
}
402419

403420
if (isMatchComplete(newMatch)) {
404-
insertMatch(newMatch);
421+
std::lock_guard<std::mutex> lock(matchMutex);
422+
if (matchStorage == MatchStorage::NewMatches) {
423+
newMatches_.insert(std::make_shared<Match>(newMatch));
424+
} else {
425+
insertMatch(std::make_shared<Match>(newMatch));
426+
}
405427
return;
406428
}
407429

@@ -411,7 +433,8 @@ class HypergraphMatcher::Implementation {
411433
eventSelectionFunction,
412434
nextInputIdxAndCandidateTokens.first,
413435
nextInputIdxAndCandidateTokens.second,
414-
shouldAbort);
436+
shouldAbort,
437+
matchStorage);
415438
}
416439

417440
bool isSpacelikeSeparated(const TokenID newToken, const std::vector<TokenID>& previousTokens) {
@@ -426,12 +449,20 @@ class HypergraphMatcher::Implementation {
426449
return true;
427450
}
428451

429-
void insertMatch(const Match& newMatch) {
430-
// careful, don't create different pointers to the same match!
431-
const auto matchPtr = std::make_shared<Match>(newMatch);
432-
433-
std::lock_guard<std::mutex> lock(matchMutex);
452+
void insertNewMatches() {
453+
std::vector<MatchPtr> sortedMatches(std::make_move_iterator(newMatches_.begin()),
454+
std::make_move_iterator(newMatches_.end()));
455+
newMatches_.clear();
456+
// We should sort them in the same order they would be added if the evaluation was sequential.
457+
std::sort(sortedMatches.begin(), sortedMatches.end(), [](const MatchPtr& first, const MatchPtr& second) {
458+
return first->rule < second->rule;
459+
});
460+
for (const auto& match : sortedMatches) {
461+
insertMatch(match);
462+
}
463+
}
434464

465+
void insertMatch(const MatchPtr matchPtr) {
435466
if (!allMatches_.insert(matchPtr).second) {
436467
return;
437468
}
@@ -447,10 +478,6 @@ class HypergraphMatcher::Implementation {
447478
tokensToMatches_[token].insert(matchPtr);
448479
}
449480
}
450-
451-
if (eventDeduplication_ == EventDeduplication::SameInputSetIsomorphicOutputs) {
452-
newMatches_.insert(matchPtr);
453-
}
454481
}
455482

456483
static bool isMatchComplete(const Match& match) {
@@ -552,30 +579,31 @@ class HypergraphMatcher::Implementation {
552579
void removeIdenticalMatches(const std::function<bool()>& abortRequested) {
553580
std::unordered_set<TokenID> currentInputsSet;
554581
std::vector<MatchPtr> addedSameInputMatches;
555-
for (const auto& newMatch : newMatches_) {
556-
if (!sameInputSet(newMatch, currentInputsSet)) {
582+
for (auto newMatchIt = newMatches_.begin(); newMatchIt != newMatches_.end();) {
583+
if (!sameInputSet(*newMatchIt, currentInputsSet)) {
557584
// matches are ordered by their input sets, so if it's different, a batch with the new inputs is starting.
558585
currentInputsSet.clear();
559-
currentInputsSet.insert(newMatch->inputTokens.begin(), newMatch->inputTokens.end());
586+
currentInputsSet.insert((*newMatchIt)->inputTokens.begin(), (*newMatchIt)->inputTokens.end());
560587
addedSameInputMatches.clear();
561588
}
562589

563590
bool matchAppearedBefore = false;
564591
for (const auto& addedMatch : addedSameInputMatches) {
565-
if (sameOutcomeAssumingSameInputs(newMatch, addedMatch, abortRequested)) {
592+
if (sameOutcomeAssumingSameInputs(*newMatchIt, addedMatch, abortRequested)) {
566593
matchAppearedBefore = true;
594+
break;
567595
}
568596
if (getCurrentError() != None) {
569597
return;
570598
}
571599
}
572600
if (matchAppearedBefore) {
573-
deleteMatch(newMatch);
601+
newMatchIt = newMatches_.erase(newMatchIt);
574602
} else { // same input set, but a different outcome
575-
addedSameInputMatches.emplace_back(newMatch);
603+
addedSameInputMatches.emplace_back(*newMatchIt);
604+
++newMatchIt;
576605
}
577606
}
578-
newMatches_.clear();
579607
}
580608

581609
// Checks if the input token IDs in the match are the same as referenceInputTokens

libSetReplace/test/HypergraphSubstitutionSystem_test.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,4 +149,16 @@ TEST(HypergraphSubstitutionSystem, replaceOnce) {
149149
{{{{-1}}, {{-1, -1}}}}, {{1}}, 1, orderingSpec, HypergraphMatcher::EventDeduplication::None, 0);
150150
EXPECT_EQ(system.replaceOnce(doNotAbort), 1);
151151
}
152+
153+
TEST(HypergraphSubstitutionSystem, multiruleSeeding) {
154+
std::array<int, 2> replacedTokenCounts = {0, 0};
155+
constexpr int trialCount = 100;
156+
for (int i = 0; i < trialCount; ++i) {
157+
HypergraphSubstitutionSystem system(
158+
{{{{1, 2}}, {}}, {{{2, 3}}, {}}}, {{1, 2}, {2, 3}}, 1, {}, HypergraphMatcher::EventDeduplication::None, 123);
159+
system.replaceOnce(doNotAbort);
160+
++replacedTokenCounts[system.events()[1].inputTokens[0]];
161+
}
162+
EXPECT_EQ(std::max(replacedTokenCounts[0], replacedTokenCounts[1]), trialCount);
163+
}
152164
} // namespace SetReplace

0 commit comments

Comments
 (0)