Skip to content

Commit 5f7b212

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 d13c210 commit 5f7b212

22 files changed

Lines changed: 946 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: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
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+
if ( improvement > 0 ) {
60+
_active_blocks_lock.lock();
61+
block_0_becomes_active = !_active_blocks[blocks.i];
62+
block_1_becomes_active = !_active_blocks[blocks.j];
63+
_active_blocks[blocks.i] = true;
64+
_active_blocks[blocks.j] = true;
65+
_active_blocks_lock.unlock();
66+
}
67+
}
68+
69+
bool ActiveBlockSchedulingRound::pushBlockPairIntoQueue(const BlockPair& blocks) {
70+
QuotientGraphEdge& qg_edge = _quotient_graph.edge(blocks);
71+
if ( qg_edge.markAsInQueue() ) {
72+
_unscheduled_blocks.push(blocks);
73+
++_remaining_blocks;
74+
return true;
75+
} else {
76+
return false;
77+
}
78+
}
79+
80+
ActiveBlockScheduler::ActiveBlockScheduler(const Context& context,
81+
QuotientGraph& quotient_graph) :
82+
_context(context),
83+
_quotient_graph(quotient_graph),
84+
_num_rounds(0),
85+
_rounds(),
86+
_min_improvement_per_round(0),
87+
_terminate(false),
88+
_round_lock(),
89+
_first_active_round(0),
90+
_is_input_hypergraph(false) { }
91+
92+
void ActiveBlockScheduler::initialize(const bool is_input_hypergraph) {
93+
reset();
94+
_is_input_hypergraph = is_input_hypergraph;
95+
96+
HyperedgeWeight best_total_improvement = 1;
97+
for ( PartitionID i = 0; i < _context.partition.k; ++i ) {
98+
for ( PartitionID j = i + 1; j < _context.partition.k; ++j ) {
99+
const BlockPair blocks{ i, j };
100+
best_total_improvement = std::max(best_total_improvement,
101+
_quotient_graph.edge(blocks).total_improvement.load(std::memory_order_relaxed));
102+
}
103+
}
104+
105+
vec<BlockPair> active_block_pairs;
106+
for ( PartitionID i = 0; i < _context.partition.k; ++i ) {
107+
for ( PartitionID j = i + 1; j < _context.partition.k; ++j ) {
108+
const BlockPair blocks{ i, j };
109+
if ( isActiveBlockPair(blocks) ) {
110+
active_block_pairs.push_back(blocks);
111+
}
112+
}
113+
}
114+
115+
if ( active_block_pairs.size() > 0 ) {
116+
std::sort(active_block_pairs.begin(), active_block_pairs.end(),
117+
[&](const BlockPair& lhs, const BlockPair& rhs) {
118+
const QuotientGraphEdge& l_edge = _quotient_graph.edge(lhs);
119+
const QuotientGraphEdge& r_edge = _quotient_graph.edge(rhs);
120+
return l_edge.total_improvement > r_edge.total_improvement ||
121+
( l_edge.total_improvement == r_edge.total_improvement &&
122+
l_edge.cut_he_weight > r_edge.cut_he_weight );
123+
});
124+
_rounds.emplace_back(_context, _quotient_graph);
125+
++_num_rounds;
126+
for ( const BlockPair& blocks : active_block_pairs ) {
127+
DBG << "Schedule blocks (" << blocks.i << "," << blocks.j << ") in round 1 ("
128+
<< "Total Improvement =" << _quotient_graph.edge(blocks).total_improvement << ","
129+
<< "Cut Weight =" << _quotient_graph.edge(blocks).cut_he_weight << ")";
130+
_rounds.back().pushBlockPairIntoQueue(blocks);
131+
}
132+
}
133+
}
134+
135+
bool ActiveBlockScheduler::popBlockPairFromQueue(BlockPair& blocks, size_t& round) {
136+
bool success = false;
137+
round = _first_active_round;
138+
while ( !_terminate && round < _num_rounds ) {
139+
success = _rounds[round].popBlockPairFromQueue(blocks);
140+
if ( success ) {
141+
break;
142+
}
143+
++round;
144+
}
145+
146+
if ( success && round == _num_rounds - 1 ) {
147+
_round_lock.lock();
148+
if ( round == _num_rounds - 1 ) {
149+
// There must always be a next round available such that we can
150+
// reschedule block pairs that become active.
151+
_rounds.emplace_back(_context, _quotient_graph);
152+
++_num_rounds;
153+
}
154+
_round_lock.unlock();
155+
}
156+
157+
return success;
158+
}
159+
160+
void ActiveBlockScheduler::finalizeSearch(const BlockPair& blocks,
161+
const size_t round,
162+
const HyperedgeWeight improvement) {
163+
ASSERT(round < _rounds.size());
164+
bool block_0_becomes_active = false;
165+
bool block_1_becomes_active = false;
166+
_rounds[round].finalizeSearch(blocks, improvement,
167+
block_0_becomes_active, block_1_becomes_active);
168+
169+
auto schedule_adjacent_blocks = [&](PartitionID block_id) {
170+
ASSERT(round + 1 < _rounds.size());
171+
for ( PartitionID other = 0; other < _context.partition.k; ++other ) {
172+
if ( block_id != other ) {
173+
const BlockPair new_blocks{ std::min(block_id, other), std::max(block_id, other) };
174+
if ( isActiveBlockPair(new_blocks) ) {
175+
DBG << "Schedule blocks (" << new_blocks.i << "," << new_blocks.j << ") in round" << (round + 2) << " ("
176+
<< "Total Improvement =" << _quotient_graph.edge(new_blocks).total_improvement << ","
177+
<< "Cut Weight =" << _quotient_graph.edge(new_blocks).cut_he_weight << ")";
178+
_rounds[round + 1].pushBlockPairIntoQueue(new_blocks);
179+
}
180+
}
181+
}
182+
};
183+
184+
// If one of the blocks becomes active, we push all adjacent blocks into the queue of the next round
185+
if ( block_0_becomes_active ) {
186+
schedule_adjacent_blocks(blocks.i);
187+
}
188+
if ( block_1_becomes_active ) {
189+
schedule_adjacent_blocks(blocks.j);
190+
}
191+
192+
// Special case
193+
if ( improvement > 0 && !_quotient_graph.edge(blocks).isInQueue() && isActiveBlockPair(blocks) &&
194+
( _rounds[round].isActive(blocks.i) || _rounds[round].isActive(blocks.j) ) ) {
195+
// The active block scheduling strategy works in multiple rounds and each contain a separate queue
196+
// to store active block pairs. A block pair is only allowed to be contained in one queue.
197+
// If a block becomes active, we schedule all quotient graph edges incident to the block in
198+
// the next round. However, there could be some edges that are already contained in a queue of
199+
// a previous round, which are then not scheduled in the next round. If this edge is scheduled and
200+
// leads to an improvement, we schedule it in the next round here.
201+
DBG << "Schedule blocks (" << blocks.i << "," << blocks.j << ") in round" << (round + 2) << " ("
202+
<< "Total Improvement =" << _quotient_graph.edge(blocks).total_improvement << ","
203+
<< "Cut Weight =" << _quotient_graph.edge(blocks).cut_he_weight << ")";
204+
_rounds[round + 1].pushBlockPairIntoQueue(blocks);
205+
}
206+
207+
// Note: decrementing the block count must happen after pushing new blocks,
208+
// otherwise the active block count is temporarily too small
209+
_rounds[round].decrementRemainingBlocks();
210+
if ( round == _first_active_round && _rounds[round].numRemainingBlocks() == 0 ) {
211+
_round_lock.lock();
212+
// We consider a round as finished, if the previous round is also finished and there
213+
// are no remaining blocks in the queue of that round.
214+
while ( _first_active_round < _rounds.size() &&
215+
_rounds[_first_active_round].numRemainingBlocks() == 0 ) {
216+
DBG << GREEN << "Round" << (_first_active_round + 1) << "terminates with improvement"
217+
<< _rounds[_first_active_round].roundImprovement() << "("
218+
<< "Minimum Required Improvement =" << _min_improvement_per_round << ")" << END;
219+
// We require that minimum improvement per round must be greater than a threshold,
220+
// otherwise we terminate early
221+
_terminate = _rounds[_first_active_round].roundImprovement() < _min_improvement_per_round;
222+
++_first_active_round;
223+
}
224+
_round_lock.unlock();
225+
}
226+
}
227+
228+
size_t ActiveBlockScheduler::numRemainingBlocks() const {
229+
size_t num_remaining_blocks = 0;
230+
for ( size_t i = _first_active_round; i < _num_rounds; ++i ) {
231+
num_remaining_blocks += _rounds[i].numRemainingBlocks();
232+
}
233+
return num_remaining_blocks;
234+
}
235+
236+
void ActiveBlockScheduler::setObjective(const HyperedgeWeight objective) {
237+
_min_improvement_per_round =
238+
_context.refinement.flows.min_relative_improvement_per_round * objective;
239+
}
240+
241+
void ActiveBlockScheduler::reset() {
242+
_num_rounds.store(0);
243+
_rounds.clear();
244+
_first_active_round = 0;
245+
_terminate = false;
246+
}
247+
248+
bool ActiveBlockScheduler::isActiveBlockPair(const BlockPair& blocks) const {
249+
const bool skip_small_cuts = !_is_input_hypergraph &&
250+
_context.refinement.flows.skip_small_cuts;
251+
const int cut_he_threshold = skip_small_cuts ? 10 : 0;
252+
253+
const bool contains_enough_cut_hes = _quotient_graph.edge(blocks).cut_he_weight > cut_he_threshold;
254+
const bool is_promising_blocks_pair =
255+
!_context.refinement.flows.skip_unpromising_blocks ||
256+
( _first_active_round == 0 || _quotient_graph.edge(blocks).num_improvements_found > 0 );
257+
return contains_enough_cut_hes && is_promising_blocks_pair;
258+
}
259+
260+
} // namespace mt_kahypar

0 commit comments

Comments
 (0)