Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions include/networkit/coarsening/CoarsenedGraphView.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,13 @@ class CoarsenedGraphView {
*/
const std::vector<node> &getOriginalNodes(node supernode) const;

/**
* Release the calling thread's computeNeighbors() scratch buffer (capacity included).
* Call at the end of a move/refine phase so a giant-supernode high-water mark from one
* level cannot leak into the next.
*/
static void releaseThreadScratch();

private:
const Graph &originalGraph;
std::vector<node> nodeMapping; // original_node -> supernode
Expand Down
67 changes: 49 additions & 18 deletions networkit/cpp/coarsening/CoarsenedGraphView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,24 @@
#include <networkit/auxiliary/Timer.hpp>
#include <networkit/coarsening/CoarsenedGraphView.hpp>

#include <unordered_map>
#include <algorithm>
#include <utility>
#include <vector>

namespace NetworKit {

namespace {
// Phase-scoped scratch for computeNeighbors(): reused across calls within a move/refine
// phase, released via releaseThreadScratch() at each phase boundary so a high-water mark
// from one coarsening level cannot leak into the next.
thread_local std::vector<std::pair<node, edgeweight>> t_staged;
} // namespace

void CoarsenedGraphView::releaseThreadScratch() {
t_staged.clear();
t_staged.shrink_to_fit();
}

CoarsenedGraphView::CoarsenedGraphView(const Graph &originalGraph, const Partition &partition)
: originalGraph(originalGraph) {

Expand Down Expand Up @@ -96,37 +110,54 @@ const std::vector<node> &CoarsenedGraphView::getOriginalNodes(node supernode) co
return supernodeToOriginal[supernode];
}

std::vector<std::pair<node, edgeweight>>
CoarsenedGraphView::computeNeighbors(node supernode) const {
std::unordered_map<node, edgeweight> aggregatedWeights;

// No locks needed here - supernodeToOriginal and nodeMapping are read-only after construction
// Iterate through all original nodes in this supernode
std::vector<std::pair<node, edgeweight>> CoarsenedGraphView::computeNeighbors(
node supernode) const { // Flat aggregation (InducedSubgraphView-style): stage (supernode,
// weight) pairs into the
// phase-scoped thread-local buffer, sort, then combine runs. This avoids per-call
// std::unordered_map hashing and its per-node bucket allocations; the only allocation
// left is the returned vector. Sort order also makes the output deterministic.
// Call releaseThreadScratch() at phase boundaries to shrink after every phase.
t_staged.clear();

// No locks needed here - supernodeToOriginal and nodeMapping are read-only after
// construction. Iterate through all original nodes in this supernode.
for (node originalNode : supernodeToOriginal[supernode]) {
// Iterate through neighbors of each original node
originalGraph.forNeighborsOf(originalNode, [&](node originalNeighbor, edgeweight weight) {
node neighborSupernode = nodeMapping[originalNeighbor];
/*
* An undirected edge sits in the adjacency of both endpoints, so an edge inside this
* supernode would be aggregated twice. Count it once, from the higher endpoint,
* mirroring ParallelPartitionCoarsening's aggregation.
* An undirected edge sits in the adjacency of both endpoints, so an edge inside
* this supernode would be aggregated twice. Count it once, from the higher
* endpoint, mirroring ParallelPartitionCoarsening's aggregation.
*/
if (neighborSupernode == supernode && originalNode < originalNeighbor)
return;
// Aggregate weights to the same supernode
aggregatedWeights[neighborSupernode] += weight;
t_staged.emplace_back(neighborSupernode, weight);
});
}

// Convert to vector format
std::vector<std::pair<node, edgeweight>> neighbors;
neighbors.reserve(aggregatedWeights.size());
if (t_staged.empty())
return {};

std::sort(t_staged.begin(), t_staged.end(),
[](const auto &a, const auto &b) { return a.first < b.first; });

for (const auto &entry : aggregatedWeights) {
if (entry.second > 0.0) { // Only include edges with positive weight
neighbors.emplace_back(entry.first, entry.second);
// Combine runs of equal supernodes into the output vector.
std::vector<std::pair<node, edgeweight>> neighbors;
neighbors.reserve(t_staged.size());
node cur = t_staged[0].first;
edgeweight acc = 0.0;
for (const auto &entry : t_staged) {
if (entry.first != cur) {
if (acc > 0.0)
neighbors.emplace_back(cur, acc);
cur = entry.first;
acc = 0.0;
}
acc += entry.second;
}
if (acc > 0.0) // Only include edges with positive weight
neighbors.emplace_back(cur, acc);

return neighbors;
}
Expand Down
77 changes: 50 additions & 27 deletions networkit/cpp/community/ParallelLeidenView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@

#include <cstdint>
#include <cstdlib>
#include <networkit/auxiliary/FlatMap.hpp>
#include <networkit/community/ParallelLeidenView.hpp>
#ifndef _WIN32
#include <dlfcn.h>
#endif
#include <algorithm>
#include <stdexcept>
#include <unordered_map>
#include <utility>
#include <vector>

namespace NetworKit {

Expand Down Expand Up @@ -433,9 +436,13 @@ ParallelLeidenView::MoveStats ParallelLeidenView::parallelMove(const GraphType &
currentNodes.reserve(tshare);
std::vector<node> newNodes;
newNodes.reserve(WORKING_SIZE);
// Sparse cutWeight[Community] storage avoids one dense community vector per thread.
std::unordered_map<index, double> cutWeights;
// Flat cut-weight storage (InducedSubgraphView-style): stage (community, weight)
// pairs once per visit, sort, and combine runs into parallel pointers/cutVals
// vectors. Binary search replaces hash lookups; per-thread staging vectors are
// reused across visits to avoid malloc/free churn.
std::vector<std::pair<index, double>> staged;
std::vector<index> pointers;
std::vector<double> cutVals;

auto moveLimitReached = [&](node candidate) {
return movesPerNode[candidate].load() >= static_cast<unsigned int>(maxMovesPerNode);
Expand Down Expand Up @@ -538,51 +545,61 @@ ParallelLeidenView::MoveStats ParallelLeidenView::parallelMove(const GraphType &
index bestCommunity = none;
double degree = 0;
count nodeMass = nodeSize(graph, u);
for (auto z : pointers) {
// Reset the clearlist : Set all cutweights to 0 and clear the pointer vector
cutWeights.erase(z);
}
staged.clear();
pointers.clear();
cutVals.clear();

graph.forNeighborsOf(u, [&](node neighbor, edgeweight ew) {
index neighborCommunity = result[neighbor];
if (cutWeights.find(neighborCommunity) == cutWeights.end()) {
pointers.push_back(neighborCommunity);
}
if (u == neighbor) {
degree += ew;
degree += 2 * ew;
} else {
cutWeights[neighborCommunity] += ew;
staged.emplace_back(result[neighbor], ew);
degree += ew; // keep track of the nodes degree. Loops count twice
}
degree += ew; // keep track of the nodes degree. Loops count twice
});

if (pointers.empty()) {
if (staged.empty()) {
finishProcessingNode(u);
continue;
}

// Sort + combine runs: one entry per adjacent community.
std::sort(staged.begin(), staged.end(),
[](const auto &a, const auto &b) { return a.first < b.first; });
for (const auto &entry : staged) {
if (!pointers.empty() && pointers.back() == entry.first) {
cutVals.back() += entry.second;
} else {
pointers.push_back(entry.first);
cutVals.push_back(entry.second);
}
}
staged.clear();

auto cutOf = [&](index community) {
auto it = std::lower_bound(pointers.begin(), pointers.end(), community);
if (it == pointers.end() || *it != community)
return 0.0;
return cutVals[static_cast<size_t>(it - pointers.begin())];
};

double singletonScore = scoreCommunity(0.0, degree, 0.0, nodeMass, 0);

// Determine move score for all neighbor communities
for (auto community : pointers) {
for (size_t k = 0; k < pointers.size(); ++k) {
index community = pointers[k];
// "Moving" a node to its current community is pointless
if (community != currentCommunity) {
double delta;
const auto cutWeightIt = cutWeights.find(community);
const double cutWeight =
cutWeightIt == cutWeights.end() ? 0.0 : cutWeightIt->second;
delta = scoreCommunity(cutWeight, degree, communityVolumes[community],
nodeMass, communitySizes[community]);
double delta =
scoreCommunity(cutVals[k], degree, communityVolumes[community],
nodeMass, communitySizes[community]);
if (delta > maxDelta) {
maxDelta = delta;
bestCommunity = community;
}
}
}
const auto currentCutWeightIt = cutWeights.find(currentCommunity);
const double currentCutWeight =
currentCutWeightIt == cutWeights.end() ? 0.0 : currentCutWeightIt->second;
const double currentCutWeight = cutOf(currentCommunity);
double modThreshold = scoreCurrentCommunityThreshold(
currentCutWeight, degree, communityVolumes[currentCommunity], nodeMass,
communitySizes[currentCommunity]);
Expand Down Expand Up @@ -693,6 +710,9 @@ ParallelLeidenView::MoveStats ParallelLeidenView::parallelMove(const GraphType &
TRACE("Thread ", omp_get_thread_num(), " worked ",
totalNodesPerThread[omp_get_thread_num()], "Nodes and moved ",
moved[omp_get_thread_num()]);
// Shrink after every phase: drop this thread's view-aggregation scratch so a
// high-water mark from this level cannot leak into the next.
CoarsenedGraphView::releaseThreadScratch();
}
result.setUpperBound(upperBound);
assert(queue.empty());
Expand Down Expand Up @@ -760,8 +780,9 @@ Partition ParallelLeidenView::parallelRefine(const GraphType &graph, bool &refin
#pragma omp parallel
{
std::vector<index> neighComms;
// Keeps track of relevant Neighbor communities. Needed to reset the clearlist fast
std::unordered_map<index, double> cutWeights; // cut from Node to Communities
// Flat cut map (InducedSubgraphView-style): sorted-vector storage instead of a hash
// table; refinement neighborhoods are small so binary search beats hashing.
Aux::flat_map<index, double> cutWeights; // cut from Node to Communities
auto &mt = Aux::Random::getURNG();
#pragma omp for
for (omp_index u = 0; u < static_cast<omp_index>(graph.upperNodeIdBound()); u++) {
Expand Down Expand Up @@ -958,6 +979,8 @@ Partition ParallelLeidenView::parallelRefine(const GraphType &graph, bool &refin
locks[bestC].unlock();
locks[u].unlock();
}
// Shrink after every phase: drop this thread's view-aggregation scratch.
CoarsenedGraphView::releaseThreadScratch();
}

refineMadeChanges = anyRefinementChanges.load();
Expand Down
Loading