Skip to content

Commit 3469ff4

Browse files
committed
Flows: large refactor of flow scheduling code
- disentangle quotient graph from scheduling - remove pointless "thread management" - eliminate search ids from flows to simplify code structure - remove flow refinement adapter (unnecessary indirection) - refactor flow scheduler code for cleanness & move more code to .cpp instead of header
1 parent 11500bb commit 3469ff4

22 files changed

Lines changed: 951 additions & 1659 deletions

mt-kahypar/partition/refinement/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ set(RefinementSources
1010
rebalancing/deterministic_rebalancer.cpp
1111
deterministic/deterministic_label_propagation.cpp
1212
deterministic/deterministic_jet_refiner.cpp
13-
flows/refiner_adapter.cpp
1413
flows/problem_construction.cpp
1514
flows/flow_refinement_scheduler.cpp
15+
flows/active_block_scheduler.cpp
1616
flows/quotient_graph.cpp
1717
flows/flow_refiner.cpp
1818
flows/sequential_construction.cpp
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
/*******************************************************************************
2+
* MIT License
3+
*
4+
* This file is part of Mt-KaHyPar.
5+
*
6+
* Copyright (C) 2025 Nikolai Maas <nikolai.maas@kit.edu>
7+
*
8+
* Permission is hereby granted, free of charge, to any person obtaining a copy
9+
* of this software and associated documentation files (the "Software"), to deal
10+
* in the Software without restriction, including without limitation the rights
11+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
* copies of the Software, and to permit persons to whom the Software is
13+
* furnished to do so, subject to the following conditions:
14+
*
15+
* The above copyright notice and this permission notice shall be included in all
16+
* copies or substantial portions of the Software.
17+
*
18+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24+
* SOFTWARE.
25+
******************************************************************************/
26+
27+
28+
#include "mt-kahypar/partition/refinement/flows/active_block_scheduler.h"
29+
30+
#include "mt-kahypar/definitions.h"
31+
32+
33+
namespace mt_kahypar {
34+
35+
ActiveBlockSchedulingRound::ActiveBlockSchedulingRound(const Context& context,
36+
QuotientGraph& quotient_graph) :
37+
_context(context),
38+
_quotient_graph(quotient_graph),
39+
_unscheduled_blocks(),
40+
_round_improvement(0),
41+
_active_blocks_lock(),
42+
_active_blocks(context.partition.k, false),
43+
_remaining_blocks(0) { }
44+
45+
bool ActiveBlockSchedulingRound::popBlockPairFromQueue(BlockPair& blocks) {
46+
blocks.i = kInvalidPartition;
47+
blocks.j = kInvalidPartition;
48+
if ( _unscheduled_blocks.try_pop(blocks) ) {
49+
_quotient_graph.edge(blocks).markAsNotInQueue();
50+
}
51+
return blocks.i != kInvalidPartition && blocks.j != kInvalidPartition;
52+
}
53+
54+
void ActiveBlockSchedulingRound::finalizeSearch(const BlockPair& blocks,
55+
const HyperedgeWeight improvement,
56+
bool& block_0_becomes_active,
57+
bool& block_1_becomes_active) {
58+
_round_improvement += improvement;
59+
--_remaining_blocks;
60+
if ( improvement > 0 ) {
61+
_active_blocks_lock.lock();
62+
block_0_becomes_active = !_active_blocks[blocks.i];
63+
block_1_becomes_active = !_active_blocks[blocks.j];
64+
_active_blocks[blocks.i] = true;
65+
_active_blocks[blocks.j] = true;
66+
_active_blocks_lock.unlock();
67+
}
68+
}
69+
70+
bool ActiveBlockSchedulingRound::pushBlockPairIntoQueue(const BlockPair& blocks) {
71+
QuotientGraphEdge& qg_edge = _quotient_graph.edge(blocks);
72+
if ( qg_edge.markAsInQueue() ) {
73+
_unscheduled_blocks.push(blocks);
74+
++_remaining_blocks;
75+
return true;
76+
} else {
77+
return false;
78+
}
79+
}
80+
81+
ActiveBlockScheduler::ActiveBlockScheduler(const Context& context,
82+
QuotientGraph& quotient_graph) :
83+
_context(context),
84+
_quotient_graph(quotient_graph),
85+
_num_rounds(0),
86+
_rounds(),
87+
_min_improvement_per_round(0),
88+
_terminate(false),
89+
_round_lock(),
90+
_first_active_round(0),
91+
_is_input_hypergraph(false) { }
92+
93+
void ActiveBlockScheduler::initialize(const bool is_input_hypergraph) {
94+
reset();
95+
_is_input_hypergraph = is_input_hypergraph;
96+
97+
HyperedgeWeight best_total_improvement = 1;
98+
for ( PartitionID i = 0; i < _context.partition.k; ++i ) {
99+
for ( PartitionID j = i + 1; j < _context.partition.k; ++j ) {
100+
const BlockPair blocks{ i, j };
101+
best_total_improvement = std::max(best_total_improvement,
102+
_quotient_graph.edge(blocks).total_improvement.load(std::memory_order_relaxed));
103+
}
104+
}
105+
106+
vec<BlockPair> active_block_pairs;
107+
for ( PartitionID i = 0; i < _context.partition.k; ++i ) {
108+
for ( PartitionID j = i + 1; j < _context.partition.k; ++j ) {
109+
const BlockPair blocks{ i, j };
110+
if ( isActiveBlockPair(blocks) ) {
111+
active_block_pairs.push_back(blocks);
112+
}
113+
}
114+
}
115+
116+
if ( active_block_pairs.size() > 0 ) {
117+
std::sort(active_block_pairs.begin(), active_block_pairs.end(),
118+
[&](const BlockPair& lhs, const BlockPair& rhs) {
119+
const QuotientGraphEdge& l_edge = _quotient_graph.edge(lhs);
120+
const QuotientGraphEdge& r_edge = _quotient_graph.edge(rhs);
121+
return l_edge.total_improvement > r_edge.total_improvement ||
122+
( l_edge.total_improvement == r_edge.total_improvement &&
123+
l_edge.cut_he_weight > r_edge.cut_he_weight );
124+
});
125+
_rounds.emplace_back(_context, _quotient_graph);
126+
++_num_rounds;
127+
for ( const BlockPair& blocks : active_block_pairs ) {
128+
DBG << "Schedule blocks (" << blocks.i << "," << blocks.j << ") in round 1 ("
129+
<< "Total Improvement =" << _quotient_graph.edge(blocks).total_improvement << ","
130+
<< "Cut Weight =" << _quotient_graph.edge(blocks).cut_he_weight << ")";
131+
_rounds.back().pushBlockPairIntoQueue(blocks);
132+
}
133+
}
134+
}
135+
136+
bool ActiveBlockScheduler::popBlockPairFromQueue(BlockPair& blocks, size_t& round) {
137+
bool success = false;
138+
round = _first_active_round;
139+
while ( !_terminate && round < _num_rounds ) {
140+
success = _rounds[round].popBlockPairFromQueue(blocks);
141+
if ( success ) {
142+
break;
143+
}
144+
++round;
145+
}
146+
147+
if ( success && round == _num_rounds - 1 ) {
148+
_round_lock.lock();
149+
if ( round == _num_rounds - 1 ) {
150+
// There must always be a next round available such that we can
151+
// reschedule block pairs that become active.
152+
_rounds.emplace_back(_context, _quotient_graph);
153+
++_num_rounds;
154+
}
155+
_round_lock.unlock();
156+
}
157+
158+
return success;
159+
}
160+
161+
void ActiveBlockScheduler::finalizeSearch(const BlockPair& blocks,
162+
const size_t round,
163+
const HyperedgeWeight improvement) {
164+
ASSERT(round < _rounds.size());
165+
bool block_0_becomes_active = false;
166+
bool block_1_becomes_active = false;
167+
_rounds[round].finalizeSearch(blocks, improvement,
168+
block_0_becomes_active, block_1_becomes_active);
169+
170+
if ( block_0_becomes_active ) {
171+
// If blocks.i becomes active, we push all adjacent blocks into the queue of the next round
172+
ASSERT(round + 1 < _rounds.size());
173+
for ( PartitionID other = 0; other < _context.partition.k; ++other ) {
174+
if ( blocks.i != other ) {
175+
const BlockPair new_blocks{ std::min(blocks.i, other), std::max(blocks.i, other) };
176+
if ( isActiveBlockPair(new_blocks) ) {
177+
DBG << "Schedule blocks (" << new_blocks.i << "," << new_blocks.j << ") in round" << (round + 2) << " ("
178+
<< "Total Improvement =" << _quotient_graph.edge(new_blocks).total_improvement << ","
179+
<< "Cut Weight =" << _quotient_graph.edge(new_blocks).cut_he_weight << ")";
180+
_rounds[round + 1].pushBlockPairIntoQueue(new_blocks);
181+
}
182+
}
183+
}
184+
}
185+
186+
if ( block_1_becomes_active ) {
187+
// If blocks.j becomes active, we push all adjacent blocks into the queue of the next round
188+
ASSERT(round + 1 < _rounds.size());
189+
for ( PartitionID other = 0; other < _context.partition.k; ++other ) {
190+
if ( blocks.j != other ) {
191+
const BlockPair new_blocks{ std::min(blocks.j, other), std::max(blocks.j, other) };
192+
if ( isActiveBlockPair(new_blocks) ) {
193+
DBG << "Schedule blocks (" << new_blocks.i << "," << new_blocks.j << ") in round" << (round + 2) << " ("
194+
<< "Total Improvement =" << _quotient_graph.edge(new_blocks).total_improvement << ","
195+
<< "Cut Weight =" << _quotient_graph.edge(new_blocks).cut_he_weight << ")";
196+
_rounds[round + 1].pushBlockPairIntoQueue(new_blocks);
197+
}
198+
}
199+
}
200+
}
201+
202+
// Special case
203+
if ( improvement > 0 && !_quotient_graph.edge(blocks).isInQueue() && isActiveBlockPair(blocks) &&
204+
( _rounds[round].isActive(blocks.i) || _rounds[round].isActive(blocks.j) ) ) {
205+
// The active block scheduling strategy works in multiple rounds and each contain a separate queue
206+
// to store active block pairs. A block pair is only allowed to be contained in one queue.
207+
// If a block becomes active, we schedule all quotient graph edges incident to the block in
208+
// the next round. However, there could be some edges that are already contained in a queue of
209+
// a previous round, which are then not scheduled in the next round. If this edge is scheduled and
210+
// leads to an improvement, we schedule it in the next round here.
211+
DBG << "Schedule blocks (" << blocks.i << "," << blocks.j << ") in round" << (round + 2) << " ("
212+
<< "Total Improvement =" << _quotient_graph.edge(blocks).total_improvement << ","
213+
<< "Cut Weight =" << _quotient_graph.edge(blocks).cut_he_weight << ")";
214+
_rounds[round + 1].pushBlockPairIntoQueue(blocks);
215+
}
216+
217+
if ( round == _first_active_round && _rounds[round].numRemainingBlocks() == 0 ) {
218+
_round_lock.lock();
219+
// We consider a round as finished, if the previous round is also finished and there
220+
// are no remaining blocks in the queue of that round.
221+
while ( _first_active_round < _rounds.size() &&
222+
_rounds[_first_active_round].numRemainingBlocks() == 0 ) {
223+
DBG << GREEN << "Round" << (_first_active_round + 1) << "terminates with improvement"
224+
<< _rounds[_first_active_round].roundImprovement() << "("
225+
<< "Minimum Required Improvement =" << _min_improvement_per_round << ")" << END;
226+
// We require that minimum improvement per round must be greater than a threshold,
227+
// otherwise we terminate early
228+
_terminate = _rounds[_first_active_round].roundImprovement() < _min_improvement_per_round;
229+
++_first_active_round;
230+
}
231+
_round_lock.unlock();
232+
}
233+
}
234+
235+
size_t ActiveBlockScheduler::numRemainingBlocks() const {
236+
size_t num_remaining_blocks = 0;
237+
for ( size_t i = _first_active_round; i < _num_rounds; ++i ) {
238+
num_remaining_blocks += _rounds[i].numRemainingBlocks();
239+
}
240+
return num_remaining_blocks;
241+
}
242+
243+
void ActiveBlockScheduler::setObjective(const HyperedgeWeight objective) {
244+
_min_improvement_per_round =
245+
_context.refinement.flows.min_relative_improvement_per_round * objective;
246+
}
247+
248+
void ActiveBlockScheduler::reset() {
249+
_num_rounds.store(0);
250+
_rounds.clear();
251+
_first_active_round = 0;
252+
_terminate = false;
253+
}
254+
255+
bool ActiveBlockScheduler::isActiveBlockPair(const BlockPair& blocks) const {
256+
const bool skip_small_cuts = !_is_input_hypergraph &&
257+
_context.refinement.flows.skip_small_cuts;
258+
const bool contains_enough_cut_hes =
259+
(skip_small_cuts && _quotient_graph.edge(blocks).cut_he_weight > 10) ||
260+
(!skip_small_cuts && _quotient_graph.edge(blocks).cut_he_weight > 0);
261+
const bool is_promising_blocks_pair =
262+
!_context.refinement.flows.skip_unpromising_blocks ||
263+
( _first_active_round == 0 || _quotient_graph.edge(blocks).num_improvements_found > 0 );
264+
return contains_enough_cut_hes && is_promising_blocks_pair;
265+
}
266+
267+
} // namespace mt_kahypar

0 commit comments

Comments
 (0)