From a972b3b570be9b0e428ee5465a1ba9e3d2c88ee8 Mon Sep 17 00:00:00 2001 From: Nikolai Maas Date: Fri, 12 May 2023 13:40:57 +0200 Subject: [PATCH 001/152] refactor gain computation for use by jet refiner --- .../gains/cut/cut_gain_computation.h | 3 +- .../refinement/gains/gain_computation_base.h | 31 ++++++++++++++++--- .../gains/km1/km1_gain_computation.h | 3 +- .../gains/soed/soed_gain_computation.h | 3 +- .../steiner_tree_gain_computation.h | 3 +- ...steiner_tree_gain_computation_for_graphs.h | 3 +- 6 files changed, 36 insertions(+), 10 deletions(-) diff --git a/mt-kahypar/partition/refinement/gains/cut/cut_gain_computation.h b/mt-kahypar/partition/refinement/gains/cut/cut_gain_computation.h index 7af5970d0..bfe6e9b7e 100644 --- a/mt-kahypar/partition/refinement/gains/cut/cut_gain_computation.h +++ b/mt-kahypar/partition/refinement/gains/cut/cut_gain_computation.h @@ -39,11 +39,12 @@ namespace mt_kahypar { class CutGainComputation : public GainComputationBase { using Base = GainComputationBase; - using RatingMap = typename Base::RatingMap; static constexpr bool enable_heavy_assert = false; public: + using RatingMap = typename Base::RatingMap; + CutGainComputation(const Context& context, bool disable_randomization = false) : Base(context, disable_randomization) { } diff --git a/mt-kahypar/partition/refinement/gains/gain_computation_base.h b/mt-kahypar/partition/refinement/gains/gain_computation_base.h index 87d234e11..27215c1bf 100644 --- a/mt-kahypar/partition/refinement/gains/gain_computation_base.h +++ b/mt-kahypar/partition/refinement/gains/gain_computation_base.h @@ -62,11 +62,29 @@ class GainComputationBase { Move computeMaxGainMove(const PartitionedHypergraph& phg, const HypernodeID hn, const bool rebalance = false, - const bool consider_non_adjacent_blocks = false) { + const bool consider_non_adjacent_blocks = false, + const bool allow_imbalance = false) { Derived* derived = static_cast(this); RatingMap& tmp_scores = _tmp_scores.local(); Gain& isolated_block_gain = _isolated_block_gain.local(); derived->precomputeGains(phg, hn, tmp_scores, isolated_block_gain, consider_non_adjacent_blocks); + Move best_move = computeMaxGainMoveForScores(phg, tmp_scores, isolated_block_gain, hn, + rebalance, consider_non_adjacent_blocks, allow_imbalance); + + isolated_block_gain = 0; + tmp_scores.clear(); + return best_move; + } + + template + Move computeMaxGainMoveForScores(const PartitionedHypergraph& phg, + const RatingMap& tmp_scores, + const Gain isolated_block_gain, + const HypernodeID hn, + const bool rebalance = false, + const bool consider_non_adjacent_blocks = false, + const bool allow_imbalance = false) { + Derived* derived = static_cast(this); PartitionID from = phg.partID(hn); Move best_move { from, from, hn, rebalance ? std::numeric_limits::max() : 0 }; @@ -80,8 +98,8 @@ class GainComputationBase { (score == best_move.gain && !_disable_randomization && (no_tie_breaking || rand.flipCoin(cpu_id))); - if (new_best_gain && phg.partWeight(to) + hn_weight <= - _context.partition.max_part_weights[to]) { + if (new_best_gain && (allow_imbalance || phg.partWeight(to) + hn_weight <= + _context.partition.max_part_weights[to])) { best_move.to = to; best_move.gain = score; return true; @@ -119,8 +137,6 @@ class GainComputationBase { } } - isolated_block_gain = 0; - tmp_scores.clear(); return best_move; } @@ -128,6 +144,11 @@ class GainComputationBase { _deltas.local() += AttributedGains::gain(sync_update); } + // ! Returns the local rating map for block scores + RatingMap& localScores() { + return _tmp_scores.local(); + } + // ! Returns the delta in the objective function for all moves // ! performed by the calling thread relative to the last call // ! reset() diff --git a/mt-kahypar/partition/refinement/gains/km1/km1_gain_computation.h b/mt-kahypar/partition/refinement/gains/km1/km1_gain_computation.h index 6146eb053..7567cfa6f 100644 --- a/mt-kahypar/partition/refinement/gains/km1/km1_gain_computation.h +++ b/mt-kahypar/partition/refinement/gains/km1/km1_gain_computation.h @@ -37,11 +37,12 @@ namespace mt_kahypar { class Km1GainComputation : public GainComputationBase { using Base = GainComputationBase; - using RatingMap = typename Base::RatingMap; static constexpr bool enable_heavy_assert = false; public: + using RatingMap = typename Base::RatingMap; + Km1GainComputation(const Context& context, bool disable_randomization = false) : Base(context, disable_randomization) { } diff --git a/mt-kahypar/partition/refinement/gains/soed/soed_gain_computation.h b/mt-kahypar/partition/refinement/gains/soed/soed_gain_computation.h index 27683723e..c1a2481d7 100644 --- a/mt-kahypar/partition/refinement/gains/soed/soed_gain_computation.h +++ b/mt-kahypar/partition/refinement/gains/soed/soed_gain_computation.h @@ -37,11 +37,12 @@ namespace mt_kahypar { class SoedGainComputation : public GainComputationBase { using Base = GainComputationBase; - using RatingMap = typename Base::RatingMap; static constexpr bool enable_heavy_assert = false; public: + using RatingMap = typename Base::RatingMap; + SoedGainComputation(const Context& context, bool disable_randomization = false) : Base(context, disable_randomization) { } diff --git a/mt-kahypar/partition/refinement/gains/steiner_tree/steiner_tree_gain_computation.h b/mt-kahypar/partition/refinement/gains/steiner_tree/steiner_tree_gain_computation.h index 742cc0892..dca600d48 100644 --- a/mt-kahypar/partition/refinement/gains/steiner_tree/steiner_tree_gain_computation.h +++ b/mt-kahypar/partition/refinement/gains/steiner_tree/steiner_tree_gain_computation.h @@ -41,12 +41,13 @@ namespace mt_kahypar { class SteinerTreeGainComputation : public GainComputationBase { using Base = GainComputationBase; - using RatingMap = typename Base::RatingMap; static constexpr bool enable_heavy_assert = false; static constexpr size_t BITS_PER_BLOCK = ds::StaticBitset::BITS_PER_BLOCK; public: + using RatingMap = typename Base::RatingMap; + SteinerTreeGainComputation(const Context& context, bool disable_randomization = false) : Base(context, disable_randomization), diff --git a/mt-kahypar/partition/refinement/gains/steiner_tree_for_graphs/steiner_tree_gain_computation_for_graphs.h b/mt-kahypar/partition/refinement/gains/steiner_tree_for_graphs/steiner_tree_gain_computation_for_graphs.h index 0d5700ec6..b82b735d3 100644 --- a/mt-kahypar/partition/refinement/gains/steiner_tree_for_graphs/steiner_tree_gain_computation_for_graphs.h +++ b/mt-kahypar/partition/refinement/gains/steiner_tree_for_graphs/steiner_tree_gain_computation_for_graphs.h @@ -41,12 +41,13 @@ namespace mt_kahypar { class GraphSteinerTreeGainComputation : public GainComputationBase { using Base = GainComputationBase; - using RatingMap = typename Base::RatingMap; static constexpr bool enable_heavy_assert = false; static constexpr size_t BITS_PER_BLOCK = ds::StaticBitset::BITS_PER_BLOCK; public: + using RatingMap = typename Base::RatingMap; + GraphSteinerTreeGainComputation(const Context& context, bool disable_randomization = false) : Base(context, disable_randomization), From 856da3192e90abc6496faba2cf92ce9643931d16 Mon Sep 17 00:00:00 2001 From: Nikolai Maas Date: Thu, 11 May 2023 09:54:29 +0200 Subject: [PATCH 002/152] jet context and implementation first phase --- mt-kahypar/partition/context.cpp | 15 + mt-kahypar/partition/context.h | 13 + mt-kahypar/partition/context_enum_classes.cpp | 22 ++ mt-kahypar/partition/context_enum_classes.h | 10 + .../partition/refinement/CMakeLists.txt | 1 + .../partition/refinement/jet/jet_refiner.cpp | 279 ++++++++++++++++++ .../partition/refinement/jet/jet_refiner.h | 171 +++++++++++ tests/partition/refinement/CMakeLists.txt | 1 + .../partition/refinement/jet_refiner_test.cc | 244 +++++++++++++++ 9 files changed, 756 insertions(+) create mode 100644 mt-kahypar/partition/refinement/jet/jet_refiner.cpp create mode 100644 mt-kahypar/partition/refinement/jet/jet_refiner.h create mode 100644 tests/partition/refinement/jet_refiner_test.cc diff --git a/mt-kahypar/partition/context.cpp b/mt-kahypar/partition/context.cpp index 5db1d965e..9a919f9ab 100644 --- a/mt-kahypar/partition/context.cpp +++ b/mt-kahypar/partition/context.cpp @@ -129,6 +129,18 @@ namespace mt_kahypar { return str; } + std::ostream & operator<< (std::ostream& str, const JetParameters& params) { + str << " Jet Parameters:" << std::endl; + str << " Algorithm: " << params.algorithm << std::endl; + if ( params.algorithm != JetAlgorithm::do_nothing ) { + // str << " Maximum Iterations: " << params.maximum_iterations << std::endl; + str << " Restrict to Border Nodes: " << std::boolalpha << params.restrict_to_border_nodes << std::endl; + str << " Negative Gain Factor (Coarse): " << params.negative_gain_factor_coarse << std::endl; + str << " Negative Gain Factor (Fine): " << params.negative_gain_factor_fine << std::endl; + } + return str; + } + std::ostream& operator<<(std::ostream& out, const FMParameters& params) { out << " FM Parameters: \n"; out << " Algorithm: " << params.algorithm << std::endl; @@ -196,6 +208,7 @@ namespace mt_kahypar { str << " Maximum Batch Size: " << params.max_batch_size << std::endl; str << " Min Border Vertices Per Thread: " << params.min_border_vertices_per_thread << std::endl; str << "\n" << params.label_propagation; + str << "\n" << params.jet; str << "\n" << params.fm; if ( params.global_fm.use_global_fm ) { str << "\n" << params.global_fm; @@ -522,6 +535,8 @@ namespace mt_kahypar { initial_partitioning.refinement.label_propagation.rebalancing = true; initial_partitioning.refinement.label_propagation.hyperedge_size_activation_threshold = 100; + // TODO(maas): jet?? + // initial partitioning -> refinement -> fm initial_partitioning.refinement.fm.algorithm = FMAlgorithm::kway_fm; initial_partitioning.refinement.fm.multitry_rounds = 5; diff --git a/mt-kahypar/partition/context.h b/mt-kahypar/partition/context.h index 45712bcab..4ad123737 100644 --- a/mt-kahypar/partition/context.h +++ b/mt-kahypar/partition/context.h @@ -140,6 +140,18 @@ struct LabelPropagationParameters { std::ostream & operator<< (std::ostream& str, const LabelPropagationParameters& params); +struct JetParameters { + JetAlgorithm algorithm = JetAlgorithm::do_nothing; + // size_t maximum_iterations = 10; + bool execute_sequential = false; + bool restrict_to_border_nodes = true; + bool vertex_locking = true; + double negative_gain_factor_coarse = 0.25; + double negative_gain_factor_fine = 0.75; +}; + +std::ostream & operator<< (std::ostream& str, const JetParameters& params); + struct FMParameters { FMAlgorithm algorithm = FMAlgorithm::do_nothing; @@ -198,6 +210,7 @@ std::ostream& operator<<(std::ostream& out, const DeterministicRefinementParamet struct RefinementParameters { LabelPropagationParameters label_propagation; + JetParameters jet; FMParameters fm; DeterministicRefinementParameters deterministic_refinement; NLevelGlobalFMParameters global_fm; diff --git a/mt-kahypar/partition/context_enum_classes.cpp b/mt-kahypar/partition/context_enum_classes.cpp index cdaac1154..c0403b313 100644 --- a/mt-kahypar/partition/context_enum_classes.cpp +++ b/mt-kahypar/partition/context_enum_classes.cpp @@ -226,6 +226,16 @@ namespace mt_kahypar { return os << static_cast(algo); } + std::ostream & operator<< (std::ostream& os, const JetAlgorithm& algo) { + switch (algo) { + case JetAlgorithm::precomputed_ordered : return os << "precomputed_ordered"; + case JetAlgorithm::greedy_unordered: return os << "greedy_unordered"; + case JetAlgorithm::do_nothing: return os << "jet_do_nothing"; + // omit default case to trigger compiler warning for missing cases + } + return os << static_cast(algo); + } + std::ostream & operator<< (std::ostream& os, const FMAlgorithm& algo) { switch (algo) { case FMAlgorithm::kway_fm: return os << "kway_fm"; @@ -442,6 +452,18 @@ namespace mt_kahypar { return LabelPropagationAlgorithm::do_nothing; } + JetAlgorithm jetAlgorithmFromString(const std::string& type) { + if (type == "precomputed_ordered") { + return JetAlgorithm::precomputed_ordered; + } else if (type == "greedy_unordered") { + return JetAlgorithm::greedy_unordered; + } else if (type == "do_nothing") { + return JetAlgorithm::do_nothing; + } + ERR("Illegal option: " + type); + return JetAlgorithm::do_nothing; + } + FMAlgorithm fmAlgorithmFromString(const std::string& type) { if (type == "kway_fm") { return FMAlgorithm::kway_fm; diff --git a/mt-kahypar/partition/context_enum_classes.h b/mt-kahypar/partition/context_enum_classes.h index 3d7fa7be7..b8b033a50 100644 --- a/mt-kahypar/partition/context_enum_classes.h +++ b/mt-kahypar/partition/context_enum_classes.h @@ -152,6 +152,12 @@ enum class LabelPropagationAlgorithm : uint8_t { do_nothing }; +enum class JetAlgorithm : uint8_t { + precomputed_ordered, + greedy_unordered, + do_nothing +}; + enum class FMAlgorithm : uint8_t { kway_fm, do_nothing @@ -213,6 +219,8 @@ std::ostream & operator<< (std::ostream& os, const InitialPartitioningAlgorithm& std::ostream & operator<< (std::ostream& os, const LabelPropagationAlgorithm& algo); +std::ostream & operator<< (std::ostream& os, const JetAlgorithm& algo); + std::ostream & operator<< (std::ostream& os, const FMAlgorithm& algo); std::ostream & operator<< (std::ostream& os, const FlowAlgorithm& algo); @@ -247,6 +255,8 @@ InitialPartitioningAlgorithm initialPartitioningAlgorithmFromString(const std::s LabelPropagationAlgorithm labelPropagationAlgorithmFromString(const std::string& type); +JetAlgorithm jetAlgorithmFromString(const std::string& type); + FMAlgorithm fmAlgorithmFromString(const std::string& type); FlowAlgorithm flowAlgorithmFromString(const std::string& type); diff --git a/mt-kahypar/partition/refinement/CMakeLists.txt b/mt-kahypar/partition/refinement/CMakeLists.txt index 08564bc97..2643931eb 100644 --- a/mt-kahypar/partition/refinement/CMakeLists.txt +++ b/mt-kahypar/partition/refinement/CMakeLists.txt @@ -4,6 +4,7 @@ set(RefinementSources fm/global_rollback.cpp fm/sequential_twoway_fm_refiner.cpp label_propagation/label_propagation_refiner.cpp + jet/jet_refiner.cpp rebalancing/rebalancer.cpp deterministic/deterministic_label_propagation.cpp flows/refiner_adapter.cpp diff --git a/mt-kahypar/partition/refinement/jet/jet_refiner.cpp b/mt-kahypar/partition/refinement/jet/jet_refiner.cpp new file mode 100644 index 000000000..0f65cbf84 --- /dev/null +++ b/mt-kahypar/partition/refinement/jet/jet_refiner.cpp @@ -0,0 +1,279 @@ +/******************************************************************************* + * MIT License + * + * This file is part of Mt-KaHyPar. + * + * Copyright (C) 2023 Nikolai Maas + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + ******************************************************************************/ + +#include "mt-kahypar/partition/refinement/jet/jet_refiner.h" + +#include "tbb/parallel_for.h" + +#include "mt-kahypar/definitions.h" +#include "mt-kahypar/partition/metrics.h" +#include "mt-kahypar/partition/refinement/gains/gain_definitions.h" +#include "mt-kahypar/utils/randomize.h" +#include "mt-kahypar/utils/utilities.h" +#include "mt-kahypar/utils/timer.h" +#include "mt-kahypar/utils/cast.h" +#include "mt-kahypar/datastructures/streaming_vector.h" + +namespace mt_kahypar { + + template + bool JetRefiner::refineImpl( + mt_kahypar_partitioned_hypergraph_t& phg, + const parallel::scalable_vector& refinement_nodes, + Metrics& best_metrics, + const double) { + PartitionedHypergraph& hypergraph = utils::cast(phg); + resizeDataStructuresForCurrentK(); + _gain.reset(); + + // Initialize set of active vertices + initializeActiveNodes(hypergraph, refinement_nodes); + + // Perform Label Propagation + labelPropagationRound(hypergraph); + + + // Update global part weight and sizes + DBG << "[JET] Old imbalance: " << best_metrics.imbalance; + best_metrics.imbalance = metrics::imbalance(hypergraph, _context); + + // Update metrics statistics + Gain delta = _gain.delta(); + DBG << "[JET] New imbalance: " << best_metrics.imbalance << ", delta: " << delta; + + recomputePenalties(hypergraph); + + HEAVY_REFINEMENT_ASSERT(hypergraph.checkTrackedPartitionInformation(_gain_cache)); + HEAVY_REFINEMENT_ASSERT(best_metrics.quality + delta == + metrics::quality(hypergraph, _context, + !_context.refinement.label_propagation.execute_sequential), + V(best_metrics.quality) << V(delta) << V((best_metrics.quality + delta)) + << V(metrics::quality(hypergraph, _context, + !_context.refinement.label_propagation.execute_sequential))); + + best_metrics.quality += delta; + utils::Utilities::instance().getStats(_context.utility_id).update_stat("jet_improvement", std::abs(delta)); + return delta < 0; + } + + template + void JetRefiner::labelPropagationRound( + PartitionedHypergraph& hypergraph) { + // This function is passed as lambda to the changeNodePart function and used + // to calculate the "real" delta of a move (in terms of the used objective function). + auto objective_delta = [&](const SyncronizedEdgeUpdate& sync_update) { + _gain.computeDeltaForHyperedge(sync_update); + }; + + auto move_node = [&](const size_t j) { + const HypernodeID hn = _active_nodes[j]; + if constexpr (precomputed) { + const PartitionID from = hypergraph.partID(hn); + const auto [gain, to] = _gains_and_target[hn]; + Gain total_gain = 0; + for (const HyperedgeID& he : hypergraph.incidentEdges(hn)) { + HypernodeID pin_count_in_from_part_after = 0; + HypernodeID pin_count_in_to_part_after = 1; + for (const HypernodeID& pin : hypergraph.pins(he)) { + if (pin != hn) { + // Jet uses an order based on the precomputed gain values: + // If the precomputed gain of another node is better than for the current node + // (or the gain is equal and the id is smaller), we assume the node is already + // moved to its target part. + auto [gain_p, to_p] = _gains_and_target[pin]; + PartitionID part = (gain_p < gain || (gain_p == gain && pin < hn)) ? to_p : hypergraph.partID(pin); + if (part == from) { + pin_count_in_from_part_after++; + } else if (part == to) { + pin_count_in_to_part_after++; + } + } + } + total_gain += AttributedGains::gain(he, hypergraph.edgeWeight(he), hypergraph.edgeSize(he), + pin_count_in_from_part_after, pin_count_in_to_part_after); + } + + if (gain < 0) { + changeNodePart(hypergraph, hn, from, to, objective_delta); + _active_node_was_moved[hn] = uint8_t(true); + } + } else { + if ( moveVertexGreedily(hypergraph, hn, objective_delta) ) { + _active_node_was_moved[hn] = uint8_t(true); + } + } + }; + + if ( _context.refinement.jet.execute_sequential ) { + for ( size_t j = 0; j < _active_nodes.size(); ++j ) { + move_node(j); + } + } else { + tbb::parallel_for(UL(0), _active_nodes.size(), move_node); + } + } + + template + void JetRefiner::rollback( + PartitionedHypergraph& hypergraph) { + auto reset_node = [&](const HypernodeID hn) { + const PartitionID part_id = hypergraph.partID(hn); + if (part_id != _old_parts[hn]) { + bool success = false; + if ( _context.forceGainCacheUpdates() && _gain_cache.isInitialized() ) { + success = hypergraph.changeNodePart(_gain_cache, hn, part_id, _old_parts[hn]); + } else { + success = hypergraph.changeNodePart(hn, part_id, _old_parts[hn]); + } + ASSERT(success); + } + }; + + if ( _context.refinement.jet.execute_sequential ) { + for ( size_t j = 0; j < _active_nodes.size(); ++j ) { + reset_node(j); + } + } else { + tbb::parallel_for(UL(0), _active_nodes.size(), reset_node); + } + // TODO: update gain cache?! + } + + template + void JetRefiner::initializeImpl(mt_kahypar_partitioned_hypergraph_t&) { + // PartitionedHypergraph& hypergraph = utils::cast(phg); + } + + template + void JetRefiner::recomputePenalties(const PartitionedHypergraph& hypergraph) { + if ( _context.forceGainCacheUpdates() && _gain_cache.isInitialized() ) { + auto recompute = [&](size_t j) { + const HypernodeID hn = _active_nodes[j]; + if ( _active_node_was_moved[hn] ) { + _gain_cache.recomputePenaltyTermEntry(hypergraph, hn); + if (!_context.refinement.jet.vertex_locking) { + _active_node_was_moved[hn] = uint8_t(false); + } + } else { + ASSERT(_gain_cache.penaltyTerm(hn, hypergraph.partID(hn)) + == _gain_cache.recomputePenaltyTerm(hypergraph, hn)); + } + }; + + if ( _context.refinement.jet.execute_sequential ) { + for (size_t j = 0; j < _active_nodes.size(); ++j) { + recompute(j); + } + } else { + tbb::parallel_for(UL(0), _active_nodes.size(), recompute); + } + } + } + + template + void JetRefiner::initializeActiveNodes( + PartitionedHypergraph& hypergraph, + const parallel::scalable_vector& refinement_nodes) { + ASSERT(_active_node_was_moved.size() >= hypergraph.initialNumNodes()); + // TODO: Fast reset array for _active_node_was_moved + _active_nodes.clear(); + _gains_and_target.clear(); + + auto process_node = [&](const HypernodeID hn, auto add_node_fn) { + bool accept_border = !_context.refinement.jet.restrict_to_border_nodes || hypergraph.isBorderNode(hn); + bool accept_locked = !_context.refinement.jet.vertex_locking || !_active_node_was_moved[hn]; + if ( accept_border && accept_locked ) { + const PartitionID from = hypergraph.partID(hn); + if constexpr (precomputed) { + RatingMap& tmp_scores = _gain.localScores(); + Gain isolated_block_gain = 0; + _gain.precomputeGains(hypergraph, hn, tmp_scores, isolated_block_gain); + Move best_move = _gain.computeMaxGainMoveForScores(hypergraph, tmp_scores, isolated_block_gain, + hn, false, false, true); + tmp_scores.clear(); + // TODO: fine factor? + bool accept_node = best_move.gain < std::floor( + _context.refinement.jet.negative_gain_factor_coarse * isolated_block_gain); + if (accept_node) { + add_node_fn(hn); + _gains_and_target[hn] = {best_move.gain, best_move.to}; + } + } else { + add_node_fn(hn); + } + _old_parts[hn] = from; + } else if (!accept_locked) { + ASSERT(_context.refinement.jet.vertex_locking); + _active_node_was_moved[hn] = false; + } + }; + + if ( refinement_nodes.empty() ) { + // setup active nodes sequentially + if ( _context.refinement.jet.execute_sequential ) { + for ( const HypernodeID hn : hypergraph.nodes() ) { + process_node(hn, [&](const HypernodeID hn) { + _active_nodes.push_back(hn); + }); + } + } else { + // setup active nodes in parallel + ds::StreamingVector tmp_active_nodes; + hypergraph.doParallelForAllNodes([&](const HypernodeID& hn) { + process_node(hn, [&](const HypernodeID hn) { + tmp_active_nodes.stream(hn); + }); + }); + + _active_nodes = tmp_active_nodes.copy_parallel(); + } + } else { + ALWAYS_ASSERT(false); // TODO: rollback + if constexpr (precomputed) { + ALWAYS_ASSERT(false); // TODO + } else { + _active_nodes = refinement_nodes; + } + } + + if ( _context.refinement.jet.execute_sequential ) { + utils::Randomize::instance().shuffleVector( + _active_nodes, UL(0), _active_nodes.size(), SCHED_GETCPU); + } else { + utils::Randomize::instance().parallelShuffleVector( + _active_nodes, UL(0), _active_nodes.size()); + } + } + + namespace { + #define JET_REFINER_GREEDY(X, Y) JetRefiner + #define JET_REFINER_PRE(X, Y) JetRefiner + } + + // explicitly instantiate so the compiler can generate them when compiling this cpp file + INSTANTIATE_CLASS_WITH_TYPE_TRAITS_AND_GAIN_TYPES(JET_REFINER_GREEDY) + INSTANTIATE_CLASS_WITH_TYPE_TRAITS_AND_GAIN_TYPES(JET_REFINER_PRE) +} diff --git a/mt-kahypar/partition/refinement/jet/jet_refiner.h b/mt-kahypar/partition/refinement/jet/jet_refiner.h new file mode 100644 index 000000000..cfe346041 --- /dev/null +++ b/mt-kahypar/partition/refinement/jet/jet_refiner.h @@ -0,0 +1,171 @@ +/******************************************************************************* + * MIT License + * + * This file is part of Mt-KaHyPar. + * + * Copyright (C) 2023 Nikolai Maas + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + ******************************************************************************/ + +#pragma once + +#include "kahypar/datastructure/fast_reset_flag_array.h" + +#include "mt-kahypar/datastructures/thread_safe_fast_reset_flag_array.h" +#include "mt-kahypar/parallel/stl/scalable_vector.h" +#include "mt-kahypar/partition/context.h" +#include "mt-kahypar/partition/refinement/i_refiner.h" +#include "mt-kahypar/partition/refinement/gains/gain_cache_ptr.h" +#include "mt-kahypar/utils/cast.h" + + +namespace mt_kahypar { +template +class JetRefiner final : public IRefiner { + private: + using Hypergraph = typename TypeTraits::Hypergraph; + using PartitionedHypergraph = typename TypeTraits::PartitionedHypergraph; + using GainCache = typename GainTypes::GainCache; + using GainCalculator = typename GainTypes::GainComputation; + using RatingMap = typename GainCalculator::RatingMap; + using AttributedGains = typename GainTypes::AttributedGains; + using ActiveNodes = parallel::scalable_vector; + + static constexpr bool debug = false; + static constexpr bool enable_heavy_assert = false; + + public: + explicit JetRefiner(const HypernodeID num_hypernodes, + const HyperedgeID, + const Context& context, + GainCache& gain_cache) : + _context(context), + _gain_cache(gain_cache), + _current_k(context.partition.k), + _gain(context), + _active_nodes(), + _active_node_was_moved(num_hypernodes, uint8_t(false)), + _old_parts(num_hypernodes), + _gains_and_target(precomputed ? num_hypernodes : 0) { } + + explicit JetRefiner(const HypernodeID num_hypernodes, + const HyperedgeID num_hyperedges, + const Context& context, + gain_cache_t gain_cache) : + JetRefiner(num_hypernodes, num_hyperedges, context, + GainCachePtr::cast(gain_cache)) { } + + JetRefiner(const JetRefiner&) = delete; + JetRefiner(JetRefiner&&) = delete; + + JetRefiner & operator= (const JetRefiner &) = delete; + JetRefiner & operator= (JetRefiner &&) = delete; + + private: + bool refineImpl(mt_kahypar_partitioned_hypergraph_t& hypergraph, + const parallel::scalable_vector& refinement_nodes, + Metrics& best_metrics, + double) final; + + void labelPropagationRound(PartitionedHypergraph& hypergraph); + + void rollback(PartitionedHypergraph& hypergraph); + + template + bool moveVertexGreedily(PartitionedHypergraph& hypergraph, + const HypernodeID hn, + const F& objective_delta) { + bool is_moved = false; + ASSERT(hn != kInvalidHypernode); + if ( hypergraph.isBorderNode(hn) ) { + ASSERT(hypergraph.nodeIsEnabled(hn)); + + Move best_move = _gain.computeMaxGainMove(hypergraph, hn, false, false, true); + const bool positive_gain = best_move.gain < 0; + if (positive_gain && best_move.from != best_move.to) { + PartitionID from = best_move.from; + PartitionID to = best_move.to; + + Gain delta_before = _gain.localDelta(); + changeNodePart(hypergraph, hn, from, to, objective_delta); + is_moved = true; + + // In case the move to block 'to' was successful, we verify that the "real" gain + // of the move is either equal to our computed gain or if not, still improves + // the solution quality. + Gain move_delta = _gain.localDelta() - delta_before; + bool accept_move = (move_delta == best_move.gain || move_delta <= 0); + if (!accept_move) { + ASSERT(hypergraph.partID(hn) == to); + changeNodePart(hypergraph, hn, to, from, objective_delta); + } + } + } + + return is_moved; + } + + void initializeActiveNodes(PartitionedHypergraph& hypergraph, + const parallel::scalable_vector& refinement_nodes); + + void initializeImpl(mt_kahypar_partitioned_hypergraph_t&) final; + + void recomputePenalties(const PartitionedHypergraph& hypergraph); // TODO: after rebalancing, after rollback??? + + template + void changeNodePart(PartitionedHypergraph& phg, + const HypernodeID hn, + const PartitionID from, + const PartitionID to, + const F& objective_delta) { + constexpr HypernodeWeight inf_weight = std::numeric_limits::max(); + bool success = false; + if ( _context.forceGainCacheUpdates() && _gain_cache.isInitialized() ) { + success = phg.changeNodePart(_gain_cache, hn, from, to, inf_weight, []{}, objective_delta); + } else { + success = phg.changeNodePart(hn, from, to, inf_weight, []{}, objective_delta); + } + ASSERT(success); + } + + void resizeDataStructuresForCurrentK() { + // If the number of blocks changes, we resize data structures + // (can happen during deep multilevel partitioning) + if ( _current_k != _context.partition.k ) { + _current_k = _context.partition.k; + _gain.changeNumberOfBlocks(_current_k); + } + } + + const Context& _context; + GainCache& _gain_cache; + PartitionID _current_k; + GainCalculator _gain; + ActiveNodes _active_nodes; + parallel::scalable_vector _active_node_was_moved; + parallel::scalable_vector _old_parts; + parallel::scalable_vector> _gains_and_target; +}; + +template +using PrecomputedJetRefiner = JetRefiner; +template +using GreedyJetRefiner = JetRefiner; +} // namespace kahypar diff --git a/tests/partition/refinement/CMakeLists.txt b/tests/partition/refinement/CMakeLists.txt index 4467f0901..616a46bd0 100644 --- a/tests/partition/refinement/CMakeLists.txt +++ b/tests/partition/refinement/CMakeLists.txt @@ -6,6 +6,7 @@ target_sources(mt_kahypar_tests PRIVATE # quotient_graph_test.cc gain_policy_test.cc label_propagation_refiner_test.cc + jet_refiner_test.cc rollback_test.cc rebalance_test.cc twoway_fm_refiner_test.cc diff --git a/tests/partition/refinement/jet_refiner_test.cc b/tests/partition/refinement/jet_refiner_test.cc new file mode 100644 index 000000000..5f150c261 --- /dev/null +++ b/tests/partition/refinement/jet_refiner_test.cc @@ -0,0 +1,244 @@ +/******************************************************************************* + * MIT License + * + * This file is part of Mt-KaHyPar. + * + * Copyright (C) 2019 Tobias Heuer + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + ******************************************************************************/ + +#include "gmock/gmock.h" + +#include "tests/datastructures/hypergraph_fixtures.h" +#include "mt-kahypar/definitions.h" +#include "mt-kahypar/io/hypergraph_factory.h" +#include "mt-kahypar/partition/context.h" +#include "mt-kahypar/partition/registries/register_refinement_algorithms.cpp" +#include "mt-kahypar/partition/initial_partitioning/bfs_initial_partitioner.h" +#include "mt-kahypar/partition/refinement/jet/jet_refiner.h" +#include "mt-kahypar/utils/randomize.h" +#include "mt-kahypar/utils/cast.h" + +using ::testing::Test; + +namespace mt_kahypar { +template +struct TestConfig { }; + +template +struct TestConfig { + using TypeTraits = TypeTraitsT; + using GainTypes = Km1GainTypes; + using Refiner = JetRefiner; + static constexpr PartitionID K = k; + static constexpr Objective OBJECTIVE = Objective::km1; + static constexpr JetAlgorithm JET_ALGO = JetAlgorithm::greedy_unordered; +}; + +template +struct TestConfig { + using TypeTraits = TypeTraitsT; + using GainTypes = CutGainTypes; + using Refiner = JetRefiner; + static constexpr PartitionID K = k; + static constexpr Objective OBJECTIVE = Objective::cut; + static constexpr JetAlgorithm JET_ALGO = JetAlgorithm::greedy_unordered; +}; + +template +class AJetRefiner : public Test { + static size_t num_threads; + + public: + using TypeTraits = typename Config::TypeTraits; + using GainTypes = typename Config::GainTypes; + using Hypergraph = typename TypeTraits::Hypergraph; + using PartitionedHypergraph = typename TypeTraits::PartitionedHypergraph; + using Refiner = typename Config::Refiner; + using GainCache = typename GainTypes::GainCache; + + AJetRefiner() : + hypergraph(), + partitioned_hypergraph(), + context(), + gain_cache(), + refiner(nullptr), + metrics() { + context.partition.graph_filename = "../tests/instances/contracted_ibm01.hgr"; + context.partition.graph_community_filename = "../tests/instances/contracted_ibm01.hgr.community"; + context.partition.mode = Mode::direct; + context.partition.objective = Config::OBJECTIVE; + context.partition.gain_policy = context.partition.objective == Objective::km1 ? GainPolicy::km1 : GainPolicy::cut; + context.partition.epsilon = 0.25; + context.partition.k = Config::K; + #ifdef KAHYPAR_ENABLE_N_LEVEL_PARTITIONING_FEATURES + context.partition.preset_type = Hypergraph::is_static_hypergraph ? + PresetType::default_preset : PresetType::quality_preset; + #else + context.partition.preset_type = PresetType::default_preset; + #endif + context.partition.instance_type = InstanceType::hypergraph; + context.partition.partition_type = PartitionedHypergraph::TYPE; + context.partition.verbose_output = false; + + // Shared Memory + context.shared_memory.num_threads = num_threads; + + // Initial Partitioning + context.initial_partitioning.mode = Mode::deep_multilevel; + context.initial_partitioning.runs = 1; + + // Jet + context.refinement.jet.algorithm = Config::JET_ALGO; + context.initial_partitioning.refinement.jet.algorithm = Config::JET_ALGO; + context.initial_partitioning.refinement.jet.vertex_locking = false; + + // Read hypergraph + hypergraph = io::readInputFile( + "../tests/instances/contracted_unweighted_ibm01.hgr", FileFormat::hMetis, true); + partitioned_hypergraph = PartitionedHypergraph( + context.partition.k, hypergraph, parallel_tag_t()); + context.setupPartWeights(hypergraph.totalWeight()); + initialPartition(); + + refiner = std::make_unique(hypergraph.initialNumNodes(), hypergraph.initialNumEdges(), context, gain_cache); + mt_kahypar_partitioned_hypergraph_t phg = utils::partitioned_hg_cast(partitioned_hypergraph); + refiner->initialize(phg); + } + + void initialPartition() { + Context ip_context(context); + ip_context.refinement.label_propagation.algorithm = LabelPropagationAlgorithm::do_nothing; + InitialPartitioningDataContainer ip_data(partitioned_hypergraph, ip_context); + ip_data_container_t* ip_data_ptr = ip::to_pointer(ip_data); + BFSInitialPartitioner initial_partitioner( + InitialPartitioningAlgorithm::bfs, ip_data_ptr, ip_context, 420, 0); + initial_partitioner.partition(); + ip_data.apply(); + metrics.quality = metrics::quality(partitioned_hypergraph, context); + metrics.imbalance = metrics::imbalance(partitioned_hypergraph, context); + } + + Hypergraph hypergraph; + PartitionedHypergraph partitioned_hypergraph; + Context context; + GainCache gain_cache; + std::unique_ptr refiner; + Metrics metrics; +}; + +template +size_t AJetRefiner::num_threads = HardwareTopology::instance().num_cpus(); + +static constexpr double EPS = 0.05; + +typedef ::testing::Types, + TestConfig, + TestConfig, + TestConfig, + TestConfig, + TestConfig, + TestConfig, + TestConfig, + TestConfig, + TestConfig, + TestConfig, + TestConfig + ENABLE_N_LEVEL(COMMA TestConfig) + ENABLE_N_LEVEL(COMMA TestConfig) + ENABLE_N_LEVEL(COMMA TestConfig) + ENABLE_N_LEVEL(COMMA TestConfig) + ENABLE_N_LEVEL(COMMA TestConfig) + ENABLE_N_LEVEL(COMMA TestConfig) > TestConfigs; + +TYPED_TEST_CASE(AJetRefiner, TestConfigs); + +TYPED_TEST(AJetRefiner, UpdatesImbalanceCorrectly) { + mt_kahypar_partitioned_hypergraph_t phg = utils::partitioned_hg_cast(this->partitioned_hypergraph); + this->refiner->refine(phg, {}, this->metrics, std::numeric_limits::max()); + ASSERT_DOUBLE_EQ(metrics::imbalance(this->partitioned_hypergraph, this->context), this->metrics.imbalance); +} + +TYPED_TEST(AJetRefiner, DoesNotViolateBalanceConstraint) { + mt_kahypar_partitioned_hypergraph_t phg = utils::partitioned_hg_cast(this->partitioned_hypergraph); + this->refiner->refine(phg, {}, this->metrics, std::numeric_limits::max()); + ASSERT_LE(this->metrics.imbalance, this->context.partition.epsilon + EPS); +} + +TYPED_TEST(AJetRefiner, UpdatesMetricsCorrectly) { + mt_kahypar_partitioned_hypergraph_t phg = utils::partitioned_hg_cast(this->partitioned_hypergraph); + this->refiner->refine(phg, {}, this->metrics, std::numeric_limits::max()); + ASSERT_EQ(metrics::quality(this->partitioned_hypergraph, this->context.partition.objective), + this->metrics.quality); +} + +TYPED_TEST(AJetRefiner, DoesNotWorsenSolutionQuality) { + HyperedgeWeight objective_before = metrics::quality(this->partitioned_hypergraph, this->context.partition.objective); + mt_kahypar_partitioned_hypergraph_t phg = utils::partitioned_hg_cast(this->partitioned_hypergraph); + this->refiner->refine(phg, {}, this->metrics, std::numeric_limits::max()); + ASSERT_LE(this->metrics.quality, objective_before); +} + + +TYPED_TEST(AJetRefiner, IncreasesTheNumberOfBlocks) { + using PartitionedHypergraph = typename TestFixture::PartitionedHypergraph; + HyperedgeWeight objective_before = metrics::quality(this->partitioned_hypergraph, this->context.partition.objective); + mt_kahypar_partitioned_hypergraph_t phg = utils::partitioned_hg_cast(this->partitioned_hypergraph); + this->refiner->refine(phg, {}, this->metrics, std::numeric_limits::max()); + ASSERT_LE(this->metrics.quality, objective_before); + + // Initialize partition with larger K + const PartitionID old_k = this->context.partition.k; + this->context.partition.k = 2 * old_k; + this->context.setupPartWeights(this->hypergraph.totalWeight()); + PartitionedHypergraph phg_with_larger_k( + this->context.partition.k, this->hypergraph, mt_kahypar::parallel_tag_t()); + utils::Randomize& rand = utils::Randomize::instance(); + vec non_optimized_partition(this->hypergraph.initialNumNodes(), kInvalidPartition); + this->partitioned_hypergraph.doParallelForAllNodes([&](const HypernodeID hn) { + const PartitionID block = this->partitioned_hypergraph.partID(hn); + phg_with_larger_k.setOnlyNodePart(hn, rand.flipCoin(SCHED_GETCPU) ? 2 * block : 2 * block + 1); + non_optimized_partition[hn] = phg_with_larger_k.partID(hn); + }); + phg_with_larger_k.initializePartition(); + this->metrics.quality = metrics::quality(phg_with_larger_k, this->context); + this->metrics.imbalance = metrics::imbalance(phg_with_larger_k, this->context); + + objective_before = metrics::quality(phg_with_larger_k, this->context.partition.objective); + mt_kahypar_partitioned_hypergraph_t phg_larger_k = utils::partitioned_hg_cast(phg_with_larger_k); + this->refiner->initialize(phg_larger_k); + this->refiner->refine(phg_larger_k, {}, this->metrics, std::numeric_limits::max()); + ASSERT_LE(this->metrics.quality, objective_before); + ASSERT_EQ(metrics::quality(phg_with_larger_k, this->context.partition.objective), + this->metrics.quality); + + // Check if refiner has moved some nodes from new blocks + bool has_moved_nodes = false; + for ( const HypernodeID hn : phg_with_larger_k.nodes() ) { + if ( non_optimized_partition[hn] >= old_k && + non_optimized_partition[hn] != phg_with_larger_k.partID(hn) ) { + has_moved_nodes = true; + break; + } + } + ASSERT_TRUE(has_moved_nodes); +} + +} // namespace mt_kahypar From 6caf0fb0055b2738910c0b87e751c0ccf0071fa4 Mon Sep 17 00:00:00 2001 From: Nikolai Maas Date: Thu, 11 May 2023 11:26:29 +0200 Subject: [PATCH 003/152] command line / factories integration --- mt-kahypar/io/command_line_options.cpp | 35 +++++++++++++++++++ .../coarsening/multilevel_uncoarsener.cpp | 10 ++++++ .../coarsening/multilevel_uncoarsener.h | 1 + .../coarsening/nlevel_uncoarsener.cpp | 6 ++++ .../partition/coarsening/nlevel_uncoarsener.h | 1 + .../partition/coarsening/uncoarsener_base.h | 5 +++ mt-kahypar/partition/factories.h | 14 ++++++++ .../register_refinement_algorithms.cpp | 35 ++++++++++++++++--- 8 files changed, 103 insertions(+), 4 deletions(-) diff --git a/mt-kahypar/io/command_line_options.cpp b/mt-kahypar/io/command_line_options.cpp index dbf5ac95e..a3403344d 100644 --- a/mt-kahypar/io/command_line_options.cpp +++ b/mt-kahypar/io/command_line_options.cpp @@ -380,6 +380,41 @@ namespace mt_kahypar { &context.initial_partitioning.refinement.label_propagation.hyperedge_size_activation_threshold))->value_name( "")->default_value(100), "LP refiner activates only neighbors of moved vertices that are part of hyperedges with a size less than this threshold") + ((initial_partitioning ? "i-r-jet-type" : "r-jet-type"), + po::value()->value_name("")->notifier( + [&, initial_partitioning](const std::string& type) { + if (initial_partitioning) { + context.initial_partitioning.refinement.jet.algorithm = jetAlgorithmFromString(type); + } else { + context.refinement.jet.algorithm = jetAlgorithmFromString(type); + } + })->default_value("do_nothing"), + "Jet Algorithm:\n" + "- precomputed_ordered\n" + "- greedy_unordered\n" + "- do_nothing") + // ((initial_partitioning ? "i-r-jet-maximum-iterations" : "r-jet-maximum-iterations"), + // po::value((!initial_partitioning ? &context.refinement.jet.maximum_iterations : + // &context.initial_partitioning.refinement.jet.maximum_iterations))->value_name( + // "")->default_value(5), + // "Maximum number of jet rounds") + ((initial_partitioning ? "i-r-jet-restrict-to-border-nodes" : "r-jet-restrict-to-border-nodes"), + po::value((!initial_partitioning ? &context.refinement.jet.restrict_to_border_nodes : + &context.initial_partitioning.refinement.jet.restrict_to_border_nodes))->value_name( + "")->default_value(true), + "If true, then only border nodes are considered for JET.") + ((initial_partitioning ? "i-r-jet-negative-gain-factor-coarse" : "r-jet-negative-gain-factor-coarse"), + po::value( + (!initial_partitioning ? &context.refinement.jet.negative_gain_factor_coarse : + &context.initial_partitioning.refinement.jet.negative_gain_factor_coarse))->value_name( + "")->default_value(0.25), + "Factor used by JET for filtering negative gain moves (only applicable to precomputed_ordered).") + ((initial_partitioning ? "i-r-jet-negative-gain-factor-fine" : "r-jet-negative-gain-factor-fine"), + po::value( + (!initial_partitioning ? &context.refinement.jet.negative_gain_factor_fine : + &context.initial_partitioning.refinement.jet.negative_gain_factor_fine))->value_name( + "")->default_value(0.75), + "Factor used by JET for filtering negative gain moves (only applicable to precomputed_ordered).") ((initial_partitioning ? "i-r-fm-type" : "r-fm-type"), po::value()->value_name("")->notifier( [&, initial_partitioning](const std::string& type) { diff --git a/mt-kahypar/partition/coarsening/multilevel_uncoarsener.cpp b/mt-kahypar/partition/coarsening/multilevel_uncoarsener.cpp index 5d85ebe81..6deec533d 100644 --- a/mt-kahypar/partition/coarsening/multilevel_uncoarsener.cpp +++ b/mt-kahypar/partition/coarsening/multilevel_uncoarsener.cpp @@ -223,6 +223,16 @@ namespace mt_kahypar { _timer.stop_timer("label_propagation"); } + if ( _jet && _context.refinement.jet.algorithm != JetAlgorithm::do_nothing ) { + _timer.start_timer("initialize_jet_refiner", "Initialize JET Refiner"); + _jet->initialize(phg); + _timer.stop_timer("initialize_jet_refiner"); + + _timer.start_timer("jet", "JET"); + improvement_found |= _jet->refine(phg, dummy, _current_metrics, time_limit); + _timer.stop_timer("jet"); + } + if ( _fm && _context.refinement.fm.algorithm != FMAlgorithm::do_nothing ) { _timer.start_timer("initialize_fm_refiner", "Initialize FM Refiner"); _fm->initialize(phg); diff --git a/mt-kahypar/partition/coarsening/multilevel_uncoarsener.h b/mt-kahypar/partition/coarsening/multilevel_uncoarsener.h index 9ee6275d0..844ad9212 100644 --- a/mt-kahypar/partition/coarsening/multilevel_uncoarsener.h +++ b/mt-kahypar/partition/coarsening/multilevel_uncoarsener.h @@ -98,6 +98,7 @@ class MultilevelUncoarsener : public IUncoarsener, using Base::_uncoarseningData; using Base::_gain_cache; using Base::_label_propagation; + using Base::_jet; using Base::_fm; using Base::_flows; using Base::_rebalancer; diff --git a/mt-kahypar/partition/coarsening/nlevel_uncoarsener.cpp b/mt-kahypar/partition/coarsening/nlevel_uncoarsener.cpp index 2f98b4e1f..f0e88953d 100644 --- a/mt-kahypar/partition/coarsening/nlevel_uncoarsener.cpp +++ b/mt-kahypar/partition/coarsening/nlevel_uncoarsener.cpp @@ -381,6 +381,12 @@ namespace mt_kahypar { improvement_found = false; const HyperedgeWeight metric_before = _current_metrics.quality; + if ( _jet && _context.refinement.jet.algorithm != JetAlgorithm::do_nothing ) { + _timer.start_timer("jet", "JET"); + improvement_found |= _jet->refine(phg, {}, _current_metrics, time_limit); + _timer.stop_timer("jet"); + } + if ( _fm && _context.refinement.fm.algorithm != FMAlgorithm::do_nothing ) { _timer.start_timer("fm", "FM"); improvement_found |= _fm->refine(phg, {}, _current_metrics, time_limit); diff --git a/mt-kahypar/partition/coarsening/nlevel_uncoarsener.h b/mt-kahypar/partition/coarsening/nlevel_uncoarsener.h index cc98ef2ec..fea98cce7 100644 --- a/mt-kahypar/partition/coarsening/nlevel_uncoarsener.h +++ b/mt-kahypar/partition/coarsening/nlevel_uncoarsener.h @@ -138,6 +138,7 @@ class NLevelUncoarsener : public IUncoarsener, using Base::_uncoarseningData; using Base::_gain_cache; using Base::_label_propagation; + using Base::_jet; using Base::_fm; using Base::_flows; using Base::_rebalancer; diff --git a/mt-kahypar/partition/coarsening/uncoarsener_base.h b/mt-kahypar/partition/coarsening/uncoarsener_base.h index 5500283a2..aeaf2ea36 100644 --- a/mt-kahypar/partition/coarsening/uncoarsener_base.h +++ b/mt-kahypar/partition/coarsening/uncoarsener_base.h @@ -61,6 +61,7 @@ class UncoarsenerBase { _uncoarseningData(uncoarseningData), _gain_cache(gain_cache_t {nullptr, GainPolicy::none}), _label_propagation(nullptr), + _jet(nullptr), _fm(nullptr), _flows(nullptr), _rebalancer(nullptr) {} @@ -81,6 +82,7 @@ class UncoarsenerBase { UncoarseningData& _uncoarseningData; gain_cache_t _gain_cache; std::unique_ptr _label_propagation; + std::unique_ptr _jet; std::unique_ptr _fm; std::unique_ptr _flows; std::unique_ptr _rebalancer; @@ -122,6 +124,9 @@ class UncoarsenerBase { _label_propagation = LabelPropagationFactory::getInstance().createObject( _context.refinement.label_propagation.algorithm, _hg.initialNumNodes(), _hg.initialNumEdges(), _context, _gain_cache); + _jet = JetFactory::getInstance().createObject( + _context.refinement.jet.algorithm, + _hg.initialNumNodes(), _hg.initialNumEdges(), _context, _gain_cache); _fm = FMFactory::getInstance().createObject( _context.refinement.fm.algorithm, _hg.initialNumNodes(), _hg.initialNumEdges(), _context, _gain_cache); diff --git a/mt-kahypar/partition/factories.h b/mt-kahypar/partition/factories.h index 03ae8ba9a..980858ee2 100644 --- a/mt-kahypar/partition/factories.h +++ b/mt-kahypar/partition/factories.h @@ -44,6 +44,7 @@ #include "mt-kahypar/partition/refinement/i_refiner.h" #include "mt-kahypar/partition/refinement/flows/i_flow_refiner.h" #include "mt-kahypar/partition/refinement/label_propagation/label_propagation_refiner.h" +#include "mt-kahypar/partition/refinement/jet/jet_refiner.h" #include "mt-kahypar/partition/refinement/deterministic/deterministic_label_propagation.h" #include "mt-kahypar/partition/refinement/fm/multitry_kway_fm.h" #include "mt-kahypar/partition/refinement/gains/gain_definitions.h" @@ -89,6 +90,19 @@ using DeterministicLabelPropagationDispatcher = kahypar::meta::StaticMultiDispat IRefiner, kahypar::meta::Typelist>; +using JetFactory = kahypar::meta::Factory; + +using PrecomputedJetDispatcher = kahypar::meta::StaticMultiDispatchFactory< + PrecomputedJetRefiner, + IRefiner, + kahypar::meta::Typelist>; + +using GreedyJetDispatcher = kahypar::meta::StaticMultiDispatchFactory< + GreedyJetRefiner, + IRefiner, + kahypar::meta::Typelist>; + using FMFactory = kahypar::meta::Factory; diff --git a/mt-kahypar/partition/registries/register_refinement_algorithms.cpp b/mt-kahypar/partition/registries/register_refinement_algorithms.cpp index 1675b2da0..abc006364 100644 --- a/mt-kahypar/partition/registries/register_refinement_algorithms.cpp +++ b/mt-kahypar/partition/registries/register_refinement_algorithms.cpp @@ -51,6 +51,25 @@ return new refiner(num_hypernodes, num_hyperedges, context, gain_cache); \ }) +#define REGISTER_DISPATCHED_JET_REFINER(id, dispatcher, ...) \ + static kahypar::meta::Registrar register_ ## dispatcher( \ + id, \ + [](const HypernodeID num_hypernodes, \ + const Context& context, gain_cache_t gain_cache) { \ + return dispatcher::create( \ + std::forward_as_tuple(num_hypernodes, context, gain_cache), \ + __VA_ARGS__ \ + ); \ + }) + +#define REGISTER_JET_REFINER(id, refiner, t) \ + static kahypar::meta::Registrar JOIN(register_ ## refiner, t)( \ + id, \ + [](const HypernodeID num_hypernodes, \ + const Context& context, gain_cache_t gain_cache) -> IRefiner* { \ + return new refiner(num_hypernodes, context, gain_cache); \ + }) + #define REGISTER_DISPATCHED_FM_REFINER(id, dispatcher, ...) \ static kahypar::meta::Registrar register_ ## dispatcher( \ id, \ @@ -136,13 +155,21 @@ REGISTER_DISPATCHED_LP_REFINER(LabelPropagationAlgorithm::deterministic, context.partition.partition_type)); REGISTER_LP_REFINER(LabelPropagationAlgorithm::do_nothing, DoNothingRefiner, 1); +REGISTER_DISPATCHED_JET_REFINER(JetAlgorithm::greedy_unordered, + GreedyJetDispatcher, + kahypar::meta::PolicyRegistry::getInstance().getPolicy( + context.partition.partition_type), + kahypar::meta::PolicyRegistry::getInstance().getPolicy( + context.partition.gain_policy)); +REGISTER_JET_REFINER(JetAlgorithm::do_nothing, DoNothingRefiner, 2); + REGISTER_DISPATCHED_FM_REFINER(FMAlgorithm::kway_fm, FMDispatcher, kahypar::meta::PolicyRegistry::getInstance().getPolicy( context.partition.partition_type), kahypar::meta::PolicyRegistry::getInstance().getPolicy( context.partition.gain_policy)); -REGISTER_FM_REFINER(FMAlgorithm::do_nothing, DoNothingRefiner, 2); +REGISTER_FM_REFINER(FMAlgorithm::do_nothing, DoNothingRefiner, 3); REGISTER_DISPATCHED_FLOW_SCHEDULER(FlowAlgorithm::flow_cutter, FlowSchedulerDispatcher, @@ -150,7 +177,7 @@ REGISTER_DISPATCHED_FLOW_SCHEDULER(FlowAlgorithm::flow_cutter, context.partition.partition_type), kahypar::meta::PolicyRegistry::getInstance().getPolicy( context.partition.gain_policy)); -REGISTER_FLOW_SCHEDULER(FlowAlgorithm::do_nothing, DoNothingRefiner, 3); +REGISTER_FLOW_SCHEDULER(FlowAlgorithm::do_nothing, DoNothingRefiner, 4); REGISTER_DISPATCHED_REBALANCER(RebalancingAlgorithm::simple_rebalancer, RebalancerDispatcher, @@ -158,7 +185,7 @@ REGISTER_DISPATCHED_REBALANCER(RebalancingAlgorithm::simple_rebalancer, context.partition.partition_type), kahypar::meta::PolicyRegistry::getInstance().getPolicy( context.partition.gain_policy)); -REGISTER_REBALANCER(RebalancingAlgorithm::do_nothing, DoNothingRefiner, 4); +REGISTER_REBALANCER(RebalancingAlgorithm::do_nothing, DoNothingRefiner, 5); REGISTER_DISPATCHED_FLOW_REFINER(FlowAlgorithm::flow_cutter, FlowRefinementDispatcher, @@ -166,5 +193,5 @@ REGISTER_DISPATCHED_FLOW_REFINER(FlowAlgorithm::flow_cutter, context.partition.partition_type), kahypar::meta::PolicyRegistry::getInstance().getPolicy( context.partition.gain_policy)); -REGISTER_FLOW_REFINER(FlowAlgorithm::do_nothing, DoNothingFlowRefiner, 5); +REGISTER_FLOW_REFINER(FlowAlgorithm::do_nothing, DoNothingFlowRefiner, 6); } // namespace mt_kahypar From d8810e71f23174263e6a2e43f311780396a5e554 Mon Sep 17 00:00:00 2001 From: Nikolai Maas Date: Thu, 11 May 2023 17:16:38 +0200 Subject: [PATCH 004/152] rebalancing and rollback --- mt-kahypar/io/command_line_options.cpp | 5 + .../coarsening/multilevel_uncoarsener.cpp | 95 +++++++++++-- .../coarsening/multilevel_uncoarsener.h | 5 + mt-kahypar/partition/context.h | 1 + .../initial_partitioning_data_container.h | 1 + .../partition/refinement/jet/jet_refiner.cpp | 131 +++++++++--------- .../partition/refinement/jet/jet_refiner.h | 19 +-- 7 files changed, 179 insertions(+), 78 deletions(-) diff --git a/mt-kahypar/io/command_line_options.cpp b/mt-kahypar/io/command_line_options.cpp index a3403344d..a3604b01f 100644 --- a/mt-kahypar/io/command_line_options.cpp +++ b/mt-kahypar/io/command_line_options.cpp @@ -325,6 +325,11 @@ namespace mt_kahypar { &context.initial_partitioning.refinement.refine_until_no_improvement))->value_name( "")->default_value(false), "Executes all refinement algorithms as long as they find an improvement on the current partition.") + ((initial_partitioning ? "i-r-rounds-with-rollback" : "r-rounds-with-rollback"), + po::value((!initial_partitioning ? &context.refinement.rounds_with_rollback : + &context.initial_partitioning.refinement.rounds_with_rollback))->value_name( + "")->default_value(0), + "Tries multiple refinement rounds even if no immediate improvement is found, rolling back to the best partition.") ((initial_partitioning ? "i-r-relative-improvement-threshold" : "r-relative-improvement-threshold"), po::value((!initial_partitioning ? &context.refinement.relative_improvement_threshold : &context.initial_partitioning.refinement.relative_improvement_threshold))->value_name( diff --git a/mt-kahypar/partition/coarsening/multilevel_uncoarsener.cpp b/mt-kahypar/partition/coarsening/multilevel_uncoarsener.cpp index 6deec533d..c4140b33a 100644 --- a/mt-kahypar/partition/coarsening/multilevel_uncoarsener.cpp +++ b/mt-kahypar/partition/coarsening/multilevel_uncoarsener.cpp @@ -198,6 +198,10 @@ namespace mt_kahypar { void MultilevelUncoarsener::refineImpl() { PartitionedHypergraph& partitioned_hypergraph = *_uncoarseningData.partitioned_hg; const double time_limit = Base::refinementTimeLimit(_context, (_uncoarseningData.hierarchy)[_current_level].coarseningTime()); + _best_metrics = _current_metrics; + if (_context.refinement.rounds_with_rollback > 0) { + writeCurrentPartition(); + } if ( debug && _context.type == ContextType::main ) { io::printHypergraphInfo(partitioned_hypergraph.hypergraph(), @@ -208,10 +212,11 @@ namespace mt_kahypar { parallel::scalable_vector dummy; bool improvement_found = true; + size_t rounds = 0; mt_kahypar_partitioned_hypergraph_t phg = utils::partitioned_hg_cast(partitioned_hypergraph); - while( improvement_found ) { + while( true ) { improvement_found = false; - const HyperedgeWeight metric_before = _current_metrics.quality; + const HyperedgeWeight metric_before = _best_metrics.quality; if ( _label_propagation && _context.refinement.label_propagation.algorithm != LabelPropagationAlgorithm::do_nothing ) { _timer.start_timer("initialize_lp_refiner", "Initialize LP Refiner"); @@ -219,7 +224,9 @@ namespace mt_kahypar { _timer.stop_timer("initialize_lp_refiner"); _timer.start_timer("label_propagation", "Label Propagation"); - improvement_found |= _label_propagation->refine(phg, dummy, _current_metrics, time_limit); + if ( _label_propagation->refine(phg, dummy, _current_metrics, time_limit) ) { + improvement_found |= checkForImprovement(); + } _timer.stop_timer("label_propagation"); } @@ -229,7 +236,9 @@ namespace mt_kahypar { _timer.stop_timer("initialize_jet_refiner"); _timer.start_timer("jet", "JET"); - improvement_found |= _jet->refine(phg, dummy, _current_metrics, time_limit); + if ( _jet->refine(phg, dummy, _current_metrics, time_limit) ) { + improvement_found |= checkForImprovement(); + } _timer.stop_timer("jet"); } @@ -239,7 +248,9 @@ namespace mt_kahypar { _timer.stop_timer("initialize_fm_refiner"); _timer.start_timer("fm", "FM"); - improvement_found |= _fm->refine(phg, dummy, _current_metrics, time_limit); + if ( _fm->refine(phg, dummy, _current_metrics, time_limit) ) { + improvement_found |= checkForImprovement(); + } _timer.stop_timer("fm"); } @@ -249,7 +260,9 @@ namespace mt_kahypar { _timer.stop_timer("initialize_flow_scheduler"); _timer.start_timer("flow_refinement_scheduler", "Flow Refinement Scheduler"); - improvement_found |= _flows->refine(phg, dummy, _current_metrics, time_limit); + if ( _flows->refine(phg, dummy, _current_metrics, time_limit) ) { + improvement_found |= checkForImprovement(); + } _timer.stop_timer("flow_refinement_scheduler"); } @@ -262,17 +275,83 @@ namespace mt_kahypar { const HyperedgeWeight metric_after = _current_metrics.quality; const double relative_improvement = 1.0 - static_cast(metric_after) / metric_before; - if ( !_context.refinement.refine_until_no_improvement || - relative_improvement <= _context.refinement.relative_improvement_threshold ) { + + if (_context.refinement.rounds_with_rollback > 0) { + // necessary for JET, since JET might worsen the partition + if (improvement_found && relative_improvement > _context.refinement.relative_improvement_threshold) { + rounds = 0; + } else { + rounds++; + if (rounds >= _context.refinement.rounds_with_rollback) { + break; + } + } + } else if ( !improvement_found || !_context.refinement.refine_until_no_improvement || + relative_improvement <= _context.refinement.relative_improvement_threshold ) { break; } } + const HyperedgeWeight best = _best_metrics.quality; + if (_context.refinement.rounds_with_rollback > 0 && best < _current_metrics.quality) { + // revert to best partition + DBG << "Rollback to best partition with value: " << best; + auto reset_node = [&](const HypernodeID hn) { + const PartitionID part_id = partitioned_hypergraph.partID(hn); + if (part_id != _block_ids[hn]) { + bool success = partitioned_hypergraph.changeNodePart(hn, part_id, _block_ids[hn]); + ASSERT(success); + unused(success); + } + }; + + if ( _context.type == ContextType::main ) { + partitioned_hypergraph.doParallelForAllNodes(reset_node); + } else { + for (const HypernodeID hn : partitioned_hypergraph.nodes()) { + reset_node(hn); + } + } + _current_metrics = _best_metrics; + } + if ( _context.type == ContextType::main) { DBG << "--------------------------------------------------\n"; } } + template + bool MultilevelUncoarsener::checkForImprovement() { + // check for improved partition with precondition that the refiner found an improvement + if (_context.refinement.rounds_with_rollback == 0) { + _best_metrics = _current_metrics; + return true; + } + + if (_current_metrics.quality < _best_metrics.quality) { + _best_metrics = _current_metrics; + writeCurrentPartition(); + return true; + } + return false; + } + + template + void MultilevelUncoarsener::writeCurrentPartition() { + PartitionedHypergraph& partitioned_hypergraph = *_uncoarseningData.partitioned_hg; + + // write current partition to _block_ids + if ( _context.type == ContextType::main ) { + partitioned_hypergraph.doParallelForAllNodes([&](const HypernodeID hn) { + _block_ids[hn] = partitioned_hypergraph.partID(hn); + }); + } else { + for (const HypernodeID hn : partitioned_hypergraph.nodes()) { + _block_ids[hn] = partitioned_hypergraph.partID(hn); + } + } + } + INSTANTIATE_CLASS_WITH_TYPE_TRAITS(MultilevelUncoarsener) } diff --git a/mt-kahypar/partition/coarsening/multilevel_uncoarsener.h b/mt-kahypar/partition/coarsening/multilevel_uncoarsener.h index 844ad9212..75965c740 100644 --- a/mt-kahypar/partition/coarsening/multilevel_uncoarsener.h +++ b/mt-kahypar/partition/coarsening/multilevel_uncoarsener.h @@ -93,6 +93,10 @@ class MultilevelUncoarsener : public IUncoarsener, PartitionedHypergraph&& movePartitionedHypergraphImpl() override; + bool checkForImprovement(); + + void writeCurrentPartition(); + using Base::_hg; using Base::_context; using Base::_uncoarseningData; @@ -109,6 +113,7 @@ class MultilevelUncoarsener : public IUncoarsener, int _num_levels; ds::Array _block_ids; Metrics _current_metrics; + Metrics _best_metrics; utils::ProgressBar _progress; }; diff --git a/mt-kahypar/partition/context.h b/mt-kahypar/partition/context.h index 4ad123737..310c773b0 100644 --- a/mt-kahypar/partition/context.h +++ b/mt-kahypar/partition/context.h @@ -220,6 +220,7 @@ struct RefinementParameters { double relative_improvement_threshold = 0.0; size_t max_batch_size = std::numeric_limits::max(); size_t min_border_vertices_per_thread = 0; + size_t rounds_with_rollback = 0; }; std::ostream & operator<< (std::ostream& str, const RefinementParameters& params); diff --git a/mt-kahypar/partition/initial_partitioning/initial_partitioning_data_container.h b/mt-kahypar/partition/initial_partitioning/initial_partitioning_data_container.h index e7ffdeee5..25f9f9544 100644 --- a/mt-kahypar/partition/initial_partitioning/initial_partitioning_data_container.h +++ b/mt-kahypar/partition/initial_partitioning/initial_partitioning_data_container.h @@ -375,6 +375,7 @@ class InitialPartitioningDataContainer { // Setup Label Propagation IRefiner Config for Initial Partitioning _context.refinement = _context.initial_partitioning.refinement; _context.refinement.label_propagation.execute_sequential = true; + _context.refinement.jet.execute_sequential = true; if (_context.partition.deterministic) { _best_partitions.resize(_max_pop_size); diff --git a/mt-kahypar/partition/refinement/jet/jet_refiner.cpp b/mt-kahypar/partition/refinement/jet/jet_refiner.cpp index 0f65cbf84..3535c65e5 100644 --- a/mt-kahypar/partition/refinement/jet/jet_refiner.cpp +++ b/mt-kahypar/partition/refinement/jet/jet_refiner.cpp @@ -31,6 +31,7 @@ #include "mt-kahypar/definitions.h" #include "mt-kahypar/partition/metrics.h" #include "mt-kahypar/partition/refinement/gains/gain_definitions.h" +#include "mt-kahypar/partition/refinement/rebalancing/rebalancer.h" #include "mt-kahypar/utils/randomize.h" #include "mt-kahypar/utils/utilities.h" #include "mt-kahypar/utils/timer.h" @@ -44,10 +45,11 @@ namespace mt_kahypar { mt_kahypar_partitioned_hypergraph_t& phg, const parallel::scalable_vector& refinement_nodes, Metrics& best_metrics, - const double) { + const double time_limit) { PartitionedHypergraph& hypergraph = utils::cast(phg); resizeDataStructuresForCurrentK(); _gain.reset(); + _gain_cache.reset(); // current rebalancer is not capable of using the gain cache // Initialize set of active vertices initializeActiveNodes(hypergraph, refinement_nodes); @@ -56,25 +58,36 @@ namespace mt_kahypar { labelPropagationRound(hypergraph); - // Update global part weight and sizes - DBG << "[JET] Old imbalance: " << best_metrics.imbalance; - best_metrics.imbalance = metrics::imbalance(hypergraph, _context); - // Update metrics statistics + const HyperedgeWeight old_quality = best_metrics.quality; + DBG << "[JET] Old imbalance: " << best_metrics.imbalance << ", objective: " << old_quality; + best_metrics.imbalance = metrics::imbalance(hypergraph, _context); Gain delta = _gain.delta(); - DBG << "[JET] New imbalance: " << best_metrics.imbalance << ", delta: " << delta; + DBG << "[JET] New imbalance: " << best_metrics.imbalance << ", tmp delta: " << delta; + best_metrics.quality += delta; + + // recomputePenalties(hypergraph, false); + // HEAVY_REFINEMENT_ASSERT(hypergraph.checkTrackedPartitionInformation(_gain_cache)); - recomputePenalties(hypergraph); + bool did_rebalance = false; + if (!metrics::isBalanced(hypergraph, _context)) { + rebalance(hypergraph, best_metrics, time_limit); + + best_metrics.imbalance = metrics::imbalance(hypergraph, _context); + delta = best_metrics.quality - old_quality; + DBG << "[JET] Imbalance after rebalancing: " << best_metrics.imbalance << ", total delta: " << delta; + did_rebalance = true; + } + + recomputePenalties(hypergraph, did_rebalance); HEAVY_REFINEMENT_ASSERT(hypergraph.checkTrackedPartitionInformation(_gain_cache)); - HEAVY_REFINEMENT_ASSERT(best_metrics.quality + delta == + HEAVY_REFINEMENT_ASSERT(best_metrics.quality == metrics::quality(hypergraph, _context, !_context.refinement.label_propagation.execute_sequential), - V(best_metrics.quality) << V(delta) << V((best_metrics.quality + delta)) - << V(metrics::quality(hypergraph, _context, + V(best_metrics.quality) << V(delta) << V(metrics::quality(hypergraph, _context, !_context.refinement.label_propagation.execute_sequential))); - best_metrics.quality += delta; utils::Utilities::instance().getStats(_context.utility_id).update_stat("jet_improvement", std::abs(delta)); return delta < 0; } @@ -118,11 +131,11 @@ namespace mt_kahypar { if (gain < 0) { changeNodePart(hypergraph, hn, from, to, objective_delta); - _active_node_was_moved[hn] = uint8_t(true); + _active_node_was_moved.set(hn); } } else { if ( moveVertexGreedily(hypergraph, hn, objective_delta) ) { - _active_node_was_moved[hn] = uint8_t(true); + _active_node_was_moved.set(hn); } } }; @@ -136,59 +149,43 @@ namespace mt_kahypar { } } - template - void JetRefiner::rollback( - PartitionedHypergraph& hypergraph) { - auto reset_node = [&](const HypernodeID hn) { - const PartitionID part_id = hypergraph.partID(hn); - if (part_id != _old_parts[hn]) { - bool success = false; - if ( _context.forceGainCacheUpdates() && _gain_cache.isInitialized() ) { - success = hypergraph.changeNodePart(_gain_cache, hn, part_id, _old_parts[hn]); - } else { - success = hypergraph.changeNodePart(hn, part_id, _old_parts[hn]); - } - ASSERT(success); - } - }; - - if ( _context.refinement.jet.execute_sequential ) { - for ( size_t j = 0; j < _active_nodes.size(); ++j ) { - reset_node(j); - } - } else { - tbb::parallel_for(UL(0), _active_nodes.size(), reset_node); - } - // TODO: update gain cache?! - } - template void JetRefiner::initializeImpl(mt_kahypar_partitioned_hypergraph_t&) { // PartitionedHypergraph& hypergraph = utils::cast(phg); } template - void JetRefiner::recomputePenalties(const PartitionedHypergraph& hypergraph) { + void JetRefiner::recomputePenalties(const PartitionedHypergraph& hypergraph, + bool did_rebalance) { if ( _context.forceGainCacheUpdates() && _gain_cache.isInitialized() ) { - auto recompute = [&](size_t j) { - const HypernodeID hn = _active_nodes[j]; - if ( _active_node_was_moved[hn] ) { - _gain_cache.recomputePenaltyTermEntry(hypergraph, hn); - if (!_context.refinement.jet.vertex_locking) { - _active_node_was_moved[hn] = uint8_t(false); + if (did_rebalance) { + if ( _context.refinement.jet.execute_sequential ) { + for ( const HypernodeID hn : hypergraph.nodes() ) { + _gain_cache.recomputePenaltyTermEntry(hypergraph, hn); } } else { - ASSERT(_gain_cache.penaltyTerm(hn, hypergraph.partID(hn)) - == _gain_cache.recomputePenaltyTerm(hypergraph, hn)); + hypergraph.doParallelForAllNodes([&](const HypernodeID& hn) { + _gain_cache.recomputePenaltyTermEntry(hypergraph, hn); + }); } - }; + } else { + auto recompute = [&](size_t j) { + const HypernodeID hn = _active_nodes[j]; + if ( _active_node_was_moved[hn] ) { + _gain_cache.recomputePenaltyTermEntry(hypergraph, hn); + } else { + ASSERT(_gain_cache.penaltyTerm(hn, hypergraph.partID(hn)) + == _gain_cache.recomputePenaltyTerm(hypergraph, hn)); + } + }; - if ( _context.refinement.jet.execute_sequential ) { - for (size_t j = 0; j < _active_nodes.size(); ++j) { - recompute(j); + if ( _context.refinement.jet.execute_sequential ) { + for (size_t j = 0; j < _active_nodes.size(); ++j) { + recompute(j); + } + } else { + tbb::parallel_for(UL(0), _active_nodes.size(), recompute); } - } else { - tbb::parallel_for(UL(0), _active_nodes.size(), recompute); } } } @@ -197,16 +194,20 @@ namespace mt_kahypar { void JetRefiner::initializeActiveNodes( PartitionedHypergraph& hypergraph, const parallel::scalable_vector& refinement_nodes) { - ASSERT(_active_node_was_moved.size() >= hypergraph.initialNumNodes()); - // TODO: Fast reset array for _active_node_was_moved _active_nodes.clear(); _gains_and_target.clear(); + if (!_context.refinement.jet.vertex_locking || hypergraph.initialNumNodes() != _current_num_nodes) { + _active_node_was_moved.reset(); + _current_num_nodes = hypergraph.initialNumNodes(); + } + const double gain_factor = (_current_num_nodes == _top_level_num_nodes) ? + _context.refinement.jet.negative_gain_factor_fine : + _context.refinement.jet.negative_gain_factor_coarse; auto process_node = [&](const HypernodeID hn, auto add_node_fn) { bool accept_border = !_context.refinement.jet.restrict_to_border_nodes || hypergraph.isBorderNode(hn); bool accept_locked = !_context.refinement.jet.vertex_locking || !_active_node_was_moved[hn]; if ( accept_border && accept_locked ) { - const PartitionID from = hypergraph.partID(hn); if constexpr (precomputed) { RatingMap& tmp_scores = _gain.localScores(); Gain isolated_block_gain = 0; @@ -214,9 +215,7 @@ namespace mt_kahypar { Move best_move = _gain.computeMaxGainMoveForScores(hypergraph, tmp_scores, isolated_block_gain, hn, false, false, true); tmp_scores.clear(); - // TODO: fine factor? - bool accept_node = best_move.gain < std::floor( - _context.refinement.jet.negative_gain_factor_coarse * isolated_block_gain); + bool accept_node = best_move.gain < std::floor(gain_factor * isolated_block_gain); if (accept_node) { add_node_fn(hn); _gains_and_target[hn] = {best_move.gain, best_move.to}; @@ -224,10 +223,9 @@ namespace mt_kahypar { } else { add_node_fn(hn); } - _old_parts[hn] = from; } else if (!accept_locked) { ASSERT(_context.refinement.jet.vertex_locking); - _active_node_was_moved[hn] = false; + _active_node_was_moved.set(hn, false); } }; @@ -268,6 +266,15 @@ namespace mt_kahypar { } } + template + void JetRefiner::rebalance(PartitionedHypergraph& hypergraph, + Metrics& current_metrics, double time_limit) { + ASSERT(!_context.partition.deterministic); + Rebalancer rebalancer(_context); + mt_kahypar_partitioned_hypergraph_t phg = utils::partitioned_hg_cast(hypergraph); + rebalancer.refine(phg, {}, current_metrics, time_limit); + } + namespace { #define JET_REFINER_GREEDY(X, Y) JetRefiner #define JET_REFINER_PRE(X, Y) JetRefiner diff --git a/mt-kahypar/partition/refinement/jet/jet_refiner.h b/mt-kahypar/partition/refinement/jet/jet_refiner.h index cfe346041..d51b083b4 100644 --- a/mt-kahypar/partition/refinement/jet/jet_refiner.h +++ b/mt-kahypar/partition/refinement/jet/jet_refiner.h @@ -59,10 +59,11 @@ class JetRefiner final : public IRefiner { _context(context), _gain_cache(gain_cache), _current_k(context.partition.k), + _top_level_num_nodes(num_hypernodes), + _current_num_nodes(num_hypernodes), _gain(context), _active_nodes(), - _active_node_was_moved(num_hypernodes, uint8_t(false)), - _old_parts(num_hypernodes), + _active_node_was_moved(num_hypernodes), _gains_and_target(precomputed ? num_hypernodes : 0) { } explicit JetRefiner(const HypernodeID num_hypernodes, @@ -82,12 +83,10 @@ class JetRefiner final : public IRefiner { bool refineImpl(mt_kahypar_partitioned_hypergraph_t& hypergraph, const parallel::scalable_vector& refinement_nodes, Metrics& best_metrics, - double) final; + double time_limit) final; void labelPropagationRound(PartitionedHypergraph& hypergraph); - void rollback(PartitionedHypergraph& hypergraph); - template bool moveVertexGreedily(PartitionedHypergraph& hypergraph, const HypernodeID hn, @@ -127,7 +126,9 @@ class JetRefiner final : public IRefiner { void initializeImpl(mt_kahypar_partitioned_hypergraph_t&) final; - void recomputePenalties(const PartitionedHypergraph& hypergraph); // TODO: after rebalancing, after rollback??? + void recomputePenalties(const PartitionedHypergraph& hypergraph, bool did_rebalance); + + void rebalance(PartitionedHypergraph& hypergraph, Metrics& current_metrics, double time_limit); template void changeNodePart(PartitionedHypergraph& phg, @@ -143,6 +144,7 @@ class JetRefiner final : public IRefiner { success = phg.changeNodePart(hn, from, to, inf_weight, []{}, objective_delta); } ASSERT(success); + unused(success); } void resizeDataStructuresForCurrentK() { @@ -157,10 +159,11 @@ class JetRefiner final : public IRefiner { const Context& _context; GainCache& _gain_cache; PartitionID _current_k; + HypernodeID _top_level_num_nodes; + HypernodeID _current_num_nodes; GainCalculator _gain; ActiveNodes _active_nodes; - parallel::scalable_vector _active_node_was_moved; - parallel::scalable_vector _old_parts; + kahypar::ds::FastResetFlagArray<> _active_node_was_moved; parallel::scalable_vector> _gains_and_target; }; From 33c0f4655f1c9d9db9e8d59a528adfa8f19ae8b8 Mon Sep 17 00:00:00 2001 From: Nikolai Maas Date: Fri, 12 May 2023 10:50:53 +0200 Subject: [PATCH 005/152] support for randomized vertex locking --- mt-kahypar/io/command_line_options.cpp | 5 +++++ mt-kahypar/partition/context.cpp | 1 + mt-kahypar/partition/context.h | 2 +- mt-kahypar/partition/refinement/jet/jet_refiner.cpp | 13 +++++++++---- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/mt-kahypar/io/command_line_options.cpp b/mt-kahypar/io/command_line_options.cpp index a3604b01f..5104b3963 100644 --- a/mt-kahypar/io/command_line_options.cpp +++ b/mt-kahypar/io/command_line_options.cpp @@ -408,6 +408,11 @@ namespace mt_kahypar { &context.initial_partitioning.refinement.jet.restrict_to_border_nodes))->value_name( "")->default_value(true), "If true, then only border nodes are considered for JET.") + ((initial_partitioning ? "i-r-jet-vertex-locking" : "r-jet-vertex-locking"), + po::value((!initial_partitioning ? &context.refinement.jet.vertex_locking : + &context.initial_partitioning.refinement.jet.vertex_locking))->value_name( + "")->default_value(1.0), + "Fraction of vertices locked by JET (which are not moved in the next iteration).") ((initial_partitioning ? "i-r-jet-negative-gain-factor-coarse" : "r-jet-negative-gain-factor-coarse"), po::value( (!initial_partitioning ? &context.refinement.jet.negative_gain_factor_coarse : diff --git a/mt-kahypar/partition/context.cpp b/mt-kahypar/partition/context.cpp index 9a919f9ab..2005a8ba8 100644 --- a/mt-kahypar/partition/context.cpp +++ b/mt-kahypar/partition/context.cpp @@ -135,6 +135,7 @@ namespace mt_kahypar { if ( params.algorithm != JetAlgorithm::do_nothing ) { // str << " Maximum Iterations: " << params.maximum_iterations << std::endl; str << " Restrict to Border Nodes: " << std::boolalpha << params.restrict_to_border_nodes << std::endl; + str << " Vertex Locking: " << std::boolalpha << params.vertex_locking << std::endl; str << " Negative Gain Factor (Coarse): " << params.negative_gain_factor_coarse << std::endl; str << " Negative Gain Factor (Fine): " << params.negative_gain_factor_fine << std::endl; } diff --git a/mt-kahypar/partition/context.h b/mt-kahypar/partition/context.h index 310c773b0..bf46aa6ce 100644 --- a/mt-kahypar/partition/context.h +++ b/mt-kahypar/partition/context.h @@ -145,7 +145,7 @@ struct JetParameters { // size_t maximum_iterations = 10; bool execute_sequential = false; bool restrict_to_border_nodes = true; - bool vertex_locking = true; + double vertex_locking = 1.0; double negative_gain_factor_coarse = 0.25; double negative_gain_factor_fine = 0.75; }; diff --git a/mt-kahypar/partition/refinement/jet/jet_refiner.cpp b/mt-kahypar/partition/refinement/jet/jet_refiner.cpp index 3535c65e5..fd531ab29 100644 --- a/mt-kahypar/partition/refinement/jet/jet_refiner.cpp +++ b/mt-kahypar/partition/refinement/jet/jet_refiner.cpp @@ -196,17 +196,21 @@ namespace mt_kahypar { const parallel::scalable_vector& refinement_nodes) { _active_nodes.clear(); _gains_and_target.clear(); - if (!_context.refinement.jet.vertex_locking || hypergraph.initialNumNodes() != _current_num_nodes) { + if ((_context.refinement.jet.vertex_locking == 0.0) || hypergraph.initialNumNodes() != _current_num_nodes) { _active_node_was_moved.reset(); _current_num_nodes = hypergraph.initialNumNodes(); } const double gain_factor = (_current_num_nodes == _top_level_num_nodes) ? _context.refinement.jet.negative_gain_factor_fine : _context.refinement.jet.negative_gain_factor_coarse; + mt_kahypar::utils::Randomize& randomize = mt_kahypar::utils::Randomize::instance(); auto process_node = [&](const HypernodeID hn, auto add_node_fn) { bool accept_border = !_context.refinement.jet.restrict_to_border_nodes || hypergraph.isBorderNode(hn); - bool accept_locked = !_context.refinement.jet.vertex_locking || !_active_node_was_moved[hn]; + bool accept_locked = (_context.refinement.jet.vertex_locking == 0.0) || !_active_node_was_moved[hn]; + if (!accept_locked && _context.refinement.jet.vertex_locking < 1.0) { + accept_locked = randomize.getRandomFloat(0.0, 1.0, SCHED_GETCPU) > _context.refinement.jet.vertex_locking; + } if ( accept_border && accept_locked ) { if constexpr (precomputed) { RatingMap& tmp_scores = _gain.localScores(); @@ -223,8 +227,9 @@ namespace mt_kahypar { } else { add_node_fn(hn); } - } else if (!accept_locked) { - ASSERT(_context.refinement.jet.vertex_locking); + } + if (!accept_locked) { + ASSERT(_context.refinement.jet.vertex_locking > 0); _active_node_was_moved.set(hn, false); } }; From 80466909f8088e5383ab37b6362ab57af5ddd403 Mon Sep 17 00:00:00 2001 From: Nikolai Maas Date: Mon, 15 May 2023 17:33:31 +0200 Subject: [PATCH 006/152] rework jet refiner to use multiple internal rounds --- mt-kahypar/io/command_line_options.cpp | 22 +- mt-kahypar/partition/context.cpp | 3 +- mt-kahypar/partition/context.h | 4 +- .../partition/refinement/jet/jet_refiner.cpp | 370 +++++++++++++----- .../partition/refinement/jet/jet_refiner.h | 33 +- .../partition/refinement/jet_refiner_test.cc | 18 +- 6 files changed, 331 insertions(+), 119 deletions(-) diff --git a/mt-kahypar/io/command_line_options.cpp b/mt-kahypar/io/command_line_options.cpp index 5104b3963..cbe5c0b68 100644 --- a/mt-kahypar/io/command_line_options.cpp +++ b/mt-kahypar/io/command_line_options.cpp @@ -398,11 +398,16 @@ namespace mt_kahypar { "- precomputed_ordered\n" "- greedy_unordered\n" "- do_nothing") - // ((initial_partitioning ? "i-r-jet-maximum-iterations" : "r-jet-maximum-iterations"), - // po::value((!initial_partitioning ? &context.refinement.jet.maximum_iterations : - // &context.initial_partitioning.refinement.jet.maximum_iterations))->value_name( - // "")->default_value(5), - // "Maximum number of jet rounds") + ((initial_partitioning ? "i-r-jet-maximum-iterations" : "r-jet-maximum-iterations"), + po::value((!initial_partitioning ? &context.refinement.jet.num_iterations : + &context.initial_partitioning.refinement.jet.num_iterations))->value_name( + "")->default_value(12), + "Maximum number of jet iterations after no improvement is found.") + ((initial_partitioning ? "i-r-jet-relative-improvement-threshold" : "r-jet-relative-improvement-threshold"), + po::value((!initial_partitioning ? &context.refinement.jet.relative_improvement_threshold : + &context.initial_partitioning.refinement.jet.relative_improvement_threshold))->value_name( + "")->default_value(0.001), + "Maximum number of jet iterations after no improvement is found.") ((initial_partitioning ? "i-r-jet-restrict-to-border-nodes" : "r-jet-restrict-to-border-nodes"), po::value((!initial_partitioning ? &context.refinement.jet.restrict_to_border_nodes : &context.initial_partitioning.refinement.jet.restrict_to_border_nodes))->value_name( @@ -425,6 +430,13 @@ namespace mt_kahypar { &context.initial_partitioning.refinement.jet.negative_gain_factor_fine))->value_name( "")->default_value(0.75), "Factor used by JET for filtering negative gain moves (only applicable to precomputed_ordered).") + ((initial_partitioning ? "i-r-jet-he-size-activation-threshold" : "r-jet-he-size-activation-threshold"), + po::value( + (!initial_partitioning ? &context.refinement.jet.hyperedge_size_activation_threshold + : + &context.initial_partitioning.refinement.jet.hyperedge_size_activation_threshold))->value_name( + "")->default_value(100), + "JET refiner activates only neighbors of moved vertices that are part of hyperedges with a size less than this threshold") ((initial_partitioning ? "i-r-fm-type" : "r-fm-type"), po::value()->value_name("")->notifier( [&, initial_partitioning](const std::string& type) { diff --git a/mt-kahypar/partition/context.cpp b/mt-kahypar/partition/context.cpp index 2005a8ba8..d34e1fea7 100644 --- a/mt-kahypar/partition/context.cpp +++ b/mt-kahypar/partition/context.cpp @@ -133,7 +133,8 @@ namespace mt_kahypar { str << " Jet Parameters:" << std::endl; str << " Algorithm: " << params.algorithm << std::endl; if ( params.algorithm != JetAlgorithm::do_nothing ) { - // str << " Maximum Iterations: " << params.maximum_iterations << std::endl; + str << " Num Iterations: " << params.num_iterations << std::endl; + str << " Relative Improvement Threshold: " << params.relative_improvement_threshold << std::endl; str << " Restrict to Border Nodes: " << std::boolalpha << params.restrict_to_border_nodes << std::endl; str << " Vertex Locking: " << std::boolalpha << params.vertex_locking << std::endl; str << " Negative Gain Factor (Coarse): " << params.negative_gain_factor_coarse << std::endl; diff --git a/mt-kahypar/partition/context.h b/mt-kahypar/partition/context.h index bf46aa6ce..ef70a34ed 100644 --- a/mt-kahypar/partition/context.h +++ b/mt-kahypar/partition/context.h @@ -142,12 +142,14 @@ std::ostream & operator<< (std::ostream& str, const LabelPropagationParameters& struct JetParameters { JetAlgorithm algorithm = JetAlgorithm::do_nothing; - // size_t maximum_iterations = 10; + size_t num_iterations = 12; + double relative_improvement_threshold = 0.001; bool execute_sequential = false; bool restrict_to_border_nodes = true; double vertex_locking = 1.0; double negative_gain_factor_coarse = 0.25; double negative_gain_factor_fine = 0.75; + size_t hyperedge_size_activation_threshold = std::numeric_limits::max(); }; std::ostream & operator<< (std::ostream& str, const JetParameters& params); diff --git a/mt-kahypar/partition/refinement/jet/jet_refiner.cpp b/mt-kahypar/partition/refinement/jet/jet_refiner.cpp index fd531ab29..9635d8a92 100644 --- a/mt-kahypar/partition/refinement/jet/jet_refiner.cpp +++ b/mt-kahypar/partition/refinement/jet/jet_refiner.cpp @@ -46,50 +46,106 @@ namespace mt_kahypar { const parallel::scalable_vector& refinement_nodes, Metrics& best_metrics, const double time_limit) { + const HyperedgeWeight input_quality = best_metrics.quality; PartitionedHypergraph& hypergraph = utils::cast(phg); + Metrics current_metrics = best_metrics; + utils::Timer& timer = utils::Utilities::instance().getTimer(_context.utility_id); resizeDataStructuresForCurrentK(); - _gain.reset(); _gain_cache.reset(); // current rebalancer is not capable of using the gain cache // Initialize set of active vertices - initializeActiveNodes(hypergraph, refinement_nodes); + timer.start_timer("compute_active_nodes", "Compute Active Nodes"); + if (refinement_nodes.empty()) { + computeActiveNodesFromGraph(hypergraph); + } else { + computeActiveNodesFromVector(hypergraph, refinement_nodes); + } + timer.stop_timer("compute_active_nodes"); + DBG << "[JET] initialization done, num_nodes=" << hypergraph.initialNumNodes(); - // Perform Label Propagation - labelPropagationRound(hypergraph); + _current_partition_is_best = true; + size_t num_rounds = 0; + while (true) { + // We need to store the best partition for rollback and the current partition to determine + // which nodes have been moved. However, we don't need to write both if the best partition + // is equal to the current partition. + if (_current_partition_is_best) { + storeCurrentPartition(hypergraph, _best_partition); + } else { + storeCurrentPartition(hypergraph, _current_partition); + } + _gain.reset(); + // Perform Label Propagation + timer.start_timer("label_propagation", "Label Propagation"); + labelPropagationRound(hypergraph); + timer.stop_timer("label_propagation"); - // Update metrics statistics - const HyperedgeWeight old_quality = best_metrics.quality; - DBG << "[JET] Old imbalance: " << best_metrics.imbalance << ", objective: " << old_quality; - best_metrics.imbalance = metrics::imbalance(hypergraph, _context); - Gain delta = _gain.delta(); - DBG << "[JET] New imbalance: " << best_metrics.imbalance << ", tmp delta: " << delta; - best_metrics.quality += delta; + // Update metrics statistics + const HyperedgeWeight old_quality = current_metrics.quality; + DBG << "[JET] Old imbalance: " << current_metrics.imbalance << ", objective: " << old_quality; + current_metrics.imbalance = metrics::imbalance(hypergraph, _context); + Gain delta = _gain.delta(); + DBG << "[JET] New imbalance: " << current_metrics.imbalance << ", tmp delta: " << delta; + current_metrics.quality += delta; - // recomputePenalties(hypergraph, false); - // HEAVY_REFINEMENT_ASSERT(hypergraph.checkTrackedPartitionInformation(_gain_cache)); + // recomputePenalties(hypergraph, false); + + bool did_rebalance = false; + if (!metrics::isBalanced(hypergraph, _context)) { + timer.start_timer("rebalance", "Rebalance"); + rebalance(hypergraph, current_metrics, time_limit); + timer.stop_timer("rebalance"); + + current_metrics.imbalance = metrics::imbalance(hypergraph, _context); + delta = current_metrics.quality - old_quality; + DBG << "[JET] Imbalance after rebalancing: " << current_metrics.imbalance << ", total delta: " << delta; + did_rebalance = true; + } + recomputePenalties(hypergraph, did_rebalance); - bool did_rebalance = false; - if (!metrics::isBalanced(hypergraph, _context)) { - rebalance(hypergraph, best_metrics, time_limit); + HEAVY_REFINEMENT_ASSERT(hypergraph.checkTrackedPartitionInformation(_gain_cache)); + HEAVY_REFINEMENT_ASSERT(current_metrics.quality == + metrics::quality(hypergraph, _context, !_context.refinement.jet.execute_sequential), + V(current_metrics.quality) << V(metrics::quality(hypergraph, _context, + !_context.refinement.jet.execute_sequential))); - best_metrics.imbalance = metrics::imbalance(hypergraph, _context); - delta = best_metrics.quality - old_quality; - DBG << "[JET] Imbalance after rebalancing: " << best_metrics.imbalance << ", total delta: " << delta; - did_rebalance = true; + ++num_rounds; + if (metrics::isBalanced(hypergraph, _context) && current_metrics.quality <= best_metrics.quality) { + if (best_metrics.quality - current_metrics.quality > + _context.refinement.jet.relative_improvement_threshold * best_metrics.quality) { + num_rounds = 0; + } + best_metrics = current_metrics; + _current_partition_is_best = true; + } else { + _current_partition_is_best = false; + } + if (num_rounds >= _context.refinement.jet.num_iterations) { + break; + } else { + // initialize active vertices for next round + timer.start_timer("compute_active_nodes", "Compute Active Nodes"); + computeActiveNodesFromPreviousRound(hypergraph); + timer.stop_timer("compute_active_nodes"); + } } - recomputePenalties(hypergraph, did_rebalance); + if (!_current_partition_is_best) { + DBG << "[JET] Rollback to best partition with value " << best_metrics.quality; + rollbackToBestPartition(hypergraph); + recomputePenalties(hypergraph, true); + } HEAVY_REFINEMENT_ASSERT(hypergraph.checkTrackedPartitionInformation(_gain_cache)); HEAVY_REFINEMENT_ASSERT(best_metrics.quality == - metrics::quality(hypergraph, _context, - !_context.refinement.label_propagation.execute_sequential), - V(best_metrics.quality) << V(delta) << V(metrics::quality(hypergraph, _context, - !_context.refinement.label_propagation.execute_sequential))); + metrics::quality(hypergraph, _context, !_context.refinement.jet.execute_sequential), + V(best_metrics.quality) << V(metrics::quality(hypergraph, _context, + !_context.refinement.jet.execute_sequential))); - utils::Utilities::instance().getStats(_context.utility_id).update_stat("jet_improvement", std::abs(delta)); - return delta < 0; + utils::Utilities::instance().getStats(_context.utility_id).update_stat( + "jet_improvement", input_quality - best_metrics.quality); + return best_metrics.quality < input_quality; } template @@ -131,20 +187,21 @@ namespace mt_kahypar { if (gain < 0) { changeNodePart(hypergraph, hn, from, to, objective_delta); - _active_node_was_moved.set(hn); } } else { - if ( moveVertexGreedily(hypergraph, hn, objective_delta) ) { - _active_node_was_moved.set(hn); - } + moveVertexGreedily(hypergraph, hn, objective_delta); } }; if ( _context.refinement.jet.execute_sequential ) { + utils::Randomize::instance().shuffleVector( + _active_nodes, UL(0), _active_nodes.size(), SCHED_GETCPU); for ( size_t j = 0; j < _active_nodes.size(); ++j ) { move_node(j); } } else { + utils::Randomize::instance().parallelShuffleVector( + _active_nodes, UL(0), _active_nodes.size()); tbb::parallel_for(UL(0), _active_nodes.size(), move_node); } } @@ -157,118 +214,229 @@ namespace mt_kahypar { template void JetRefiner::recomputePenalties(const PartitionedHypergraph& hypergraph, bool did_rebalance) { + parallel::scalable_vector& current_parts = _current_partition_is_best ? _best_partition : _current_partition; + auto recompute = [&](const HypernodeID hn) { + const bool node_was_moved = (hypergraph.partID(hn) != current_parts[hn]); + if (node_was_moved) { + _gain_cache.recomputePenaltyTermEntry(hypergraph, hn); + } else { + ASSERT(_gain_cache.penaltyTerm(hn, hypergraph.partID(hn)) + == _gain_cache.recomputePenaltyTerm(hypergraph, hn)); + } + }; + if ( _context.forceGainCacheUpdates() && _gain_cache.isInitialized() ) { if (did_rebalance) { if ( _context.refinement.jet.execute_sequential ) { for ( const HypernodeID hn : hypergraph.nodes() ) { - _gain_cache.recomputePenaltyTermEntry(hypergraph, hn); + recompute(hn); } } else { - hypergraph.doParallelForAllNodes([&](const HypernodeID& hn) { - _gain_cache.recomputePenaltyTermEntry(hypergraph, hn); - }); + hypergraph.doParallelForAllNodes(recompute); } } else { - auto recompute = [&](size_t j) { - const HypernodeID hn = _active_nodes[j]; - if ( _active_node_was_moved[hn] ) { - _gain_cache.recomputePenaltyTermEntry(hypergraph, hn); - } else { - ASSERT(_gain_cache.penaltyTerm(hn, hypergraph.partID(hn)) - == _gain_cache.recomputePenaltyTerm(hypergraph, hn)); - } - }; - if ( _context.refinement.jet.execute_sequential ) { for (size_t j = 0; j < _active_nodes.size(); ++j) { - recompute(j); + recompute(_active_nodes[j]); } } else { - tbb::parallel_for(UL(0), _active_nodes.size(), recompute); + tbb::parallel_for(UL(0), _active_nodes.size(), [&](const size_t j) { + recompute(_active_nodes[j]); + }); } } } } template - void JetRefiner::initializeActiveNodes( - PartitionedHypergraph& hypergraph, - const parallel::scalable_vector& refinement_nodes) { + void JetRefiner::computeActiveNodesFromGraph(const PartitionedHypergraph& hypergraph) { + const bool top_level = (hypergraph.initialNumNodes() == _top_level_num_nodes); _active_nodes.clear(); - _gains_and_target.clear(); - if ((_context.refinement.jet.vertex_locking == 0.0) || hypergraph.initialNumNodes() != _current_num_nodes) { - _active_node_was_moved.reset(); - _current_num_nodes = hypergraph.initialNumNodes(); - } - const double gain_factor = (_current_num_nodes == _top_level_num_nodes) ? - _context.refinement.jet.negative_gain_factor_fine : - _context.refinement.jet.negative_gain_factor_coarse; - mt_kahypar::utils::Randomize& randomize = mt_kahypar::utils::Randomize::instance(); auto process_node = [&](const HypernodeID hn, auto add_node_fn) { bool accept_border = !_context.refinement.jet.restrict_to_border_nodes || hypergraph.isBorderNode(hn); - bool accept_locked = (_context.refinement.jet.vertex_locking == 0.0) || !_active_node_was_moved[hn]; - if (!accept_locked && _context.refinement.jet.vertex_locking < 1.0) { - accept_locked = randomize.getRandomFloat(0.0, 1.0, SCHED_GETCPU) > _context.refinement.jet.vertex_locking; - } - if ( accept_border && accept_locked ) { - if constexpr (precomputed) { - RatingMap& tmp_scores = _gain.localScores(); - Gain isolated_block_gain = 0; - _gain.precomputeGains(hypergraph, hn, tmp_scores, isolated_block_gain); - Move best_move = _gain.computeMaxGainMoveForScores(hypergraph, tmp_scores, isolated_block_gain, - hn, false, false, true); - tmp_scores.clear(); - bool accept_node = best_move.gain < std::floor(gain_factor * isolated_block_gain); - if (accept_node) { - add_node_fn(hn); - _gains_and_target[hn] = {best_move.gain, best_move.to}; - } - } else { - add_node_fn(hn); - } - } - if (!accept_locked) { - ASSERT(_context.refinement.jet.vertex_locking > 0); - _active_node_was_moved.set(hn, false); + if (accept_border) { + processNode(hypergraph, hn, add_node_fn, top_level); } }; - if ( refinement_nodes.empty() ) { - // setup active nodes sequentially + if ( _context.refinement.jet.execute_sequential ) { + // setup active nodes sequentially + for ( const HypernodeID hn : hypergraph.nodes() ) { + process_node(hn, [&] { + _active_nodes.push_back(hn); + }); + } + } else { + // setup active nodes in parallel + ds::StreamingVector tmp_active_nodes; + hypergraph.doParallelForAllNodes([&](const HypernodeID& hn) { + process_node(hn, [&] { + tmp_active_nodes.stream(hn); + }); + }); + _active_nodes = tmp_active_nodes.copy_parallel(); + } + } + + template + void JetRefiner::computeActiveNodesFromVector(const PartitionedHypergraph& hypergraph, + const parallel::scalable_vector& refinement_nodes) { + _active_nodes.clear(); + + if constexpr (precomputed) { + const bool top_level = (hypergraph.initialNumNodes() == _top_level_num_nodes); if ( _context.refinement.jet.execute_sequential ) { - for ( const HypernodeID hn : hypergraph.nodes() ) { - process_node(hn, [&](const HypernodeID hn) { + // setup active nodes sequentially + for ( const HypernodeID hn : refinement_nodes ) { + processNode(hypergraph, hn, [&] { _active_nodes.push_back(hn); - }); + }, top_level); } } else { // setup active nodes in parallel ds::StreamingVector tmp_active_nodes; - hypergraph.doParallelForAllNodes([&](const HypernodeID& hn) { - process_node(hn, [&](const HypernodeID hn) { + tbb::parallel_for(UL(0), refinement_nodes.size(), [&](const size_t j) { + const HypernodeID hn = refinement_nodes[j]; + processNode(hypergraph, hn, [&] { tmp_active_nodes.stream(hn); - }); + }, top_level); }); - _active_nodes = tmp_active_nodes.copy_parallel(); } } else { - ALWAYS_ASSERT(false); // TODO: rollback - if constexpr (precomputed) { - ALWAYS_ASSERT(false); // TODO - } else { - _active_nodes = refinement_nodes; + _active_nodes = refinement_nodes; + } + } + + template + void JetRefiner::computeActiveNodesFromPreviousRound(const PartitionedHypergraph& hypergraph) { + const bool top_level = (hypergraph.initialNumNodes() == _top_level_num_nodes); + mt_kahypar::utils::Randomize& randomize = mt_kahypar::utils::Randomize::instance(); + parallel::scalable_vector& current_parts = _current_partition_is_best ? _best_partition : _current_partition; + _active_nodes.clear(); + _visited_he.reset(); + _next_active.reset(); + + auto activate_neighbors_if_moved = [&](const HypernodeID hn, auto add_node_fn) { + if (hypergraph.partID(hn) == current_parts[hn]) { + return; // vertex was not moved + } + + // Set all neighbors of the vertex to active + for (const HyperedgeID& he : hypergraph.incidentEdges(hn)) { + if ( hypergraph.edgeSize(he) <= ID(_context.refinement.jet.hyperedge_size_activation_threshold) ) { + if ( !_visited_he[he] ) { + for (const HypernodeID& pin : hypergraph.pins(he)) { + if ( pin != hn && _next_active.compare_and_set_to_true(pin) ) { + // check whether vertex locking forbids activating this vertex + bool accept_locked = (_context.refinement.jet.vertex_locking == 0.0) || hypergraph.partID(pin) == current_parts[pin]; + if (!accept_locked && _context.refinement.jet.vertex_locking < 1.0) { + accept_locked = randomize.getRandomFloat(0.0, 1.0, SCHED_GETCPU) > _context.refinement.jet.vertex_locking; + } + if (accept_locked) { + add_node_fn(pin); + } + } + } + _visited_he.set(he, true); + } + } + } + }; + + if ( _context.refinement.jet.execute_sequential ) { + // setup active nodes sequentially + for ( const HypernodeID hn : hypergraph.nodes() ) { + activate_neighbors_if_moved(hn, [&](const HypernodeID neighbor) { + processNode(hypergraph, neighbor, [&] { + _active_nodes.push_back(neighbor); + }, top_level); + }); + } + } else { + // setup active nodes in parallel (use two phases for better load balancing) + ds::StreamingVector tmp_active_nodes; + hypergraph.doParallelForAllNodes([&](const HypernodeID& hn) { + activate_neighbors_if_moved(hn, [&](const HypernodeID neighbor) { + tmp_active_nodes.stream(neighbor); + }); + }); + _active_nodes = tmp_active_nodes.copy_parallel(); + tmp_active_nodes.clear_sequential(); + tbb::parallel_for(UL(0), _active_nodes.size(), [&](const size_t j) { + processNode(hypergraph, _active_nodes[j], [&] { + tmp_active_nodes.stream(_active_nodes[j]); + }, top_level); + }); + _active_nodes = tmp_active_nodes.copy_parallel(); + tmp_active_nodes.clear_parallel(); + } + } + + template + template + void JetRefiner::processNode(const PartitionedHypergraph& hypergraph, + const HypernodeID hn, F add_node_fn, + const bool top_level) { + const double gain_factor = top_level ? _context.refinement.jet.negative_gain_factor_fine : + _context.refinement.jet.negative_gain_factor_coarse; + if constexpr (precomputed) { + RatingMap& tmp_scores = _gain.localScores(); + Gain isolated_block_gain = 0; + _gain.precomputeGains(hypergraph, hn, tmp_scores, isolated_block_gain); + Move best_move = _gain.computeMaxGainMoveForScores(hypergraph, tmp_scores, isolated_block_gain, + hn, false, false, true); + tmp_scores.clear(); + bool accept_node = best_move.gain < std::floor(gain_factor * isolated_block_gain); + if (accept_node) { + add_node_fn(); + _gains_and_target[hn] = {best_move.gain, best_move.to}; } + } else { + unused(hn); + add_node_fn(); } + } + template + void JetRefiner::storeCurrentPartition(const PartitionedHypergraph& hypergraph, + parallel::scalable_vector& parts) { if ( _context.refinement.jet.execute_sequential ) { - utils::Randomize::instance().shuffleVector( - _active_nodes, UL(0), _active_nodes.size(), SCHED_GETCPU); + for (const HypernodeID hn : hypergraph.nodes()) { + parts[hn] = hypergraph.partID(hn); + } } else { - utils::Randomize::instance().parallelShuffleVector( - _active_nodes, UL(0), _active_nodes.size()); + hypergraph.doParallelForAllNodes([&](const HypernodeID hn) { + parts[hn] = hypergraph.partID(hn); + }); + } + } + + template + void JetRefiner::rollbackToBestPartition(PartitionedHypergraph& hypergraph) { + // This function is passed as lambda to the changeNodePart function and used + // to calculate the "real" delta of a move (in terms of the used objective function). + auto objective_delta = [&](const SyncronizedEdgeUpdate& sync_update) { + _gain.computeDeltaForHyperedge(sync_update); + }; + + auto reset_node = [&](const HypernodeID hn) { + const PartitionID part_id = hypergraph.partID(hn); + if (part_id != _best_partition[hn]) { + ASSERT(_best_partition[hn] != kInvalidPartition); + changeNodePart(hypergraph, hn, part_id, _best_partition[hn], objective_delta); + } + }; + + if ( _context.refinement.jet.execute_sequential ) { + for (const HypernodeID hn : hypergraph.nodes()) { + reset_node(hn); + } + } else { + hypergraph.doParallelForAllNodes(reset_node); } + _current_partition_is_best = true; } template diff --git a/mt-kahypar/partition/refinement/jet/jet_refiner.h b/mt-kahypar/partition/refinement/jet/jet_refiner.h index d51b083b4..802216a1a 100644 --- a/mt-kahypar/partition/refinement/jet/jet_refiner.h +++ b/mt-kahypar/partition/refinement/jet/jet_refiner.h @@ -53,18 +53,21 @@ class JetRefiner final : public IRefiner { public: explicit JetRefiner(const HypernodeID num_hypernodes, - const HyperedgeID, + const HyperedgeID num_hyperedges, const Context& context, GainCache& gain_cache) : _context(context), _gain_cache(gain_cache), _current_k(context.partition.k), _top_level_num_nodes(num_hypernodes), - _current_num_nodes(num_hypernodes), + _current_partition_is_best(true), + _best_partition(num_hypernodes, kInvalidPartition), + _current_partition(num_hypernodes, kInvalidPartition), _gain(context), _active_nodes(), - _active_node_was_moved(num_hypernodes), - _gains_and_target(precomputed ? num_hypernodes : 0) { } + _gains_and_target(precomputed ? num_hypernodes : 0), + _next_active(num_hypernodes), + _visited_he(num_hyperedges) { } explicit JetRefiner(const HypernodeID num_hypernodes, const HyperedgeID num_hyperedges, @@ -126,8 +129,23 @@ class JetRefiner final : public IRefiner { void initializeImpl(mt_kahypar_partitioned_hypergraph_t&) final; + void computeActiveNodesFromGraph(const PartitionedHypergraph& hypergraph); + + void computeActiveNodesFromVector(const PartitionedHypergraph& hypergraph, + const parallel::scalable_vector& refinement_nodes); + + void computeActiveNodesFromPreviousRound(const PartitionedHypergraph& hypergraph); + + // ! Applied during computation of active nodes. If precomputed, applies the first JET filter + template + void processNode(const PartitionedHypergraph& hypergraph, const HypernodeID hn, F add_node_fn, const bool top_level); + void recomputePenalties(const PartitionedHypergraph& hypergraph, bool did_rebalance); + void storeCurrentPartition(const PartitionedHypergraph& hypergraph, parallel::scalable_vector& parts); + + void rollbackToBestPartition(PartitionedHypergraph& hypergraph); + void rebalance(PartitionedHypergraph& hypergraph, Metrics& current_metrics, double time_limit); template @@ -160,11 +178,14 @@ class JetRefiner final : public IRefiner { GainCache& _gain_cache; PartitionID _current_k; HypernodeID _top_level_num_nodes; - HypernodeID _current_num_nodes; + bool _current_partition_is_best; + parallel::scalable_vector _best_partition; + parallel::scalable_vector _current_partition; GainCalculator _gain; ActiveNodes _active_nodes; - kahypar::ds::FastResetFlagArray<> _active_node_was_moved; parallel::scalable_vector> _gains_and_target; + ds::ThreadSafeFastResetFlagArray<> _next_active; + kahypar::ds::FastResetFlagArray<> _visited_he; }; template diff --git a/tests/partition/refinement/jet_refiner_test.cc b/tests/partition/refinement/jet_refiner_test.cc index 5f150c261..3dfa3e767 100644 --- a/tests/partition/refinement/jet_refiner_test.cc +++ b/tests/partition/refinement/jet_refiner_test.cc @@ -39,27 +39,29 @@ using ::testing::Test; namespace mt_kahypar { -template +template struct TestConfig { }; -template -struct TestConfig { +template +struct TestConfig { using TypeTraits = TypeTraitsT; using GainTypes = Km1GainTypes; using Refiner = JetRefiner; static constexpr PartitionID K = k; static constexpr Objective OBJECTIVE = Objective::km1; static constexpr JetAlgorithm JET_ALGO = JetAlgorithm::greedy_unordered; + static constexpr bool execute_sequential = sequential; }; -template -struct TestConfig { +template +struct TestConfig { using TypeTraits = TypeTraitsT; using GainTypes = CutGainTypes; using Refiner = JetRefiner; static constexpr PartitionID K = k; static constexpr Objective OBJECTIVE = Objective::cut; static constexpr JetAlgorithm JET_ALGO = JetAlgorithm::greedy_unordered; + static constexpr bool execute_sequential = sequential; }; template @@ -109,6 +111,7 @@ class AJetRefiner : public Test { context.refinement.jet.algorithm = Config::JET_ALGO; context.initial_partitioning.refinement.jet.algorithm = Config::JET_ALGO; context.initial_partitioning.refinement.jet.vertex_locking = false; + context.initial_partitioning.refinement.jet.execute_sequential = Config::execute_sequential; // Read hypergraph hypergraph = io::readInputFile( @@ -150,18 +153,23 @@ size_t AJetRefiner::num_threads = HardwareTopology::instance().num_cpus( static constexpr double EPS = 0.05; typedef ::testing::Types, + TestConfig, TestConfig, TestConfig, TestConfig, + TestConfig, TestConfig, TestConfig, TestConfig, + TestConfig, TestConfig, TestConfig, TestConfig, + TestConfig, TestConfig, TestConfig ENABLE_N_LEVEL(COMMA TestConfig) + ENABLE_N_LEVEL(COMMA TestConfig) ENABLE_N_LEVEL(COMMA TestConfig) ENABLE_N_LEVEL(COMMA TestConfig) ENABLE_N_LEVEL(COMMA TestConfig) From a6f58be52796fea9a0e2a30d42685bb84c80a2b4 Mon Sep 17 00:00:00 2001 From: Nikolai Maas Date: Thu, 27 Jul 2023 16:51:30 +0200 Subject: [PATCH 007/152] jet refiner fixes --- mt-kahypar/partition/refinement/jet/jet_refiner.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/mt-kahypar/partition/refinement/jet/jet_refiner.cpp b/mt-kahypar/partition/refinement/jet/jet_refiner.cpp index 9635d8a92..66a880ff9 100644 --- a/mt-kahypar/partition/refinement/jet/jet_refiner.cpp +++ b/mt-kahypar/partition/refinement/jet/jet_refiner.cpp @@ -162,7 +162,7 @@ namespace mt_kahypar { if constexpr (precomputed) { const PartitionID from = hypergraph.partID(hn); const auto [gain, to] = _gains_and_target[hn]; - Gain total_gain = 0; + /* Gain total_gain = 0; for (const HyperedgeID& he : hypergraph.incidentEdges(hn)) { HypernodeID pin_count_in_from_part_after = 0; HypernodeID pin_count_in_to_part_after = 1; @@ -181,9 +181,10 @@ namespace mt_kahypar { } } } - total_gain += AttributedGains::gain(he, hypergraph.edgeWeight(he), hypergraph.edgeSize(he), - pin_count_in_from_part_after, pin_count_in_to_part_after); - } + // TODO: does not compile with new sync update + // total_gain += AttributedGains::gain(he, hypergraph.edgeWeight(he), hypergraph.edgeSize(he), + // pin_count_in_from_part_after, pin_count_in_to_part_after); + } */ if (gain < 0) { changeNodePart(hypergraph, hn, from, to, objective_delta); @@ -218,7 +219,7 @@ namespace mt_kahypar { auto recompute = [&](const HypernodeID hn) { const bool node_was_moved = (hypergraph.partID(hn) != current_parts[hn]); if (node_was_moved) { - _gain_cache.recomputePenaltyTermEntry(hypergraph, hn); + _gain_cache.recomputePenaltyTerm(hypergraph, hn); } else { ASSERT(_gain_cache.penaltyTerm(hn, hypergraph.partID(hn)) == _gain_cache.recomputePenaltyTerm(hypergraph, hn)); @@ -384,7 +385,7 @@ namespace mt_kahypar { if constexpr (precomputed) { RatingMap& tmp_scores = _gain.localScores(); Gain isolated_block_gain = 0; - _gain.precomputeGains(hypergraph, hn, tmp_scores, isolated_block_gain); + _gain.precomputeGains(hypergraph, hn, tmp_scores, isolated_block_gain, false); Move best_move = _gain.computeMaxGainMoveForScores(hypergraph, tmp_scores, isolated_block_gain, hn, false, false, true); tmp_scores.clear(); From c998bdcecd007eaa0a460a54c77732af499f0d7c Mon Sep 17 00:00:00 2001 From: Nikolai Maas Date: Tue, 16 May 2023 11:03:24 +0200 Subject: [PATCH 008/152] use factory for rebalancer --- .../partition/refinement/jet/jet_refiner.cpp | 30 ++++++++++++++++--- .../partition/refinement/jet/jet_refiner.h | 25 +++++----------- .../refinement/rebalancing/rebalancer.cpp | 1 + .../refinement/rebalancing/rebalancer.h | 13 ++++++++ .../partition/refinement/jet_refiner_test.cc | 2 ++ 5 files changed, 50 insertions(+), 21 deletions(-) diff --git a/mt-kahypar/partition/refinement/jet/jet_refiner.cpp b/mt-kahypar/partition/refinement/jet/jet_refiner.cpp index 66a880ff9..ca5fe729d 100644 --- a/mt-kahypar/partition/refinement/jet/jet_refiner.cpp +++ b/mt-kahypar/partition/refinement/jet/jet_refiner.cpp @@ -32,6 +32,7 @@ #include "mt-kahypar/partition/metrics.h" #include "mt-kahypar/partition/refinement/gains/gain_definitions.h" #include "mt-kahypar/partition/refinement/rebalancing/rebalancer.h" +#include "mt-kahypar/partition/factories.h" #include "mt-kahypar/utils/randomize.h" #include "mt-kahypar/utils/utilities.h" #include "mt-kahypar/utils/timer.h" @@ -40,6 +41,28 @@ namespace mt_kahypar { + template + JetRefiner::JetRefiner(const HypernodeID num_hypernodes, + const HyperedgeID num_hyperedges, + const Context& context, + GainCache& gain_cache) : + _context(context), + _gain_cache(gain_cache), + _current_k(context.partition.k), + _top_level_num_nodes(num_hypernodes), + _current_partition_is_best(true), + _best_partition(num_hypernodes, kInvalidPartition), + _current_partition(num_hypernodes, kInvalidPartition), + _gain(context), + _active_nodes(), + _gains_and_target(precomputed ? num_hypernodes : 0), + _next_active(num_hypernodes), + _visited_he(num_hyperedges), + _rebalancer(nullptr) { + _rebalancer = RebalancerFactory::getInstance().createObject( + _context.refinement.rebalancer, _context); + } + template bool JetRefiner::refineImpl( mt_kahypar_partitioned_hypergraph_t& phg, @@ -208,8 +231,8 @@ namespace mt_kahypar { } template - void JetRefiner::initializeImpl(mt_kahypar_partitioned_hypergraph_t&) { - // PartitionedHypergraph& hypergraph = utils::cast(phg); + void JetRefiner::initializeImpl(mt_kahypar_partitioned_hypergraph_t& phg) { + _rebalancer->initialize(phg); } template @@ -444,9 +467,8 @@ namespace mt_kahypar { void JetRefiner::rebalance(PartitionedHypergraph& hypergraph, Metrics& current_metrics, double time_limit) { ASSERT(!_context.partition.deterministic); - Rebalancer rebalancer(_context); mt_kahypar_partitioned_hypergraph_t phg = utils::partitioned_hg_cast(hypergraph); - rebalancer.refine(phg, {}, current_metrics, time_limit); + _rebalancer->refine(phg, {}, current_metrics, time_limit); } namespace { diff --git a/mt-kahypar/partition/refinement/jet/jet_refiner.h b/mt-kahypar/partition/refinement/jet/jet_refiner.h index 802216a1a..0805d8e6f 100644 --- a/mt-kahypar/partition/refinement/jet/jet_refiner.h +++ b/mt-kahypar/partition/refinement/jet/jet_refiner.h @@ -26,6 +26,8 @@ #pragma once +#include + #include "kahypar/datastructure/fast_reset_flag_array.h" #include "mt-kahypar/datastructures/thread_safe_fast_reset_flag_array.h" @@ -54,27 +56,15 @@ class JetRefiner final : public IRefiner { public: explicit JetRefiner(const HypernodeID num_hypernodes, const HyperedgeID num_hyperedges, - const Context& context, - GainCache& gain_cache) : - _context(context), - _gain_cache(gain_cache), - _current_k(context.partition.k), - _top_level_num_nodes(num_hypernodes), - _current_partition_is_best(true), - _best_partition(num_hypernodes, kInvalidPartition), - _current_partition(num_hypernodes, kInvalidPartition), - _gain(context), - _active_nodes(), - _gains_and_target(precomputed ? num_hypernodes : 0), - _next_active(num_hypernodes), - _visited_he(num_hyperedges) { } + const Context &context, + GainCache &gain_cache); explicit JetRefiner(const HypernodeID num_hypernodes, const HyperedgeID num_hyperedges, - const Context& context, + const Context &context, gain_cache_t gain_cache) : JetRefiner(num_hypernodes, num_hyperedges, context, - GainCachePtr::cast(gain_cache)) { } + GainCachePtr::cast(gain_cache)) {} JetRefiner(const JetRefiner&) = delete; JetRefiner(JetRefiner&&) = delete; @@ -127,7 +117,7 @@ class JetRefiner final : public IRefiner { void initializeActiveNodes(PartitionedHypergraph& hypergraph, const parallel::scalable_vector& refinement_nodes); - void initializeImpl(mt_kahypar_partitioned_hypergraph_t&) final; + void initializeImpl(mt_kahypar_partitioned_hypergraph_t& phg) final; void computeActiveNodesFromGraph(const PartitionedHypergraph& hypergraph); @@ -186,6 +176,7 @@ class JetRefiner final : public IRefiner { parallel::scalable_vector> _gains_and_target; ds::ThreadSafeFastResetFlagArray<> _next_active; kahypar::ds::FastResetFlagArray<> _visited_he; + std::unique_ptr _rebalancer; }; template diff --git a/mt-kahypar/partition/refinement/rebalancing/rebalancer.cpp b/mt-kahypar/partition/refinement/rebalancing/rebalancer.cpp index 686c5c220..be808d60c 100644 --- a/mt-kahypar/partition/refinement/rebalancing/rebalancer.cpp +++ b/mt-kahypar/partition/refinement/rebalancing/rebalancer.cpp @@ -48,6 +48,7 @@ namespace mt_kahypar { Metrics& best_metrics, double) { PartitionedHypergraph& phg = utils::cast(hypergraph); + resizeDataStructuresForCurrentK(); // If partition is imbalanced, rebalancer is activated bool improvement = false; if ( !metrics::isBalanced(phg, _context) ) { diff --git a/mt-kahypar/partition/refinement/rebalancing/rebalancer.h b/mt-kahypar/partition/refinement/rebalancing/rebalancer.h index 84fcb5c92..8502811f6 100644 --- a/mt-kahypar/partition/refinement/rebalancing/rebalancer.h +++ b/mt-kahypar/partition/refinement/rebalancing/rebalancer.h @@ -69,6 +69,7 @@ class Rebalancer final : public IRefiner { explicit Rebalancer(const Context& context) : _context(context), + _current_k(context.partition.k), _gain(context), _part_weights(_context.partition.k) { } @@ -115,7 +116,19 @@ class Rebalancer final : public IRefiner { return false; } + + void resizeDataStructuresForCurrentK() { + // If the number of blocks changes, we resize data structures + // (can happen during deep multilevel partitioning) + if ( _current_k != _context.partition.k ) { + _current_k = _context.partition.k; + _gain.changeNumberOfBlocks(_current_k); + _part_weights = parallel::scalable_vector(_context.partition.k); + } + } + const Context& _context; + PartitionID _current_k; GainCalculator _gain; parallel::scalable_vector _part_weights; }; diff --git a/tests/partition/refinement/jet_refiner_test.cc b/tests/partition/refinement/jet_refiner_test.cc index 3dfa3e767..ba1d6d09d 100644 --- a/tests/partition/refinement/jet_refiner_test.cc +++ b/tests/partition/refinement/jet_refiner_test.cc @@ -109,6 +109,7 @@ class AJetRefiner : public Test { // Jet context.refinement.jet.algorithm = Config::JET_ALGO; + context.refinement.rebalancer = RebalancingAlgorithm::simple_rebalancer; context.initial_partitioning.refinement.jet.algorithm = Config::JET_ALGO; context.initial_partitioning.refinement.jet.vertex_locking = false; context.initial_partitioning.refinement.jet.execute_sequential = Config::execute_sequential; @@ -129,6 +130,7 @@ class AJetRefiner : public Test { void initialPartition() { Context ip_context(context); ip_context.refinement.label_propagation.algorithm = LabelPropagationAlgorithm::do_nothing; + ip_context.refinement.jet.algorithm = JetAlgorithm::do_nothing; InitialPartitioningDataContainer ip_data(partitioned_hypergraph, ip_context); ip_data_container_t* ip_data_ptr = ip::to_pointer(ip_data); BFSInitialPartitioner initial_partitioner( From 3d17dcc02583981364a6be29d44d3ec1293d471e Mon Sep 17 00:00:00 2001 From: Nikolai Maas Date: Tue, 23 May 2023 15:00:10 +0200 Subject: [PATCH 009/152] jet rebalancer initial implementation and tests --- .../datastructures/concurrent_bucket_map.h | 31 +- mt-kahypar/io/command_line_options.cpp | 27 +- mt-kahypar/partition/context.cpp | 13 + mt-kahypar/partition/context.h | 12 + mt-kahypar/partition/context_enum_classes.cpp | 3 + mt-kahypar/partition/context_enum_classes.h | 1 + .../partition/refinement/CMakeLists.txt | 1 + .../refinement/rebalancing/jet_rebalancer.cpp | 392 ++++++++++++++++++ .../refinement/rebalancing/jet_rebalancer.h | 222 ++++++++++ .../refinement/rebalancing/rebalancer.h | 4 + tests/partition/refinement/CMakeLists.txt | 1 + .../refinement/jet_rebalancer_test.cc | 145 +++++++ 12 files changed, 842 insertions(+), 10 deletions(-) create mode 100644 mt-kahypar/partition/refinement/rebalancing/jet_rebalancer.cpp create mode 100644 mt-kahypar/partition/refinement/rebalancing/jet_rebalancer.h create mode 100644 tests/partition/refinement/jet_rebalancer_test.cc diff --git a/mt-kahypar/datastructures/concurrent_bucket_map.h b/mt-kahypar/datastructures/concurrent_bucket_map.h index b21051197..7e8a0f4a7 100644 --- a/mt-kahypar/datastructures/concurrent_bucket_map.h +++ b/mt-kahypar/datastructures/concurrent_bucket_map.h @@ -72,6 +72,15 @@ class ConcurrentBucketMap { _num_buckets(align_to_next_power_of_two( BUCKET_FACTOR * std::thread::hardware_concurrency())), _mod_mask(_num_buckets - 1), + _estimated_num_insertions(0), + _spin_locks(_num_buckets), + _buckets(_num_buckets) { } + + ConcurrentBucketMap(const size_t bucket_factor) : + _num_buckets(align_to_next_power_of_two( + bucket_factor * std::thread::hardware_concurrency())), + _mod_mask(_num_buckets - 1), + _estimated_num_insertions(0), _spin_locks(_num_buckets), _buckets(_num_buckets) { } @@ -79,11 +88,11 @@ class ConcurrentBucketMap { ConcurrentBucketMap & operator= (const ConcurrentBucketMap &) = delete; ConcurrentBucketMap(ConcurrentBucketMap&& other) : - _num_buckets(align_to_next_power_of_two( - BUCKET_FACTOR * std::thread::hardware_concurrency())), + _num_buckets(other._num_buckets), _mod_mask(_num_buckets - 1), + _estimated_num_insertions(other._estimated_num_insertions), _spin_locks(_num_buckets), - _buckets(std::move(other._buffer)) { } + _buckets(std::move(other._buckets)) { } template void doParallelForAllBuckets(const F& f) { @@ -106,12 +115,15 @@ class ConcurrentBucketMap { // ! Reserves memory in each bucket such that the estimated number of insertions // ! can be handled without the need (with high probability) of expensive bucket resizing. void reserve_for_estimated_number_of_insertions(const size_t estimated_num_insertions) { - // ! Assumption is that keys are evenly distributed among buckets (with a small buffer) - const size_t estimated_bucket_size = std::max( - static_cast( 1.5 * estimated_num_insertions ) / _num_buckets, UL(1)); - tbb::parallel_for(UL(0), _num_buckets, [&](const size_t i) { - _buckets[i].reserve(estimated_bucket_size); - }); + if (estimated_num_insertions > _estimated_num_insertions) { + _estimated_num_insertions = estimated_num_insertions; + // ! Assumption is that keys are evenly distributed among buckets (with a small buffer) + const size_t estimated_bucket_size = std::max( + static_cast( 1.5 * estimated_num_insertions ) / _num_buckets, UL(1)); + tbb::parallel_for(UL(0), _num_buckets, [&](const size_t i) { + _buckets[i].reserve(estimated_bucket_size); + }); + } } // ! Inserts a key-value pair @@ -153,6 +165,7 @@ class ConcurrentBucketMap { const size_t _num_buckets; const size_t _mod_mask; + size_t _estimated_num_insertions; std::vector _spin_locks; parallel::scalable_vector _buckets; }; diff --git a/mt-kahypar/io/command_line_options.cpp b/mt-kahypar/io/command_line_options.cpp index cbe5c0b68..71e80f386 100644 --- a/mt-kahypar/io/command_line_options.cpp +++ b/mt-kahypar/io/command_line_options.cpp @@ -533,7 +533,32 @@ namespace mt_kahypar { })->default_value("do_nothing"), "Rebalancer Algorithm:\n" "- simple_rebalancer\n" - "- do_nothing"); + "- jet_rebalancer\n" + "- do_nothing") + ((initial_partitioning ? "i-r-jetr-num-weak-iterations" : "r-jetr-num-weak-iterations"), + po::value((initial_partitioning ? &context.initial_partitioning.refinement.jet_rebalancing.num_weak_iterations : + &context.refinement.jet_rebalancing.num_weak_iterations))->value_name("")->default_value(2), + "Number of weak iterations performed by jet rebalancer.") + ((initial_partitioning ? "i-r-jetr-num-strong-iterations" : "r-jetr-num-strong-iterations"), + po::value((initial_partitioning ? &context.initial_partitioning.refinement.jet_rebalancing.num_strong_iterations : + &context.refinement.jet_rebalancing.num_strong_iterations))->value_name("")->default_value(3), + "Number of strong iterations performed by jet rebalancer.") + ((initial_partitioning ? "i-r-jetr-heavy-vertex-exclusion-factor" : "r-jetr-heavy-vertex-exclusion-factor"), + po::value((initial_partitioning ? &context.initial_partitioning.refinement.jet_rebalancing.heavy_vertex_exclusion_factor : + &context.refinement.jet_rebalancing.heavy_vertex_exclusion_factor))->value_name("")->default_value(1.5), + "Heavy vertices are not considered by the rebalancer (factor relative to imbalance of block).") + ((initial_partitioning ? "i-r-jetr-relative-deadzone-size" : "r-jetr-relative-deadzone-size"), + po::value((initial_partitioning ? &context.initial_partitioning.refinement.jet_rebalancing.relative_deadzone_size : + &context.refinement.jet_rebalancing.relative_deadzone_size))->value_name("")->default_value(1.0), + "Size of deadzone relative to allowed imbalance.") + ((initial_partitioning ? "i-r-jetr-use-greedy-balanced-instead-of-strong-iteration" : "r-jetr-use-greedy-balanced-instead-of-strong-iteration"), + po::value((initial_partitioning ? &context.initial_partitioning.refinement.jet_rebalancing.use_greedy_balanced_instead_of_strong_iteration : + &context.refinement.jet_rebalancing.use_greedy_balanced_instead_of_strong_iteration))->value_name("")->default_value(false), + "Size of deadzone relative to allowed imbalance.") + ((initial_partitioning ? "i-r-jetr-greedy-balanced-use-deadzone" : "r-jetr-greedy-balanced-use-deadzone"), + po::value((initial_partitioning ? &context.initial_partitioning.refinement.jet_rebalancing.greedy_balanced_use_deadzone : + &context.refinement.jet_rebalancing.greedy_balanced_use_deadzone))->value_name("")->default_value(true), + "Size of deadzone relative to allowed imbalance."); return options; } diff --git a/mt-kahypar/partition/context.cpp b/mt-kahypar/partition/context.cpp index d34e1fea7..0d5c1b000 100644 --- a/mt-kahypar/partition/context.cpp +++ b/mt-kahypar/partition/context.cpp @@ -194,6 +194,16 @@ namespace mt_kahypar { return out; } + std::ostream& operator<<(std::ostream& out, const JetRebalancingParameters& params) { + out << " Jet Rebalancing Parameters: \n"; + out << " Weak Iterations: " << params.num_weak_iterations << std::endl; + out << " Strong Iterations: " << params.num_strong_iterations << std::endl; + out << " Heavy Vertex Exclusion Factor: " << params.heavy_vertex_exclusion_factor << std::endl; + out << " Relative Deadzone Size: " << params.relative_deadzone_size << std::endl; + out << " Use Greedy Balanced: " << std::boolalpha << params.use_greedy_balanced_instead_of_strong_iteration << std::endl; + return out; + } + std::ostream& operator<<(std::ostream& out, const DeterministicRefinementParameters& params) { out << " Number of sub-rounds for Sync LP: " << params.num_sub_rounds_sync_lp << std::endl; out << " Use active node set: " << std::boolalpha << params.use_active_node_set << std::endl; @@ -209,6 +219,9 @@ namespace mt_kahypar { str << " Relative Improvement Threshold: " << params.relative_improvement_threshold << std::endl; str << " Maximum Batch Size: " << params.max_batch_size << std::endl; str << " Min Border Vertices Per Thread: " << params.min_border_vertices_per_thread << std::endl; + if (params.rebalancer == RebalancingAlgorithm::jet_rebalancer) { + str << "\n" << params.jet_rebalancing; + } str << "\n" << params.label_propagation; str << "\n" << params.jet; str << "\n" << params.fm; diff --git a/mt-kahypar/partition/context.h b/mt-kahypar/partition/context.h index ef70a34ed..bcb3ed7b1 100644 --- a/mt-kahypar/partition/context.h +++ b/mt-kahypar/partition/context.h @@ -202,6 +202,17 @@ struct FlowParameters { std::ostream& operator<<(std::ostream& out, const FlowParameters& params); +struct JetRebalancingParameters { + size_t num_weak_iterations = 2; + size_t num_strong_iterations = 3; + double heavy_vertex_exclusion_factor = 1.5; + double relative_deadzone_size = 1.0; + bool use_greedy_balanced_instead_of_strong_iteration = false; + bool greedy_balanced_use_deadzone = true; +}; + +std::ostream& operator<<(std::ostream& out, const JetRebalancingParameters& params); + struct DeterministicRefinementParameters { size_t num_sub_rounds_sync_lp = 5; bool use_active_node_set = false; @@ -218,6 +229,7 @@ struct RefinementParameters { NLevelGlobalFMParameters global_fm; FlowParameters flows; RebalancingAlgorithm rebalancer = RebalancingAlgorithm::do_nothing; + JetRebalancingParameters jet_rebalancing; bool refine_until_no_improvement = false; double relative_improvement_threshold = 0.0; size_t max_batch_size = std::numeric_limits::max(); diff --git a/mt-kahypar/partition/context_enum_classes.cpp b/mt-kahypar/partition/context_enum_classes.cpp index c0403b313..c78b85775 100644 --- a/mt-kahypar/partition/context_enum_classes.cpp +++ b/mt-kahypar/partition/context_enum_classes.cpp @@ -259,6 +259,7 @@ namespace mt_kahypar { std::ostream & operator<< (std::ostream& os, const RebalancingAlgorithm& algo) { switch (algo) { case RebalancingAlgorithm::simple_rebalancer: return os << "simple_rebalancer"; + case RebalancingAlgorithm::jet_rebalancer: return os << "jet_rebalancer"; case RebalancingAlgorithm::do_nothing: return os << "do_nothing"; // omit default case to trigger compiler warning for missing cases } @@ -487,6 +488,8 @@ namespace mt_kahypar { RebalancingAlgorithm rebalancingAlgorithmFromString(const std::string& type) { if (type == "simple_rebalancer") { return RebalancingAlgorithm::simple_rebalancer; + } else if (type == "jet_rebalancer") { + return RebalancingAlgorithm::jet_rebalancer; } else if (type == "do_nothing") { return RebalancingAlgorithm::do_nothing; } diff --git a/mt-kahypar/partition/context_enum_classes.h b/mt-kahypar/partition/context_enum_classes.h index b8b033a50..0d8f95834 100644 --- a/mt-kahypar/partition/context_enum_classes.h +++ b/mt-kahypar/partition/context_enum_classes.h @@ -171,6 +171,7 @@ enum class FlowAlgorithm : uint8_t { enum class RebalancingAlgorithm : uint8_t { simple_rebalancer, + jet_rebalancer, do_nothing }; diff --git a/mt-kahypar/partition/refinement/CMakeLists.txt b/mt-kahypar/partition/refinement/CMakeLists.txt index 2643931eb..ebad948c0 100644 --- a/mt-kahypar/partition/refinement/CMakeLists.txt +++ b/mt-kahypar/partition/refinement/CMakeLists.txt @@ -6,6 +6,7 @@ set(RefinementSources label_propagation/label_propagation_refiner.cpp jet/jet_refiner.cpp rebalancing/rebalancer.cpp + rebalancing/jet_rebalancer.cpp deterministic/deterministic_label_propagation.cpp flows/refiner_adapter.cpp flows/problem_construction.cpp diff --git a/mt-kahypar/partition/refinement/rebalancing/jet_rebalancer.cpp b/mt-kahypar/partition/refinement/rebalancing/jet_rebalancer.cpp new file mode 100644 index 000000000..44dade94e --- /dev/null +++ b/mt-kahypar/partition/refinement/rebalancing/jet_rebalancer.cpp @@ -0,0 +1,392 @@ +/******************************************************************************* + * MIT License + * + * This file is part of Mt-KaHyPar. + * + * Copyright (C) 2023 Nikolai Maas + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + ******************************************************************************/ + +#include "mt-kahypar/partition/refinement/rebalancing/jet_rebalancer.h" + +#include + +#include "tbb/parallel_for.h" + +#include "mt-kahypar/datastructures/streaming_vector.h" +#include "mt-kahypar/definitions.h" +#include "mt-kahypar/parallel/parallel_prefix_sum.h" +#include "mt-kahypar/partition/metrics.h" +#include "mt-kahypar/partition/refinement/gains/gain_definitions.h" +#include "mt-kahypar/utils/timer.h" +#include "mt-kahypar/utils/cast.h" +#include "mt-kahypar/utils/randomize.h" + +namespace mt_kahypar { + template + bool JetRebalancer::refineImpl(mt_kahypar_partitioned_hypergraph_t& hypergraph, + const vec&, + Metrics& best_metrics, + double) { + PartitionedHypergraph& phg = utils::cast(hypergraph); + resizeDataStructuresForCurrentK(); + // If partition is imbalanced, rebalancer is activated + bool improvement = false; + if ( !metrics::isBalanced(phg, _context) ) { + _gain.reset(); + initializeDataStructures(phg); + + for (size_t i = 0; _num_imbalanced_blocks > 0 && i < _context.refinement.jet_rebalancing.num_weak_iterations; ++i) { + weakRebalancingRound(phg); + } + + for (size_t i = 0; _num_imbalanced_blocks > 0 && i < _context.refinement.jet_rebalancing.num_strong_iterations; ++i) { + if (_context.refinement.jet_rebalancing.use_greedy_balanced_instead_of_strong_iteration) { + weakRebalancingRound(phg); + } else { + strongRebalancingRound(phg); + } + } + + if (_gain_cache.isInitialized()) { + phg.doParallelForAllNodes([&](const HypernodeID hn) { + if (_node_was_moved[hn]) { + _gain_cache.recomputePenaltyTerm(phg, hn); + _node_was_moved[hn] = uint8_t(false); + } + }); + } + + // Update metrics statistics + Gain delta = _gain.delta(); + HEAVY_REFINEMENT_ASSERT(phg.checkTrackedPartitionInformation(_gain_cache)); + HEAVY_REFINEMENT_ASSERT(best_metrics.quality + delta == metrics::quality(phg, _context), + V(best_metrics.quality) << V(delta) << V(metrics::quality(phg, _context))); + best_metrics.quality += delta; + best_metrics.imbalance = metrics::imbalance(phg, _context); + DBG << "[REBALANCE] " << V(delta) << " imbalance=" << best_metrics.imbalance; + improvement = delta < 0; + } + return improvement; + } + + template + template + void JetRebalancer::weakRebalancingRound(PartitionedHypergraph& phg) { + bool use_deadzone = !ensure_balanced_moves || _context.refinement.jet_rebalancing.greedy_balanced_use_deadzone; + insertNodesIntoBuckets(phg, [&](const HypernodeID hn) { + return computeGainAndTargetPart(phg, hn, false, false, use_deadzone).first; + }); + + // move nodes greedily to best part + processBuckets(phg, [&](const HypernodeID hn, const PartitionID from, bool is_retry) { + auto [_, to] = computeGainAndTargetPart(phg, hn, true, is_retry, use_deadzone); + return changeNodePart(phg, hn, from, to, ensure_balanced_moves); + }, ensure_balanced_moves, false); + } + + template + void JetRebalancer::strongRebalancingRound(PartitionedHypergraph& phg) { + insertNodesIntoBuckets(phg, [&](const HypernodeID hn) { + return computeAverageGain(phg, hn); + }); + + // collect nodes into unordered list + ds::StreamingVector tmp_moved_nodes; + processBuckets(phg, [&](const HypernodeID hn, const PartitionID, bool) { + tmp_moved_nodes.stream(hn); + return true; + }, false, true); + vec moved_nodes = tmp_moved_nodes.copy_parallel(); + + // compute prefix sum over the weights to quickly find appropriate range for each block + vec node_weights(moved_nodes.size()); + tbb::parallel_for(0UL, moved_nodes.size(), [&](const size_t i) { + node_weights[i] = phg.nodeWeight(moved_nodes[i]); + }); + parallel::TBBPrefixSum node_weight_prefix_sum(node_weights); + tbb::parallel_scan(tbb::blocked_range(ID(0), node_weights.size()), node_weight_prefix_sum); + + // compute assignment of nodes to blocks + vec block_ranges(_context.partition.k + 1); + double total_free_capacity = 0; + for (PartitionID block = 0; block < _context.partition.k; ++block) { + total_free_capacity += std::max(deadzoneForPart(block) - phg.partWeight(block), 0); + } + for (PartitionID block = 0; block < _context.partition.k; ++block) { + const HypernodeWeight capacity = std::max(deadzoneForPart(block) - phg.partWeight(block), 0); + if (capacity > 0) { + const double fraction_of_nodes = static_cast(capacity) / total_free_capacity; + const HypernodeWeight added_weight = fraction_of_nodes * node_weight_prefix_sum.total_sum(); + const HypernodeWeight start_weight = node_weight_prefix_sum[block_ranges[block]]; + // find correct index via binary search + auto end_it = std::lower_bound(node_weights.cbegin() + block_ranges[block], + node_weights.cend(), start_weight + added_weight); + size_t end_of_range = std::min(static_cast(end_it - node_weights.cbegin()) + 1, node_weights.size()); + + ASSERT(node_weight_prefix_sum[end_of_range - 1] <= start_weight + added_weight && + (end_of_range == node_weights.size() || node_weight_prefix_sum[end_of_range] >= start_weight + added_weight)); + ASSERT(end_of_range >= block_ranges[block]); + block_ranges[block + 1] = end_of_range; + } else { + block_ranges[block + 1] = block_ranges[block]; + } + } + ASSERT(block_ranges.back() == moved_nodes.size()); + + // move the nodes according to the assignment + tbb::parallel_for(static_cast(0), _context.partition.k, [&](const PartitionID block) { + const size_t start = block_ranges[block]; + const size_t end = block_ranges[block + 1]; + if (end > start) { + tbb::parallel_for(start, end, [&](const size_t i) { + const HypernodeID hn = moved_nodes[i]; + changeNodePart(phg, hn, phg.partID(hn), block, false); + }); + } + }); + updateImbalance(phg); + } + + template + template + void JetRebalancer::insertNodesIntoBuckets(PartitionedHypergraph& phg, F compute_gain_fn) { + parallel::scalable_vector bucket_counts(NUM_BUCKETS); // TODO: remove + + phg.doParallelForAllNodes([&](const HypernodeID hn) { + const PartitionID from = phg.partID(hn); + const HypernodeWeight weight = phg.nodeWeight(hn); + if (imbalance(from) > 0 && mayMoveNode(from, weight)) { + auto& local_weights = _local_bucket_weights.local(); + const size_t bucket = getBucketID(compute_gain_fn(hn)); + bucket_counts[bucket].fetch_add(1); // TODO: remove + _buckets[bucket].insert(hn, HypernodeID(hn)); + local_weights[from * NUM_BUCKETS + bucket] += weight; + } + }); + + // sum the bucket weight, so we know which buckets should be moved completely + auto add_range_fn = [&](size_t start, size_t end) { + for (const auto& local_weights: _local_bucket_weights) { + ASSERT(_bucket_weights.size() == local_weights.size()); + for (size_t i = start; i < end; ++i) { + ASSERT(i < local_weights.size()); + _bucket_weights[i] += local_weights[i]; + } + } + }; + if (_context.partition.k < 64) { + add_range_fn(0, _bucket_weights.size()); + } else { + tbb::parallel_for(static_cast(0), _context.partition.k, [&](const PartitionID k) { + add_range_fn(k * NUM_BUCKETS, (k + 1) * NUM_BUCKETS); + }); + } + } + + template + template + void JetRebalancer::processBuckets(PartitionedHypergraph& phg, + F move_node_fn, + bool retry_invalid_moves, + bool update_local_part_weights) { + + // process buckets in ascending order of loss value, moving the nodes to a block with free capacity + tbb::enumerable_thread_specific> retry_nodes; + for (size_t bucket = 0; bucket < _buckets.size(); ++bucket) { + auto process_current_node = [&](HypernodeID hn, PartitionID from, bool is_retry) { + bool success = true; + if (_bucket_weights[from * NUM_BUCKETS + bucket] < imbalance(from)) { + // all nodes of the current bucket should be moved + success = move_node_fn(hn, from, is_retry); + if (update_local_part_weights && success) { + _part_weights[from].fetch_sub(phg.nodeWeight(hn), std::memory_order_relaxed); + } + } else if (imbalance(from) > 0) { + // the nodes of the current bucket will restore the balance of the block, thus we need to + // directly apply the weight updates so that we don't remove more nodes than necessary + const HypernodeWeight hn_weight = phg.nodeWeight(hn); + const HypernodeWeight old_block_weight = _part_weights[from].fetch_sub(hn_weight, std::memory_order_relaxed); + bool moved = false; + if (old_block_weight > _context.partition.max_part_weights[from]) { + // block is still imbalanced, try to move the node + moved = move_node_fn(hn, from, is_retry); + success = moved; + } + if (!moved) { + _part_weights[from].fetch_add(hn_weight, std::memory_order_relaxed); + } + } + return success; + }; + + BucketMap& map = _buckets[bucket]; + map.doParallelForAllBuckets([&](const size_t j) { + vec& local_retry_nodes = retry_nodes.local(); + const auto& batch = map.getBucket(j); + // handle all nodes in the bucket + for (const HypernodeID hn: batch) { + const PartitionID from = phg.partID(hn); + bool success = process_current_node(hn, from, false); + if (retry_invalid_moves && !success) { + local_retry_nodes.push_back(hn); + } + } + + if (retry_invalid_moves) { + for (const HypernodeID hn: local_retry_nodes) { + const PartitionID from = phg.partID(hn); + bool success = false; + // retry ten times (in practice, first try should succeed in almost all cases) + for (int i = 0; !success && i < 10; ++i) { + success = process_current_node(hn, from, true); + } + } + local_retry_nodes.clear(); + } + }); + + updateImbalance(phg, !update_local_part_weights); + if (_num_imbalanced_blocks == 0) { + break; + } + } + + for (size_t bucket = 0; bucket < _buckets.size(); ++bucket) { + _buckets[bucket].clearParallel(); + } + } + + template + std::pair JetRebalancer::computeGainAndTargetPart(const PartitionedHypergraph& hypergraph, + const HypernodeID hn, + bool non_adjacent_blocks, + bool use_precise_part_weights, + bool use_deadzone) { + const HypernodeWeight hn_weight = hypergraph.nodeWeight(hn); + RatingMap& tmp_scores = _gain.localScores(); + Gain isolated_block_gain = 0; + _gain.precomputeGains(hypergraph, hn, tmp_scores, isolated_block_gain, false); + + Gain best_gain = isolated_block_gain; + PartitionID best_target = kInvalidPartition; + for (const auto& entry : tmp_scores) { + const PartitionID to = entry.key; + const Gain gain = _gain.gain(entry.value, isolated_block_gain); + if (isValidTarget(hypergraph, to, hn_weight, use_precise_part_weights, use_deadzone) + && gain <= best_gain) { + best_gain = gain; + best_target = to; + } + } + + // if no adjacent block with free capacity exists, we need to consider non-adjacent blocks + if (non_adjacent_blocks && best_target == kInvalidPartition) { + utils::Randomize& rand = utils::Randomize::instance(); + // we start with a block that is chosen by random, to ensure a reasonable distribution of nodes + // to target blocks (note: this does not always result in a uniform distribution since some blocks + // are not an acceptable target, but it should be good enough) + const PartitionID start = rand.getRandomInt(0, static_cast(_context.partition.k - 1), SCHED_GETCPU); + PartitionID to = start; + do { + if (isValidTarget(hypergraph, to, hn_weight, use_precise_part_weights, use_deadzone) + && !tmp_scores.contains(to)) { + best_target = to; + break; + } + + ++to; + if (to == _context.partition.k) { + to = 0; + } + } while (to != start); + // assertion does not always hold with tight balance constraint or large node weights + // ASSERT(best_target != kInvalidPartition); + } + tmp_scores.clear(); + return std::make_pair(best_gain, best_target); + } + + template + Gain JetRebalancer::computeAverageGain(const PartitionedHypergraph& hypergraph, + const HypernodeID hn) { + ASSERT(_num_valid_targets > 0); + const HypernodeWeight hn_weight = hypergraph.nodeWeight(hn); + RatingMap& tmp_scores = _gain.localScores(); + Gain isolated_block_gain = 0; + _gain.precomputeGains(hypergraph, hn, tmp_scores, isolated_block_gain, false); + + Gain sum = 0; + PartitionID connected_valid_targets = 0; + for (const auto& entry : tmp_scores) { + if (isValidTarget(hypergraph, entry.key, hn_weight, false)) { + sum += _gain.gain(entry.value, isolated_block_gain); + ++connected_valid_targets; + } + } + tmp_scores.clear(); + + PartitionID remaining_valid_targets = _num_valid_targets - connected_valid_targets; + sum += remaining_valid_targets * isolated_block_gain; + return sum / _num_valid_targets; + } + + template + void JetRebalancer::updateImbalance(const PartitionedHypergraph& hypergraph, + bool read_weights_from_graph) { + _num_imbalanced_blocks = 0; + _num_valid_targets = 0; + for ( PartitionID block = 0; block < _context.partition.k; ++block ) { + if (read_weights_from_graph) { + _part_weights[block] = hypergraph.partWeight(block); + } + if (imbalance(block) > 0) { + ++_num_imbalanced_blocks; + } else if (isValidTarget(hypergraph, block, 0, false)) { + ++_num_valid_targets; + } + } + } + + template + void JetRebalancer::initializeDataStructures(const PartitionedHypergraph& hypergraph) { + ASSERT(_buckets.size() == NUM_BUCKETS && _part_weights.size() == static_cast(_context.partition.k)); + updateImbalance(hypergraph); + _bucket_weights.assign(_current_k * NUM_BUCKETS, 0); + for (auto& local_weights: _local_bucket_weights) { + local_weights.assign(_current_k * NUM_BUCKETS, 0); + } + + size_t estimated_insertions = _num_imbalanced_blocks * hypergraph.initialNumNodes() / _context.partition.k; + // buckets zero and one are probably mostly empty + for (size_t i = 2; i < _buckets.size(); ++i) { + estimated_insertions /= 2; + _buckets[i].reserve_for_estimated_number_of_insertions(estimated_insertions); + } + } + + // explicitly instantiate so the compiler can generate them when compiling this cpp file + namespace { + #define JET_REBALANCER(X, Y) JetRebalancer + } + + // explicitly instantiate so the compiler can generate them when compiling this cpp file + INSTANTIATE_CLASS_WITH_TYPE_TRAITS_AND_GAIN_TYPES(JET_REBALANCER) +} \ No newline at end of file diff --git a/mt-kahypar/partition/refinement/rebalancing/jet_rebalancer.h b/mt-kahypar/partition/refinement/rebalancing/jet_rebalancer.h new file mode 100644 index 000000000..39fcd3ae7 --- /dev/null +++ b/mt-kahypar/partition/refinement/rebalancing/jet_rebalancer.h @@ -0,0 +1,222 @@ +/******************************************************************************* + * MIT License + * + * This file is part of Mt-KaHyPar. + * + * Copyright (C) 2023 Nikolai Maas + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + ******************************************************************************/ + +#pragma once + +#include "tbb/enumerable_thread_specific.h" + +#include "mt-kahypar/datastructures/concurrent_bucket_map.h" +#include "mt-kahypar/partition/context.h" +#include "mt-kahypar/partition/metrics.h" +#include "mt-kahypar/partition/refinement/i_refiner.h" +#include "mt-kahypar/partition/refinement/gains/km1/km1_gain_computation.h" +#include "mt-kahypar/partition/refinement/gains/cut/cut_gain_computation.h" + +namespace mt_kahypar { +template +class JetRebalancer final : public IRefiner { + private: + using BucketMap = ds::ConcurrentBucketMap; + using PartitionedHypergraph = typename TypeTraits::PartitionedHypergraph; + using GainCache = typename GainTypes::GainCache; + using GainCalculator = typename GainTypes::GainComputation; + using RatingMap = typename GainCalculator::RatingMap; + using AtomicWeight = parallel::IntegralAtomicWrapper; + + static constexpr size_t NUM_BUCKETS = 12; + static constexpr size_t BUCKET_FACTOR = 32; + + static constexpr bool debug = false; + static constexpr bool enable_heavy_assert = false; + +public: + // TODO: add repairEmptyBlocks functionality? + + explicit JetRebalancer(const Context& context, + GainCache& gain_cache) : + _context(context), + _gain_cache(gain_cache), + _current_k(_context.partition.k), + _gain(context), + _num_imbalanced_blocks(0), + _num_valid_targets(0), + _part_weights(_context.partition.k), + _buckets(), + _bucket_weights(_context.partition.k * NUM_BUCKETS, 0), + _local_bucket_weights([&] { + return constructBucketWeightVector(); + }), + _node_was_moved() { + for (size_t i = 0; i < NUM_BUCKETS; ++i) { + _buckets.emplace_back(BUCKET_FACTOR); + } + } + + JetRebalancer(const JetRebalancer&) = delete; + JetRebalancer(JetRebalancer&&) = delete; + + JetRebalancer & operator= (const JetRebalancer &) = delete; + JetRebalancer & operator= (JetRebalancer &&) = delete; + + bool refineImpl(mt_kahypar_partitioned_hypergraph_t& hypergraph, + const vec&, + Metrics& best_metrics, + double); + + void initializeImpl(mt_kahypar_partitioned_hypergraph_t& hypergraph) final { + PartitionedHypergraph& phg = utils::cast(hypergraph); + _node_was_moved.resize(phg.initialNumNodes(), uint8_t(false)); + } + +private: + template + void weakRebalancingRound(PartitionedHypergraph& phg); + + void strongRebalancingRound(PartitionedHypergraph& phg); + + template + void insertNodesIntoBuckets(PartitionedHypergraph& phg, F compute_gain_fn); + + template + void processBuckets(PartitionedHypergraph& phg, F move_node_fn, bool retry_invalid_moves, bool update_local_part_weights); + + std::pair computeGainAndTargetPart(const PartitionedHypergraph& hypergraph, + const HypernodeID hn, + bool non_adjacent_blocks, + bool use_precise_part_weights = false, + bool use_deadzone = true); + + // used for Jetrs (strong rebalancing), rounded down + Gain computeAverageGain(const PartitionedHypergraph& hypergraph, const HypernodeID hn); + + void updateImbalance(const PartitionedHypergraph& hypergraph, bool read_weights_from_graph = true); + + void initializeDataStructures(const PartitionedHypergraph& hypergraph); + + bool mayMoveNode(PartitionID block, HypernodeWeight hn_weight) const { + double allowed_weight = _part_weights[block].load(std::memory_order_relaxed) + - _context.partition.perfect_balance_part_weights[block]; + allowed_weight *= _context.refinement.jet_rebalancing.heavy_vertex_exclusion_factor; + return hn_weight <= allowed_weight; + } + + HypernodeWeight imbalance(PartitionID block) const { + return _part_weights[block].load(std::memory_order_relaxed) + - _context.partition.max_part_weights[block]; + } + + HypernodeWeight deadzoneForPart(PartitionID block) const { + const HypernodeWeight balanced = _context.partition.perfect_balance_part_weights[block]; + const HypernodeWeight max = _context.partition.max_part_weights[block]; + return max - _context.refinement.jet_rebalancing.relative_deadzone_size * (max - balanced); + } + + bool isValidTarget(const PartitionedHypergraph& hypergraph, + PartitionID block, + HypernodeWeight hn_weight, + bool use_precise_part_weights, + bool use_deadzone = true) const { + const HypernodeWeight block_weight = use_precise_part_weights ? + hypergraph.partWeight(block) : _part_weights[block].load(std::memory_order_relaxed); + return (!use_deadzone || block_weight < deadzoneForPart(block)) && + block_weight + hn_weight <= _context.partition.max_part_weights[block]; + } + + bool changeNodePart(PartitionedHypergraph& phg, + const HypernodeID hn, + const PartitionID from, + const PartitionID to, + bool ensure_balanced) { + if (to == kInvalidPartition) { + return false; + } + + // This function is passed as lambda to the changeNodePart function and used + // to calculate the "real" delta of a move (in terms of the used objective function). + auto objective_delta = [&](const SyncronizedEdgeUpdate& sync_update) { + _gain.computeDeltaForHyperedge(sync_update); + }; + + HypernodeWeight max_weight = ensure_balanced ? _context.partition.max_part_weights[to] + : std::numeric_limits::max(); + bool success = false; + if ( _gain_cache.isInitialized() ) { + success = phg.changeNodePart(_gain_cache, hn, from, to, max_weight, []{}, objective_delta); + } else { + success = phg.changeNodePart(hn, from, to, max_weight, []{}, objective_delta); + } + ASSERT(success || ensure_balanced); + if (success) { + _node_was_moved[hn] = uint8_t(true); + } + return success; + } + + MT_KAHYPAR_ATTRIBUTE_ALWAYS_INLINE size_t log2(Gain gain) const { + size_t value = 0; + while (gain >>= 1) { + ++value; + } + return value; + } + + MT_KAHYPAR_ATTRIBUTE_ALWAYS_INLINE size_t getBucketID(Gain gain) const { + if (gain > 0) { + return std::min(2 + log2(gain), NUM_BUCKETS - 1); + } else if (gain == 0) { + return 1; + } + return 0; + } + + void resizeDataStructuresForCurrentK() { + // If the number of blocks changes, we resize data structures + // (can happen during deep multilevel partitioning) + if ( _current_k != _context.partition.k ) { + _current_k = _context.partition.k; + _gain.changeNumberOfBlocks(_current_k); + _part_weights = parallel::scalable_vector(_current_k); + } + } + + parallel::scalable_vector constructBucketWeightVector() const { + return parallel::scalable_vector(_current_k * NUM_BUCKETS, 0); + } + + const Context& _context; + GainCache& _gain_cache; + PartitionID _current_k; + GainCalculator _gain; + PartitionID _num_imbalanced_blocks; + PartitionID _num_valid_targets; + parallel::scalable_vector _part_weights; + parallel::scalable_vector _buckets; + parallel::scalable_vector _bucket_weights; + tbb::enumerable_thread_specific> _local_bucket_weights; + parallel::scalable_vector _node_was_moved; +}; + +} // namespace kahypar diff --git a/mt-kahypar/partition/refinement/rebalancing/rebalancer.h b/mt-kahypar/partition/refinement/rebalancing/rebalancer.h index 8502811f6..088c9f312 100644 --- a/mt-kahypar/partition/refinement/rebalancing/rebalancer.h +++ b/mt-kahypar/partition/refinement/rebalancing/rebalancer.h @@ -40,6 +40,7 @@ template class Rebalancer final : public IRefiner { private: using PartitionedHypergraph = typename TypeTraits::PartitionedHypergraph; + using GainCache = typename GainTypes::GainCache; using GainCalculator = typename GainTypes::GainComputation; using AtomicWeight = parallel::IntegralAtomicWrapper; @@ -73,6 +74,9 @@ class Rebalancer final : public IRefiner { _gain(context), _part_weights(_context.partition.k) { } + explicit Rebalancer(const Context& context, GainCache&) : + Rebalancer(context) { } + Rebalancer(const Rebalancer&) = delete; Rebalancer(Rebalancer&&) = delete; diff --git a/tests/partition/refinement/CMakeLists.txt b/tests/partition/refinement/CMakeLists.txt index 616a46bd0..6c21f3d27 100644 --- a/tests/partition/refinement/CMakeLists.txt +++ b/tests/partition/refinement/CMakeLists.txt @@ -10,6 +10,7 @@ target_sources(mt_kahypar_tests PRIVATE rollback_test.cc rebalance_test.cc twoway_fm_refiner_test.cc + jet_rebalancer_test.cc gain_test.cc gain_cache_test.cc multitry_fm_test.cc diff --git a/tests/partition/refinement/jet_rebalancer_test.cc b/tests/partition/refinement/jet_rebalancer_test.cc new file mode 100644 index 000000000..b9cff4235 --- /dev/null +++ b/tests/partition/refinement/jet_rebalancer_test.cc @@ -0,0 +1,145 @@ +/******************************************************************************* + * MIT License + * + * This file is part of Mt-KaHyPar. + * + * Copyright (C) 2023 Nikolai Maas + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + ******************************************************************************/ + +#include + +#include "gmock/gmock.h" + +#include "mt-kahypar/definitions.h" +#include "mt-kahypar/io/hypergraph_factory.h" +#include "mt-kahypar/partition/refinement/rebalancing/jet_rebalancer.h" +#include "mt-kahypar/partition/refinement/rebalancing/rebalancer.h" +#include "mt-kahypar/partition/refinement/gains/gain_definitions.h" +#include "mt-kahypar/utils/randomize.h" + +using ::testing::Test; + +namespace mt_kahypar { + +namespace { + using TypeTraits = StaticHypergraphTypeTraits; + using Hypergraph = typename TypeTraits::Hypergraph; + using PartitionedHypergraph = typename TypeTraits::PartitionedHypergraph; + using GainCache = typename Km1GainTypes::GainCache; +} + +template