Skip to content

Commit 2e012b2

Browse files
authored
fix: flow non-determinism with steiner_tree metric (#221)
Fixes a rare case where the deterministic_quality preset could produce non-deterministic results when using the steiner_tree metric
1 parent 455ff42 commit 2e012b2

4 files changed

Lines changed: 50 additions & 15 deletions

File tree

mt-kahypar/partition/refinement/flows/deterministic/deterministic_flow_refinement_scheduler.cpp

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -74,43 +74,78 @@ bool DeterministicFlowRefinementScheduler<GraphAndGainTypes>::refineImpl(mt_kahy
7474
HyperedgeWeight min_improvement = _context.refinement.flows.min_relative_improvement_per_round * current_metrics.quality;
7575
std::atomic<HyperedgeWeight> round_delta(std::numeric_limits<HyperedgeWeight>::max());
7676
tbb::concurrent_queue<BlockPair> scheduled_blocks;
77+
vec<std::pair<BlockPair, MoveSequence>> resulting_moves;
78+
std::atomic<size_t> result_id;
7779
for (size_t round = 0; round_delta >= min_improvement; ++round) {
7880
size_t num_scheduled_blocks = _schedule.getNextMatching(scheduled_blocks);
7981
if (num_scheduled_blocks == 0) break;
8082

8183
round_delta = 0;
8284
while (num_scheduled_blocks > 0) {
8385
DBG << "Next matching:" << num_scheduled_blocks << "blocks";
84-
_new_cut_hes.clear_sequential();
86+
resulting_moves.assign(num_scheduled_blocks, {});
87+
resulting_moves.resize(num_scheduled_blocks);
88+
result_id = 0;
8589

90+
// Important:
91+
// The next step runs the actual flow computations, but doesnt't apply the results yet.
92+
// Applying the moves immediately would cause a determinism bug with the steiner_tree
93+
// objective! This is because with the steiner_tree metric, the gain for moving pins of
94+
// a hyperedge is influenced by all blocks in the connectivity set (not only source and
95+
// target block). Therefore, applying moves constitutes a race condition with all threads
96+
// that currently construct a flow problem which includes a part of the same hyperedge -
97+
// which can happen even if we only schedule matchings. We solve this by applying the
98+
// moves afterwards in a separate step.
8699
tbb::parallel_for(UL(0), _context.refinement.flows.num_parallel_searches, [&](const size_t refiner_idx) {
87100
BlockPair blocks;
88101
while (scheduled_blocks.try_pop(blocks)) {
89102
timer.start_timer("region_growing", "Grow Region", true);
90103
const Subhypergraph sub_hg = _constructor.construct(blocks, _quotient_graph, phg, /*deterministic=*/true);
91104
timer.stop_timer("region_growing");
92105

93-
HyperedgeWeight improvement = 0;
106+
MoveSequence moves;
107+
size_t local_result_id = result_id.fetch_add(1, std::memory_order_relaxed);
94108
auto start = std::chrono::high_resolution_clock::now();
95109
if ( sub_hg.numNodes() > 0 ) {
96110
auto partitioned_hg = utils::partitioned_hg_const_cast(phg);
97111
if (_refiner[refiner_idx] == nullptr) {
98112
_refiner[refiner_idx] = constructFlowRefiner();
99113
}
100114
_refiner[refiner_idx]->initialize(partitioned_hg);
101-
MoveSequence moves = _refiner[refiner_idx]->refine(partitioned_hg, sub_hg, start);
115+
moves = _refiner[refiner_idx]->refine(partitioned_hg, sub_hg, start);
116+
}
117+
ASSERT(local_result_id < resulting_moves.size());
118+
resulting_moves[local_result_id] = {blocks, std::move(moves)};
119+
}
120+
});
102121

103-
timer.start_timer("apply_moves", "Apply Moves", true);
104-
improvement = applyMoves(blocks, moves, phg);
105-
timer.stop_timer("apply_moves");
122+
// apply the resulting moves
123+
timer.start_timer("apply_moves", "Apply Moves");
124+
auto apply_moves = [&](BlockPair blocks, const MoveSequence& moves) {
125+
ASSERT(blocks.i != kInvalidPartition && blocks.j != kInvalidPartition);
126+
HyperedgeWeight improvement = applyMoves(blocks, moves, phg);
106127

107-
_schedule.reportResults(blocks, improvement);
108-
}
128+
round_delta.fetch_add(improvement, std::memory_order_relaxed);
129+
_schedule.reportResults(blocks, improvement);
130+
_quotient_graph.edge(blocks).reportImprovement(improvement);
131+
};
109132

110-
round_delta.fetch_add(improvement, std::memory_order_relaxed);
111-
_quotient_graph.edge(blocks).reportImprovement(improvement);
133+
_new_cut_hes.clear_sequential();
134+
if constexpr (FlowNetworkConstruction::is_exact_model) {
135+
tbb::parallel_for(UL(0), num_scheduled_blocks, [&](const size_t i) {
136+
apply_moves(resulting_moves[i].first, resulting_moves[i].second);
137+
});
138+
} else {
139+
// in case of the steiner_tree metric, we need to apply the moves sequentially
140+
// to ensure the reported improvement for each quotient graph edge is deterministic
141+
std::sort(resulting_moves.begin(), resulting_moves.end(), [](auto& lhs, auto& rhs) {
142+
return std::tie(lhs.first.i, lhs.first.j) < std::tie(rhs.first.i, rhs.first.j);
143+
});
144+
for (const auto& pair: resulting_moves) {
145+
apply_moves(pair.first, pair.second);
112146
}
113-
});
147+
}
148+
timer.stop_timer("apply_moves");
114149

115150
// adding HEs to the quotient graph must happen after all moves are applied,
116151
// since the result depends on the current connectivity set of the HE
@@ -200,7 +235,7 @@ bool changeNodePart(PartitionedHypergraph& phg,
200235

201236
template<typename GraphAndGainTypes>
202237
HyperedgeWeight DeterministicFlowRefinementScheduler<GraphAndGainTypes>::applyMoves(const BlockPair& blocks,
203-
MoveSequence& sequence,
238+
const MoveSequence& sequence,
204239
PartitionedHypergraph& phg) {
205240
const bool gain_cache_update = _context.forceGainCacheUpdates();
206241
HyperedgeWeight improvement = 0;

mt-kahypar/partition/refinement/flows/deterministic/deterministic_flow_refinement_scheduler.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ class DeterministicFlowRefinementScheduler final : public IRefiner {
8484

8585
std::unique_ptr<IFlowRefiner> constructFlowRefiner();
8686

87-
HyperedgeWeight applyMoves(const BlockPair& blocks, MoveSequence& sequence, PartitionedHypergraph& phg);
87+
HyperedgeWeight applyMoves(const BlockPair& blocks, const MoveSequence& sequence, PartitionedHypergraph& phg);
8888

8989
void initializeImpl(mt_kahypar_partitioned_hypergraph_t& hypergraph);
9090

mt-kahypar/partition/refinement/flows/flow_common.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ enum class MoveSequenceState : uint8_t {
4646
// apply the moves
4747
struct MoveSequence {
4848
vec<Move> moves;
49-
Gain expected_improvement; // >= 0
49+
Gain expected_improvement = 0; // >= 0
5050
MoveSequenceState state = MoveSequenceState::IN_PROGRESS;
5151
};
5252

mt-kahypar/partition/refinement/gains/steiner_tree/steiner_tree_flow_network_construction.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ FlowNetworkEdgeParameters SteinerTreeFlowNetworkConstruction::getParameters(cons
121121
const HyperedgeWeight distance_without_block_1 = target_graph.distanceWithoutBlock(connectivity_set, block_1);
122122
const HyperedgeWeight gain_0 = (current_distance - distance_without_block_0) * edge_weight;
123123
const HyperedgeWeight gain_1 = (current_distance - distance_without_block_1) * edge_weight;
124-
parameters.capacity = capacity_for_cut_edge(context.refinement.flows.steiner_tree_policy, gain_0, gain_1);
124+
parameters.capacity = std::max(0, capacity_for_cut_edge(context.refinement.flows.steiner_tree_policy, gain_0, gain_1));
125125
}
126126
return parameters;
127127
}

0 commit comments

Comments
 (0)