Skip to content

Commit 11cd7e0

Browse files
garth-wellschrisrichardsonnate-sime
authored
Create parallel communication graph and support NetworkX analysis and visualiation (#3803)
* Work on more detailed IndexMap stats * more stats * More stats * Compile more data * Remove imbalance * Remove imbalance from Python * Provide more detail * Index map split * Work on docs and tests * Add placeholder * Implement function * Remove OpenMPI-specific test * Updates * Work on edge weights * Add simple test * Work on stats * Work on stats * Use ranges * Updates * Updates * Simplify * Type fix * Enable init * Improve error message * Test * Fix test * Format * Enable * Testing * Fix * lint * Enable * Re-enable * Work on stats * Simplify * Updates * Add networkx demo * mypy fix * lint * lint * Fix type hints * Fix * Type fixes * Update test * Simplify * Work on comm plotting * Improve demo * Tidy up * Test update * Misc fixes * Doc fixes * Fixes * Fixes * Include fixes * Add networkx for conda * Demo name fixes * Update names * Fix extension * Add demo plot options * Doc fix * Add bar chart plots * Remove C++ IndexMap stats code * Update python/dolfinx/wrappers/graph.cpp Co-authored-by: Nate <34454754+nate-sime@users.noreply.github.com> * Update test * Update cpp/dolfinx/common/IndexMap.h Co-authored-by: Nate <34454754+nate-sime@users.noreply.github.com> * Update python/demo/demo_comm-pattern.py Co-authored-by: Nate <34454754+nate-sime@users.noreply.github.com> * Update cpp/dolfinx/common/IndexMap.h Co-authored-by: Nate <34454754+nate-sime@users.noreply.github.com> * Update node weights * Doc fixes * Comment fixes * More fixes * Lint * Use free function * Simplify * Fix merge * Small updates * Small formatting updates * Move code to avoid (possible) circular dependencies. Best of dolfinx::common namespace doesn't depend on other namespaces. * Tidy * Add files * Wrap funtionality * Work in docs * Fix imports * Work in docs * Update cpp tests * Doc fix * Doc fix --------- Co-authored-by: Chris Richardson <chris@bpi.cam.ac.uk> Co-authored-by: Nate <34454754+nate-sime@users.noreply.github.com>
1 parent 8b2c4be commit 11cd7e0

File tree

29 files changed

+993
-186
lines changed

29 files changed

+993
-186
lines changed

.github/workflows/oneapi-conda/environment.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ dependencies:
2020
- mpi4py
2121
- mpich
2222
- nanobind
23+
- networkx
2324
- ninja
2425
- numba
2526
- numpy

cpp/demo/hyperelasticity/main.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include <dolfinx/mesh/Mesh.h>
3030
#include <dolfinx/mesh/cell_types.h>
3131
#include <dolfinx/nls/NewtonSolver.h>
32+
#include <numbers>
3233
#include <petscmat.h>
3334
#include <petscsys.h>
3435
#include <petscsystypes.h>
@@ -142,8 +143,7 @@ int main(int argc, char* argv[])
142143
PetscInitialize(&argc, &argv, nullptr, nullptr);
143144

144145
// Set the logging thread name to show the process rank
145-
int mpi_rank;
146-
MPI_Comm_rank(MPI_COMM_WORLD, &mpi_rank);
146+
int mpi_rank = dolfinx::MPI::rank(MPI_COMM_WORLD);
147147
std::string fmt = "[%Y-%m-%d %H:%M:%S.%e] [RANK " + std::to_string(mpi_rank)
148148
+ "] [%l] %v";
149149
spdlog::set_pattern(fmt);
@@ -194,7 +194,7 @@ int main(int argc, char* argv[])
194194
constexpr U x2_c = 0.5;
195195

196196
// Large angle of rotation (60 degrees)
197-
constexpr U theta = 1.04719755;
197+
constexpr U theta = std::numbers::pi / 3;
198198

199199
// New coordinates
200200
std::vector<U> fdata(3 * x.extent(1), 0.0);

cpp/dolfinx/common/IndexMap.cpp

Lines changed: 76 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <cstdint>
1212
#include <functional>
1313
#include <numeric>
14+
#include <ranges>
1415
#include <span>
1516
#include <utility>
1617
#include <vector>
@@ -182,9 +183,9 @@ compute_submap_indices(const IndexMap& imap,
182183
std::ranges::for_each(indices,
183184
[&is_in_submap](auto i) { is_in_submap[i] = 1; });
184185

185-
// --- Step 1 ---: Send ghost indices in `indices` to their owners
186-
// and receive indices owned by this process that are in `indices`
187-
// on other processes
186+
// --- Step 1 ---: Send ghost indices in `indices` to their owners and
187+
// receive indices owned by this process that are in `indices` on
188+
// other processes.
188189
const auto [send_indices, recv_indices, ghost_buffer_pos, send_sizes,
189190
recv_sizes, send_disp, recv_disp]
190191
= communicate_ghosts_to_owners(
@@ -198,7 +199,7 @@ compute_submap_indices(const IndexMap& imap,
198199
// all indices in `recv_indices` will necessarily be in `indices` on
199200
// this process, and thus other processes must own them in the submap.
200201
// If ownership of received index doesn't change, then this process
201-
// has the receiving rank as a destination
202+
// has the receiving rank as a destination.
202203
std::vector<int> recv_owners(send_disp.back());
203204
std::vector<int> submap_dest;
204205
submap_dest.reserve(1);
@@ -571,7 +572,7 @@ common::compute_owned_indices(std::span<const std::int32_t> indices,
571572
std::vector<int> send_sizes(src.size(), 0);
572573
std::vector<int> send_disp(src.size() + 1, 0);
573574
auto it = ghost_owners.begin();
574-
for (std::size_t i = 0; i < src.size(); i++)
575+
for (std::size_t i = 0; i < src.size(); ++i)
575576
{
576577
int owner = src[i];
577578
auto begin = std::find(it, ghost_owners.end(), owner);
@@ -850,7 +851,6 @@ common::create_sub_index_map(const IndexMap& imap,
850851
submap_ghost_gidxs, submap_ghost_owners),
851852
std::move(sub_imap_to_imap)};
852853
}
853-
854854
//-----------------------------------------------------------------------------
855855
//-----------------------------------------------------------------------------
856856
IndexMap::IndexMap(MPI_Comm comm, std::int32_t local_size) : _comm(comm, true)
@@ -1112,7 +1112,6 @@ graph::AdjacencyList<int> IndexMap::index_to_dest_ranks(int tag) const
11121112
// (index, [sharing ranks]). Non-owned indices are ghosted but
11131113
// not owned by this rank.
11141114
{
1115-
11161115
// Send data for owned indices back to ghosting ranks (this is
11171116
// necessary to share with ghosting ranks all the ranks that also
11181117
// ghost a ghost index)
@@ -1302,35 +1301,76 @@ std::span<const int> IndexMap::src() const noexcept { return _src; }
13021301
//-----------------------------------------------------------------------------
13031302
std::span<const int> IndexMap::dest() const noexcept { return _dest; }
13041303
//-----------------------------------------------------------------------------
1305-
std::array<double, 2> IndexMap::imbalance() const
1304+
std::vector<std::int32_t> IndexMap::weights_src() const
1305+
{
1306+
std::vector<std::int32_t> weights(_src.size(), 0);
1307+
for (int r : _owners)
1308+
{
1309+
auto it = std::ranges::lower_bound(_src, r);
1310+
assert(it != _src.end() and *it == r);
1311+
std::size_t pos = std::distance(_src.begin(), it);
1312+
assert(pos < weights.size());
1313+
weights[pos] += 1;
1314+
}
1315+
1316+
return weights;
1317+
}
1318+
//-----------------------------------------------------------------------------
1319+
std::vector<std::int32_t> IndexMap::weights_dest() const
1320+
{
1321+
int ierr = 0;
1322+
std::vector<std::int32_t> w_src = this->weights_src();
1323+
1324+
std::vector<MPI_Request> requests(_dest.size() + _src.size());
1325+
1326+
std::vector<std::int32_t> w_dest(_dest.size());
1327+
for (std::size_t i = 0; i < _dest.size(); ++i)
1328+
{
1329+
ierr = MPI_Irecv(w_dest.data() + i, 1, MPI_INT32_T, _dest[i], MPI_ANY_TAG,
1330+
_comm.comm(), &requests[i]);
1331+
dolfinx::MPI::check_error(_comm.comm(), ierr);
1332+
}
1333+
1334+
for (std::size_t i = 0; i < _src.size(); ++i)
1335+
{
1336+
ierr = MPI_Isend(w_src.data() + i, 1, MPI_INT32_T, _src[i], 0, _comm.comm(),
1337+
&requests[i + _dest.size()]);
1338+
dolfinx::MPI::check_error(_comm.comm(), ierr);
1339+
}
1340+
1341+
ierr = MPI_Waitall(requests.size(), requests.data(), MPI_STATUS_IGNORE);
1342+
dolfinx::MPI::check_error(_comm.comm(), ierr);
1343+
1344+
return w_dest;
1345+
}
1346+
//-----------------------------------------------------------------------------
1347+
std::array<std::vector<int>, 2> IndexMap::rank_type(int split_type) const
13061348
{
1307-
std::array<double, 2> imbalance{-1., -1.};
1308-
std::array<std::int32_t, 2> max_count;
1309-
std::array<std::int32_t, 2> local_sizes
1310-
= {static_cast<std::int32_t>(_local_range[1] - _local_range[0]),
1311-
static_cast<std::int32_t>(_ghosts.size())};
1312-
1313-
// Find the maximum number of owned indices and the maximum number of ghost
1314-
// indices across all processes.
1315-
MPI_Allreduce(local_sizes.data(), max_count.data(), 2, MPI_INT32_T, MPI_MAX,
1316-
_comm.comm());
1317-
1318-
std::int32_t total_num_ghosts = 0;
1319-
MPI_Allreduce(&local_sizes[1], &total_num_ghosts, 1, MPI_INT32_T, MPI_SUM,
1320-
_comm.comm());
1321-
1322-
// Compute the average number of owned and ghost indices per process.
1323-
int comm_size = dolfinx::MPI::size(_comm.comm());
1324-
double avg_owned = static_cast<double>(_size_global) / comm_size;
1325-
double avg_ghosts = static_cast<double>(total_num_ghosts) / comm_size;
1326-
1327-
// Compute the imbalance by dividing the maximum number of indices by the
1328-
// corresponding average.
1329-
if (avg_owned > 0)
1330-
imbalance[0] = max_count[0] / avg_owned;
1331-
if (avg_ghosts > 0)
1332-
imbalance[1] = max_count[1] / avg_ghosts;
1333-
1334-
return imbalance;
1349+
int ierr;
1350+
1351+
MPI_Comm comm_s;
1352+
ierr = MPI_Comm_split_type(_comm.comm(), split_type, 0, MPI_INFO_NULL,
1353+
&comm_s);
1354+
dolfinx::MPI::check_error(_comm.comm(), ierr);
1355+
1356+
int size_s = dolfinx::MPI::size(comm_s);
1357+
int rank = dolfinx::MPI::rank(_comm.comm());
1358+
1359+
// Note: in most cases, size_s will be much smaller than the size of
1360+
// _comm
1361+
std::vector<int> ranks_s(size_s);
1362+
ierr = MPI_Allgather(&rank, 1, MPI_INT, ranks_s.data(), 1, MPI_INT, comm_s);
1363+
dolfinx::MPI::check_error(comm_s, ierr);
1364+
1365+
std::vector<int> split_dest, split_src;
1366+
std::ranges::set_intersection(_dest, ranks_s, std::back_inserter(split_dest));
1367+
assert(std::ranges::is_sorted(split_dest));
1368+
std::ranges::set_intersection(_src, ranks_s, std::back_inserter(split_src));
1369+
assert(std::ranges::is_sorted(split_src));
1370+
1371+
ierr = MPI_Comm_free(&comm_s);
1372+
dolfinx::MPI::check_error(comm_s, ierr);
1373+
1374+
return {std::move(split_dest), std::move(split_src)};
13351375
}
13361376
//-----------------------------------------------------------------------------

cpp/dolfinx/common/IndexMap.h

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
#pragma once
88

99
#include "IndexMap.h"
10+
#include "MPI.h"
1011
#include <cstdint>
11-
#include <dolfinx/common/MPI.h>
12+
#include <dolfinx/graph/AdjacencyList.h>
1213
#include <memory>
1314
#include <span>
15+
#include <tuple>
1416
#include <utility>
1517
#include <vector>
1618

@@ -255,22 +257,56 @@ class IndexMap
255257
/// and sorted.
256258
std::span<const int> dest() const noexcept;
257259

258-
/// @brief Returns the imbalance of the current IndexMap.
260+
/// @brief Compute the number of ghost indices owned by each rank in
261+
/// IndexMap::src.
259262
///
260-
/// The imbalance is a measure of load balancing across all processes,
261-
/// defined as the maximum number of indices on any process divided by
262-
/// the average number of indices per process. This function
263-
/// calculates the imbalance separately for owned indices and ghost
264-
/// indices and returns them as a std::array<double, 2>. If the total
265-
/// number of owned or ghost indices is zero, the respective entry in
266-
/// the array is set to -1.
263+
/// This is a measure of the amount of data:
267264
///
268-
/// @note This is a collective operation and must be called by all
269-
/// processes in the communicator associated with the IndexMap.
265+
/// 1. Sent from this rank to other ranks when performing a reverse
266+
/// (owner <- ghost) scatter.
270267
///
271-
/// @return An array containing the imbalance in owned indices (first
272-
/// element) and the imbalance in ghost indices (second element).
273-
std::array<double, 2> imbalance() const;
268+
/// 2. Received by this rank from other ranks when performing a
269+
/// forward (owner -> ghost) scatter.
270+
///
271+
/// @return A weight vector, where `weight[i]` the the number of
272+
/// ghost indices owned by rank IndexMap::src()`[i]`.
273+
std::vector<std::int32_t> weights_src() const;
274+
275+
/// @brief Compute the number of ghost indices owned by each rank in
276+
/// IndexMap::dest.
277+
///
278+
/// This is a measure of the amount of data:
279+
///
280+
/// 1. Sent from this rank to other ranks when performing a forward
281+
/// (owner -> ghost) scatter.
282+
///
283+
/// 2. Received by this rank from other ranks when performing a
284+
/// reverse forward (owner <- ghost) scatter.
285+
///
286+
/// @return A weight vector, where `weight[i]` the the number of ghost
287+
/// indices owned by rank IndexMap::dest()`[i]`.
288+
std::vector<std::int32_t> weights_dest() const;
289+
290+
/// @brief Destination and source ranks by type, e.g, ranks that are
291+
/// destination/source ranks for the caller and are in a common
292+
/// shared memory region.
293+
///
294+
/// This function is used to group destination and source ranks by
295+
/// 'type'. The type is defined by the MPI `split_type`. Split types
296+
/// include ranks from a common shared memory region
297+
/// (`MPI_COMM_TYPE_SHARED`) or a common NUMA region. Splits types are
298+
/// listed at
299+
/// https://docs.open-mpi.org/en/main/man-openmpi/man3/MPI_Comm_split_type.3.html#split-types.
300+
///
301+
/// @note Collective operation on comm().
302+
///
303+
/// @param[in] split_type MPI split type, as used in the function
304+
/// `MPI_Comm_split_type`. See
305+
/// https://docs.open-mpi.org/en/main/man-openmpi/man3/MPI_Comm_split_type.3.html#split-types.
306+
/// @return (0) Intersection of ranks in `split_type` and in dest(),
307+
/// and (1) intersection of ranks in `split_type` and in src().
308+
/// Returned ranks are on the comm() communicator.
309+
std::array<std::vector<int>, 2> rank_type(int split_type) const;
274310

275311
private:
276312
// Range of indices (global) owned by this process
@@ -294,4 +330,5 @@ class IndexMap
294330
// Set of ranks ghost owned indices
295331
std::vector<int> _dest;
296332
};
333+
297334
} // namespace dolfinx::common

cpp/dolfinx/common/MPI.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
#include <cassert>
1515
#include <complex>
1616
#include <cstdint>
17-
#include <dolfinx/graph/AdjacencyList.h>
1817
#include <numeric>
1918
#include <set>
2019
#include <span>

cpp/dolfinx/common/Scatterer.h

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ namespace dolfinx::common
2323
/// @brief A Scatterer supports the MPI scattering and gathering of data
2424
/// that is associated with a common::IndexMap.
2525
///
26-
/// Scatter and gather operations use MPI neighbourhood collectives.
27-
/// The implementation is designed for sparse communication patterns,
28-
/// as it typical of patterns based on an IndexMap.
26+
/// Scatter and gather operations use MPI neighbourhood collectives. The
27+
/// implementation is designed for sparse communication patterns, as it
28+
/// typical of patterns based on an IndexMap.
2929
template <class Allocator = std::allocator<std::int32_t>>
3030
class Scatterer
3131
{
@@ -395,7 +395,7 @@ class Scatterer
395395
if (_sizes_local.empty() and _sizes_remote.empty())
396396
return;
397397

398-
// // Send and receive data
398+
// Send and receive data
399399

400400
switch (type)
401401
{
@@ -411,16 +411,17 @@ class Scatterer
411411
case type::p2p:
412412
{
413413
assert(requests.size() == _dest.size() + _src.size());
414-
// Start non-blocking send from this process to ghost owners.
414+
415+
// Start non-blocking send from this process to ghost owners
415416
for (std::size_t i = 0; i < _dest.size(); i++)
416417
{
417418
MPI_Irecv(recv_buffer.data() + _displs_local[i], _sizes_local[i],
418419
dolfinx::MPI::mpi_t<T>, _dest[i], MPI_ANY_TAG, _comm0.comm(),
419420
&requests[i]);
420421
}
421422

422-
// Start non-blocking receive from neighbor process for which an owned
423-
// index is a ghost.
423+
// Start non-blocking receive from neighbor process for which an
424+
// owned index is a ghost
424425
for (std::size_t i = 0; i < _src.size(); i++)
425426
{
426427
MPI_Isend(send_buffer.data() + _displs_remote[i], _sizes_remote[i],
@@ -440,8 +441,8 @@ class Scatterer
440441
/// The buffers passed to Scatterer::scatter_rev_begin must not be
441442
/// modified until after the function has been called.
442443
///
443-
/// @param[in] request The handle used when calling
444-
/// Scatterer::scatter_rev_begin
444+
/// @param[in] request Handle used when calling
445+
/// Scatterer::scatter_rev_begin.
445446
void scatter_rev_end(std::span<MPI_Request> request) const
446447
{
447448
// Return early if there are no incoming or outgoing edges
@@ -568,28 +569,33 @@ class Scatterer
568569
return _remote_inds.size();
569570
}
570571

571-
/// Return a vector of local indices (owned) used to pack/unpack local
572-
/// data. These indices are grouped by neighbor process (process for
573-
/// which an index is a ghost).
572+
/// @brief Return a vector of local indices (owned) used to
573+
/// pack/unpack local data.
574+
///
575+
/// Indices are grouped by neighbor process (process for which an
576+
/// index is a ghost).
574577
const std::vector<std::int32_t>& local_indices() const noexcept
575578
{
576579
return _local_inds;
577580
}
578581

579-
/// Return a vector of remote indices (ghosts) used to pack/unpack ghost
580-
/// data. These indices are grouped by neighbor process (ghost owners).
582+
/// @brief Return a vector of remote indices (ghosts) used to
583+
/// pack/unpack ghost data.
584+
///
585+
/// These indices are grouped by neighbor process (ghost owners).
581586
const std::vector<std::int32_t>& remote_indices() const noexcept
582587
{
583588
return _remote_inds;
584589
}
585590

586591
/// @brief The number values (block size) to send per index in the
587-
/// common::IndexMap use to create the scatterer
592+
/// common::IndexMap use to create the scatterer.
588593
/// @return The block size
589594
int bs() const noexcept { return _bs; }
590595

591-
/// @brief Create a vector of MPI_Requests for a given Scatterer::type
592-
/// @return A vector of MPI requests
596+
/// @brief Create a vector of MPI_Requests for a given
597+
/// Scatterer::type.
598+
/// @return Vector of MPI requests.
593599
std::vector<MPI_Request> create_request_vector(Scatterer::type type
594600
= type::neighbor)
595601
{

0 commit comments

Comments
 (0)