@@ -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
201236template <typename GraphAndGainTypes>
202237HyperedgeWeight 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 ;
0 commit comments