|
| 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