Skip to content

Commit b64d7e4

Browse files
authored
Mesh build performance improvements (#4110)
* Small improvements * Speed up topology * Performance improvements * Small improrvements * Test fix
1 parent 4a5f841 commit b64d7e4

10 files changed

Lines changed: 161 additions & 88 deletions

File tree

cpp/dolfinx/graph/ordering.cpp

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,14 @@ residual_graph_components(const graph::AdjacencyList<int>& graph,
7575
}
7676
//-----------------------------------------------------------------------------
7777
// Get the (maximum) width of a level structure
78-
int max_level_width(const graph::AdjacencyList<int>& levels)
78+
std::size_t max_level_width(const graph::AdjacencyList<int>& levels)
7979
{
80-
int wmax = 0;
81-
for (int i = 0; i < levels.num_nodes(); ++i)
82-
wmax = std::max(wmax, levels.num_links(i));
83-
return wmax;
80+
const std::vector<std::int32_t>& offsets = levels.offsets();
81+
return std::transform_reduce(
82+
offsets.begin(), std::prev(offsets.end()), std::next(offsets.begin()),
83+
std::size_t(0),
84+
[](auto x0, auto x1) -> std::size_t { return std::max(x1, x0); },
85+
[](auto x0, auto x1) -> std::size_t { return x1 - x0; });
8486
}
8587
//-----------------------------------------------------------------------------
8688
// Create a level structure from graph, rooted at node s
@@ -120,7 +122,6 @@ create_level_structure(const graph::AdjacencyList<int>& graph, int s)
120122
return graph::AdjacencyList(std::move(level_structure),
121123
std::move(level_offsets));
122124
}
123-
124125
//-----------------------------------------------------------------------------
125126
// Gibbs-Poole-Stockmeyer algorithm, finding a reordering for the given
126127
// graph, operating only on nodes which are yet unlabelled (indicated
@@ -134,7 +135,7 @@ gps_reorder_unlabelled(const graph::AdjacencyList<std::int32_t>& graph,
134135
const std::int32_t n = graph.num_nodes();
135136

136137
// Degree comparison function
137-
auto cmp_degree = [&graph](int a, int b)
138+
auto cmp_degree = [&graph](auto a, auto b)
138139
{ return graph.num_links(a) < graph.num_links(b); };
139140

140141
// ALGORITHM I. Finding endpoints of a pseudo-diameter.
@@ -164,7 +165,6 @@ gps_reorder_unlabelled(const graph::AdjacencyList<std::int32_t>& graph,
164165
S.resize(lv_final.size());
165166
std::partial_sort_copy(lv_final.begin(), lv_final.end(), S.begin(), S.end(),
166167
cmp_degree);
167-
168168
int w_min = std::numeric_limits<int>::max();
169169
done = true;
170170

@@ -258,15 +258,15 @@ gps_reorder_unlabelled(const graph::AdjacencyList<std::int32_t>& graph,
258258
[](int vl, int vn) { return (vl > vn) ? vl : 0; });
259259

260260
// Find maximum of those that did increase
261-
int h0 = *std::ranges::max_element(wh);
262-
int l0 = *std::ranges::max_element(wl);
261+
int h0 = std::ranges::max(wh);
262+
int l0 = std::ranges::max(wl);
263263

264264
// Choose which side to use
265265
int side = h0 < l0 ? 0 : 1;
266266

267-
// If h0 == l0, then use the elements of the level pairs which arise
268-
// from the rooted level structure of smaller width. If the widths are
269-
// equal, use the first elements. (i.e. lvp[][0]).
267+
// If h0 == l0, then use the elements of the level pairs which
268+
// arise from the rooted level structure of smaller width. If the
269+
// widths are equal, use the first elements. (i.e. lvp[][0]).
270270
if (h0 == l0)
271271
side = max_level_width(lu) < max_level_width(lv) ? 1 : 0;
272272

@@ -336,10 +336,12 @@ gps_reorder_unlabelled(const graph::AdjacencyList<std::int32_t>& graph,
336336
// with lowest degree
337337
nrem.clear();
338338
for (int w : lslevel)
339+
{
339340
if (!labelled[w])
340341
nrem.push_back(w);
342+
}
341343

342-
if (nrem.size() == 0)
344+
if (nrem.empty())
343345
break;
344346

345347
std::ranges::sort(nrem, cmp_degree);

cpp/dolfinx/graph/partition.cpp

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (C) 2020 Garth N. Wells
1+
// Copyright (C) 2020-2026 Garth N. Wells
22
//
33
// This file is part of DOLFINx (https://www.fenicsproject.org)
44
//
@@ -8,6 +8,7 @@
88
#include "AdjacencyList.h"
99
#include "partitioners.h"
1010
#include <algorithm>
11+
#include <boost/sort/sort.hpp>
1112
#include <dolfinx/common/MPI.h>
1213
#include <dolfinx/common/Timer.h>
1314
#include <dolfinx/common/log.h>
@@ -390,12 +391,12 @@ graph::build::distribute(MPI_Comm comm, std::span<const std::int64_t> list,
390391
std::move(ghost_index_owner)};
391392
}
392393
//-----------------------------------------------------------------------------
393-
std::vector<std::int64_t>
394-
graph::build::compute_ghost_indices(MPI_Comm comm,
395-
std::span<const std::int64_t> owned_indices,
396-
std::span<const std::int64_t> ghost_indices,
397-
std::span<const int> ghost_owners)
394+
std::vector<std::int64_t> graph::build::compute_ghost_indices(
395+
MPI_Comm comm, std::span<const std::int64_t> owned_indices,
396+
std::span<const std::int64_t> ghost_indices,
397+
std::span<const int> ghost_owners, int num_threads)
398398
{
399+
common::Timer timer("Compute ghost indices");
399400
spdlog::info("Compute ghost indices");
400401

401402
// Get number of local cells determine global offset
@@ -424,7 +425,8 @@ graph::build::compute_ghost_indices(MPI_Comm comm,
424425

425426
MPI_Comm neighbor_comm_fwd, neighbor_comm_rev;
426427

427-
std::vector<int> in_edges = MPI::compute_graph_edges_pcx(comm, neighbors);
428+
std::vector<int> in_edges
429+
= dolfinx::MPI::compute_graph_edges_pcx(comm, neighbors);
428430
MPI_Dist_graph_create_adjacent(comm, in_edges.size(), in_edges.data(),
429431
MPI_UNWEIGHTED, neighbors.size(),
430432
neighbors.data(), MPI_UNWEIGHTED,
@@ -487,7 +489,13 @@ graph::build::compute_ghost_indices(MPI_Comm comm,
487489
old_to_new.push_back(
488490
{idx, static_cast<std::int64_t>(offset_local + old_to_new.size())});
489491
}
490-
std::ranges::sort(old_to_new);
492+
if (num_threads > 0)
493+
{
494+
boost::sort::block_indirect_sort(old_to_new.begin(), old_to_new.end(),
495+
num_threads);
496+
}
497+
else
498+
std::ranges::sort(old_to_new);
491499

492500
// Replace values in recv_data with new_index and send back
493501
std::ranges::transform(recv_data, recv_data.begin(),

cpp/dolfinx/graph/partition.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,13 @@ distribute(MPI_Comm comm, std::span<const std::int64_t> list,
114114
/// @param[in] ghost_indices List of ghost global indices.
115115
/// @param[in] ghost_owners The owning rank for each entry in
116116
/// `ghost_indices`.
117+
/// @param[in] num_threads Number of threads to use.
117118
/// @return New global indices for the ghost indices.
118119
std::vector<std::int64_t>
119120
compute_ghost_indices(MPI_Comm comm,
120121
std::span<const std::int64_t> owned_indices,
121122
std::span<const std::int64_t> ghost_indices,
122-
std::span<const int> ghost_owners);
123+
std::span<const int> ghost_owners, int num_threads);
123124

124125
/// Given an adjacency list with global, possibly non-contiguous, link
125126
/// indices and a local adjacency list with contiguous link indices

cpp/dolfinx/mesh/Topology.cpp

Lines changed: 71 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -689,19 +689,38 @@ std::vector<std::array<std::int64_t, 3>> exchange_ghost_indexing(
689689
/// @param[in] global_to_local Sorted array of (global, local) indices.
690690
std::vector<std::int32_t> convert_to_local_indexing(
691691
std::span<const std::int64_t> g,
692-
std::span<const std::pair<std::int64_t, std::int32_t>> global_to_local)
692+
std::span<const std::pair<std::int64_t, std::int32_t>> global_to_local,
693+
int num_threads)
693694
{
695+
auto transform =
696+
[](std::span<std::int32_t> data, std::span<const std::int64_t> g,
697+
std::span<const std::pair<std::int64_t, std::int32_t>> global_to_local)
698+
{
699+
std::transform(g.begin(), g.end(), data.begin(),
700+
[&global_to_local](auto i)
701+
{
702+
auto it = std::ranges::lower_bound(
703+
global_to_local, i, std::ranges::less(),
704+
[](auto& e) { return e.first; });
705+
assert(it != global_to_local.end());
706+
assert(it->first == i);
707+
return it->second;
708+
});
709+
};
710+
694711
std::vector<std::int32_t> data(g.size());
695-
std::transform(g.begin(), std::next(g.begin(), data.size()), data.begin(),
696-
[&global_to_local](auto i)
697-
{
698-
auto it = std::ranges::lower_bound(
699-
global_to_local, i, std::ranges::less(),
700-
[](auto& e) { return e.first; });
701-
assert(it != global_to_local.end());
702-
assert(it->first == i);
703-
return it->second;
704-
});
712+
if (num_threads > 0)
713+
{
714+
std::vector<std::jthread> threads(num_threads);
715+
for (int i = 0; i < num_threads; ++i)
716+
{
717+
auto [c0, c1] = dolfinx::MPI::local_range(i, g.size(), num_threads);
718+
threads[i] = std::jthread(transform, std::span(data.data() + c0, c1 - c0),
719+
g.subspan(c0, c1 - c0), global_to_local);
720+
}
721+
}
722+
else
723+
transform(data, g, global_to_local);
705724

706725
return data;
707726
}
@@ -733,7 +752,6 @@ build_entity_types(const std::vector<CellType>& cell_types)
733752
}
734753
return entity_types;
735754
}
736-
737755
} // namespace
738756

739757
//-----------------------------------------------------------------------------
@@ -834,9 +852,10 @@ std::shared_ptr<const common::IndexMap> Topology::index_map(int dim) const
834852
= this->index_maps(dim);
835853
if (im.empty())
836854
{
837-
throw std::runtime_error(std::format(
838-
"Missing IndexMap in Topology. Maybe you need to create_entities({}).",
839-
dim));
855+
throw std::runtime_error(
856+
std::format("Missing IndexMap in Topology. Maybe you need to "
857+
"create_entities({}).",
858+
dim));
840859
}
841860

842861
return im.at(0);
@@ -1034,7 +1053,7 @@ Topology mesh::create_topology(
10341053
std::vector<std::span<const std::int64_t>> cells,
10351054
std::vector<std::span<const std::int64_t>> original_cell_index,
10361055
std::vector<std::span<const int>> ghost_owners,
1037-
std::span<const std::int64_t> boundary_vertices)
1056+
std::span<const std::int64_t> boundary_vertices, int num_threads)
10381057
{
10391058
common::Timer timer("Topology: create");
10401059

@@ -1044,6 +1063,7 @@ Topology mesh::create_topology(
10441063

10451064
// Check cell data consistency and compile spans of owned and ghost
10461065
// cells
1066+
common::Timer timer0("Topology: 0");
10471067
spdlog::info("Create topology (generalised)");
10481068
std::vector<std::int32_t> num_local_cells(cell_types.size());
10491069
std::vector<std::span<const std::int64_t>> owned_cells;
@@ -1064,6 +1084,11 @@ Topology mesh::create_topology(
10641084
ghost_cells.push_back(cells[i].last(ghost_owners[i].size() * num_vertices));
10651085
}
10661086

1087+
timer0.stop();
1088+
timer0.flush();
1089+
1090+
common::Timer timer1("Topology: 1");
1091+
10671092
// Create sets of owned and unowned vertices from the cell ownership
10681093
// and the list of boundary vertices
10691094
auto [owned_vertices, unowned_vertices]
@@ -1075,9 +1100,14 @@ Topology mesh::create_topology(
10751100
const graph::AdjacencyList<int> global_vertex_to_ranks
10761101
= determine_sharing_ranks(comm, boundary_vertices);
10771102

1103+
timer1.stop();
1104+
timer1.flush();
1105+
10781106
// Iterate over vertices that have 'unknown' ownership, and if flagged
10791107
// as owned by determine_sharing_ranks update ownership status
10801108
{
1109+
common::Timer timer2("Topology: 2");
1110+
10811111
const int mpi_rank = dolfinx::MPI::rank(comm);
10821112
std::vector<std::int64_t> owned_shared_vertices;
10831113
for (std::size_t i = 0; i < boundary_vertices.size(); ++i)
@@ -1102,13 +1132,15 @@ Topology mesh::create_topology(
11021132
dolfinx::radix_sort(owned_vertices);
11031133
}
11041134

1135+
// NOTE: This block is relatively expensive
11051136
// Number all owned vertices, iterating over vertices cell-wise
11061137
std::vector<std::int32_t> local_vertex_indices(owned_vertices.size(), -1);
11071138
{
1139+
common::Timer timer3("Topology: 3 ");
11081140
std::int32_t v = 0;
1109-
for (std::size_t i = 0; i < cell_types.size(); ++i)
1141+
for (std::span<const std::int64_t> cells_t : cells)
11101142
{
1111-
for (auto vtx : cells[i])
1143+
for (auto vtx : cells_t)
11121144
{
11131145
if (auto it = std::ranges::lower_bound(owned_vertices, vtx);
11141146
it != owned_vertices.end() and *it == vtx)
@@ -1121,6 +1153,8 @@ Topology mesh::create_topology(
11211153
}
11221154
}
11231155

1156+
common::Timer timer4("Topology: 4");
1157+
11241158
// Compute the global offset for owned (local) vertex indices
11251159
std::int64_t global_offset_v = 0;
11261160
{
@@ -1136,29 +1170,39 @@ Topology mesh::create_topology(
11361170
std::span cell_idx(original_cell_index[i]);
11371171
cell_ghost_indices.push_back(graph::build::compute_ghost_indices(
11381172
comm, cell_idx.first(num_local_cells[i]),
1139-
cell_idx.last(ghost_owners[i].size()), ghost_owners[i]));
1173+
cell_idx.last(ghost_owners[i].size()), ghost_owners[i], num_threads));
11401174

11411175
// Create index maps for each cell type
11421176
index_map_c.push_back(std::make_shared<common::IndexMap>(
11431177
comm, num_local_cells[i], cell_ghost_indices[i], ghost_owners[i],
11441178
static_cast<int>(dolfinx::MPI::tag::consensus_nbx) + i));
11451179
}
11461180

1181+
timer4.stop();
1182+
timer4.flush();
1183+
11471184
// Send and receive ((input vertex index) -> (new global index, owner
11481185
// rank)) data with neighbours (for vertices on 'true domain
11491186
// boundary')
1187+
common::Timer timer5("Topology: 5");
1188+
11501189
const std::vector<std::int64_t> unowned_vertex_data = exchange_indexing(
11511190
comm, boundary_vertices, global_vertex_to_ranks, global_offset_v,
11521191
owned_vertices, local_vertex_indices);
11531192
assert(unowned_vertex_data.size() % 3 == 0);
11541193

1194+
timer5.stop();
1195+
timer5.flush();
1196+
11551197
// Unpack received data and build array of ghost vertices and owners
11561198
// of the ghost vertices
11571199
std::vector<std::int64_t> ghost_vertices;
11581200
std::vector<int> ghost_vertex_owners;
11591201
std::vector<std::int32_t> local_vertex_indices_unowned(
11601202
unowned_vertices.size(), -1);
11611203
{
1204+
common::Timer timer6("Topology: 6");
1205+
11621206
std::int32_t v = owned_vertices.size();
11631207
for (std::size_t i = 0; i < unowned_vertex_data.size(); i += 3)
11641208
{
@@ -1244,14 +1288,18 @@ Topology mesh::create_topology(
12441288
{ return {idx0, idx1}; });
12451289
std::ranges::sort(global_to_local_vertices);
12461290

1291+
common::Timer timer7("Topology: 7");
12471292
std::vector<std::vector<std::int32_t>> _cells_local_idx;
12481293
_cells_local_idx.reserve(cells.size());
12491294
for (std::span<const std::int64_t> c : cells)
12501295
{
12511296
_cells_local_idx.push_back(
1252-
convert_to_local_indexing(c, global_to_local_vertices));
1297+
convert_to_local_indexing(c, global_to_local_vertices, num_threads));
12531298
}
12541299

1300+
timer7.stop();
1301+
timer7.flush();
1302+
12551303
// -- Create Topology object
12561304

12571305
// Determine which ranks ghost vertices that are owned by this rank.
@@ -1309,11 +1357,12 @@ Topology
13091357
mesh::create_topology(MPI_Comm comm, std::span<const std::int64_t> cells,
13101358
std::span<const std::int64_t> original_cell_index,
13111359
std::span<const int> ghost_owners, CellType cell_type,
1312-
std::span<const std::int64_t> boundary_vertices)
1360+
std::span<const std::int64_t> boundary_vertices,
1361+
int num_threads)
13131362
{
13141363
spdlog::info("Create topology (single cell type)");
13151364
return create_topology(comm, {cell_type}, {cells}, {original_cell_index},
1316-
{ghost_owners}, boundary_vertices);
1365+
{ghost_owners}, boundary_vertices, num_threads);
13171366
}
13181367
//-----------------------------------------------------------------------------
13191368
std::tuple<Topology, std::vector<int32_t>, std::vector<int32_t>>

0 commit comments

Comments
 (0)