libMesh
metis_partitioner.C
Go to the documentation of this file.
1 // The libMesh Finite Element Library.
2 // Copyright (C) 2002-2025 Benjamin S. Kirk, John W. Peterson, Roy H. Stogner
3 
4 // This library is free software; you can redistribute it and/or
5 // modify it under the terms of the GNU Lesser General Public
6 // License as published by the Free Software Foundation; either
7 // version 2.1 of the License, or (at your option) any later version.
8 
9 // This library is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 // Lesser General Public License for more details.
13 
14 // You should have received a copy of the GNU Lesser General Public
15 // License along with this library; if not, write to the Free Software
16 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 
18 
19 
20 // Local Includes
21 #include "libmesh/metis_partitioner.h"
22 
23 #include "libmesh/libmesh_config.h"
24 #include "libmesh/elem.h"
25 #include "libmesh/enum_partitioner_type.h"
26 #include "libmesh/error_vector.h"
27 #include "libmesh/libmesh_logging.h"
28 #include "libmesh/mesh_base.h"
29 #include "libmesh/mesh_communication.h"
30 #include "libmesh/mesh_tools.h"
31 #include "libmesh/metis_csr_graph.h"
32 #include "libmesh/parallel.h"
33 #include "libmesh/utility.h"
34 #include "libmesh/vectormap.h"
35 
36 #ifdef LIBMESH_HAVE_METIS
37 // MIPSPro 7.4.2 gets confused about these nested namespaces
38 # ifdef __sgi
39 # include <cstdarg>
40 # endif
41 namespace Metis {
42 extern "C" {
43 # include "libmesh/ignore_warnings.h"
44 # include "metis.h"
45 # include "libmesh/restore_warnings.h"
46 }
47 }
48 #else
49 # include "libmesh/sfc_partitioner.h"
50 #endif
51 
52 
53 // C++ includes
54 #include <map>
55 #include <unordered_map>
56 #include <vector>
57 
58 
59 namespace libMesh
60 {
61 
62 
64 {
65  return METIS_PARTITIONER;
66 }
67 
68 
72  unsigned int n_pieces)
73 {
74  // Check for easy returns
75  if (beg == end)
76  return;
77 
78  if (n_pieces == 1)
79  {
80  this->single_partition_range (beg, end);
81  return;
82  }
83 
84  libmesh_assert_greater (n_pieces, 0);
85 
86  // We don't yet support distributed meshes with this Partitioner
87  if (!mesh.is_serial())
88  {
89  libMesh::out << "WARNING: Forced to gather a distributed mesh for METIS" << std::endl;
90  mesh.allgather();
91  }
92 
93  // What to do if the Metis library IS NOT present
94 #ifndef LIBMESH_HAVE_METIS
95 
96  libmesh_do_once(
97  libMesh::out << "ERROR: The library has been built without" << std::endl
98  << "Metis support. Using a space-filling curve" << std::endl
99  << "partitioner instead!" << std::endl;);
100 
101  SFCPartitioner sfcp;
102  sfcp.partition_range (mesh, beg, end, n_pieces);
103 
104  // What to do if the Metis library IS present
105 #else
106 
107  LOG_SCOPE("partition_range()", "MetisPartitioner");
108 
109  const dof_id_type n_range_elem = cast_int<dof_id_type>(std::distance(beg, end));
110 
111  // Metis will only consider the elements in the range.
112  // We need to map the range element ids into a
113  // contiguous range. Further, we want the unique range indexing to be
114  // independent of the element ordering, otherwise a circular dependency
115  // can result in which the partitioning depends on the ordering which
116  // depends on the partitioning...
117  vectormap<dof_id_type, dof_id_type> global_index_map;
118  global_index_map.reserve (n_range_elem);
119 
120  {
121  std::vector<dof_id_type> global_index;
122 
125  beg, end, global_index);
126 
127  libmesh_assert_equal_to (global_index.size(), n_range_elem);
128 
129  // If we have unique_id() then global_index entries should be
130  // unique, so we just need a map for them.
131 #ifdef LIBMESH_ENABLE_UNIQUE_ID
132  std::size_t cnt=0;
133  for (const auto & elem : as_range(beg, end))
134  global_index_map.emplace(elem->id(), global_index[cnt++]);
135 #else
136  // If we don't have unique_id() then Hilbert SFC based
137  // global_index entries might overlap if elements overlap, so we
138  // need to fix any duplicates ...
139  //
140  // First, check for duplicates in O(N) time
141  std::unordered_map<dof_id_type, std::vector<dof_id_type>>
142  global_index_inverses;
143  bool found_duplicate_indices = false;
144  {
145  std::size_t cnt=0;
146  for (const auto & elem : as_range(beg, end))
147  {
148  auto & vec = global_index_inverses[global_index[cnt++]];
149  if (!vec.empty())
150  found_duplicate_indices = true;
151  vec.push_back(elem->id());
152  }
153  }
154 
155  // But if we find them, we'll need O(N log N) to fix them while
156  // retaining the same space-filling-curve for efficient initial
157  // partitioning.
158  if (found_duplicate_indices)
159  {
160  const std::map<dof_id_type, std::vector<dof_id_type>>
161  sorted_inverses(global_index_inverses.begin(),
162  global_index_inverses.end());
163  std::size_t new_global_index=0;
164  for (const auto & [index, elem_ids] : sorted_inverses)
165  for (const dof_id_type elem_id : elem_ids)
166  global_index_map.emplace(elem_id, new_global_index++);
167  }
168  else
169  {
170  std::size_t cnt=0;
171  for (const auto & elem : as_range(beg, end))
172  global_index_map.emplace(elem->id(), global_index[cnt++]);
173  }
174 #endif
175 
176  libmesh_assert_equal_to (global_index_map.size(), n_range_elem);
177 
178 #ifdef DEBUG
179  std::unordered_set<dof_id_type> distinct_global_indices, distinct_ids;
180  for (const auto & [elem_id, index] : global_index_map)
181  {
182  distinct_ids.insert(elem_id);
183  distinct_global_indices.insert(index);
184  }
185  libmesh_assert_equal_to (distinct_global_indices.size(), n_range_elem);
186  libmesh_assert_equal_to (distinct_ids.size(), n_range_elem);
187 #endif
188  }
189 
190  // If we have boundary elements in this mesh, we want to account for
191  // the connectivity between them and interior elements. We can find
192  // interior elements from boundary elements, but we need to build up
193  // a lookup map to do the reverse.
194  typedef std::unordered_multimap<const Elem *, const Elem *> map_type;
195  map_type interior_to_boundary_map;
196 
197  for (const auto & elem : as_range(beg, end))
198  {
199  // If we don't have an interior_parent then there's nothing
200  // to look us up.
201  if ((elem->dim() >= LIBMESH_DIM) ||
202  !elem->interior_parent())
203  continue;
204 
205  // get all relevant interior elements
206  std::set<Elem *> neighbor_set;
207  elem->find_interior_neighbors(neighbor_set);
208 
209  for (auto & neighbor : neighbor_set)
210  interior_to_boundary_map.emplace(neighbor, elem);
211  }
212 
213  // Data structure that Metis will fill up on processor 0 and broadcast.
214  std::vector<Metis::idx_t> part(n_range_elem);
215 
216  // Invoke METIS, but only on processor 0.
217  // Then broadcast the resulting decomposition
218  if (mesh.processor_id() == 0)
219  {
220  // Data structures and parameters needed only on processor 0 by Metis.
221  // std::vector<Metis::idx_t> options(5);
222  std::vector<Metis::idx_t> vwgt(n_range_elem);
223 
224  Metis::idx_t
225  n = static_cast<Metis::idx_t>(n_range_elem), // number of "nodes" (elements) in the graph
226  // wgtflag = 2, // weights on vertices only, none on edges
227  // numflag = 0, // C-style 0-based numbering
228  nparts = static_cast<Metis::idx_t>(n_pieces), // number of subdomains to create
229  edgecut = 0; // the numbers of edges cut by the resulting partition
230 
231  // Set the options
232  // options[0] = 0; // use default options
233 
234  // build the graph
236 
237  csr_graph.offsets.resize(n_range_elem + 1, 0);
238 
239  // Local scope for these
240  {
241  // build the graph in CSR format. Note that
242  // the edges in the graph will correspond to
243  // face neighbors
244 
245 #ifdef LIBMESH_ENABLE_AMR
246  std::vector<const Elem *> neighbors_offspring;
247 #endif
248 
249 #ifndef NDEBUG
250  std::size_t graph_size=0;
251 #endif
252 
253  // (1) first pass - get the row sizes for each element by counting the number
254  // of face neighbors. Also populate the vwght array if necessary
255  for (const auto & elem : as_range(beg, end))
256  {
257  const dof_id_type elem_global_index =
258  global_index_map[elem->id()];
259 
260  libmesh_assert_less (elem_global_index, vwgt.size());
261 
262  // The weight is used to define what a balanced graph is
263  if (!_weights)
264  {
265  // Spline nodes are a special case (storing all the
266  // unconstrained DoFs in an IGA simulation), but in
267  // general we'll try to distribute work by expecting
268  // it to be roughly proportional to DoFs, which are
269  // roughly proportional to nodes.
270  if (elem->type() == NODEELEM &&
271  elem->mapping_type() == RATIONAL_BERNSTEIN_MAP)
272  vwgt[elem_global_index] = 50;
273  else
274  vwgt[elem_global_index] = elem->n_nodes();
275  }
276  else
277  vwgt[elem_global_index] = static_cast<Metis::idx_t>((*_weights)[elem->id()]);
278 
279  unsigned int num_neighbors = 0;
280 
281  // Loop over the element's neighbors. An element
282  // adjacency corresponds to a face neighbor
283  for (auto neighbor : elem->neighbor_ptr_range())
284  {
285  if (neighbor != nullptr)
286  {
287  // If the neighbor is active, but is not in the
288  // range of elements being partitioned, treat it
289  // as a nullptr neighbor.
290  if (neighbor->active() && !global_index_map.count(neighbor->id()))
291  continue;
292 
293  // If the neighbor is active treat it
294  // as a connection
295  if (neighbor->active())
296  num_neighbors++;
297 
298 #ifdef LIBMESH_ENABLE_AMR
299 
300  // Otherwise we need to find all of the
301  // neighbor's children that are connected to
302  // us and add them
303  else
304  {
305  // The side of the neighbor to which
306  // we are connected
307  const unsigned int ns =
308  neighbor->which_neighbor_am_i (elem);
309  libmesh_assert_less (ns, neighbor->n_neighbors());
310 
311  // Get all the active children (& grandchildren, etc...)
312  // of the neighbor.
313 
314  // FIXME - this is the wrong thing, since we
315  // should be getting the active family tree on
316  // our side only. But adding too many graph
317  // links may cause hanging nodes to tend to be
318  // on partition interiors, which would reduce
319  // communication overhead for constraint
320  // equations, so we'll leave it.
321  neighbor->active_family_tree (neighbors_offspring);
322 
323  // Get all the neighbor's children that
324  // live on that side and are thus connected
325  // to us
326  for (const auto & child : neighbors_offspring)
327  {
328  // Skip neighbor offspring which are not in the range of elements being partitioned.
329  if (!global_index_map.count(child->id()))
330  continue;
331 
332  // This does not assume a level-1 mesh.
333  // Note that since children have sides numbered
334  // coincident with the parent then this is a sufficient test.
335  if (child->neighbor_ptr(ns) == elem)
336  {
337  libmesh_assert (child->active());
338  num_neighbors++;
339  }
340  }
341  }
342 
343 #endif /* ifdef LIBMESH_ENABLE_AMR */
344 
345  }
346  }
347 
348  // Check for any interior neighbors
349  if ((elem->dim() < LIBMESH_DIM) && elem->interior_parent())
350  {
351  // get all relevant interior elements
352  std::set<const Elem *> neighbor_set;
353  elem->find_interior_neighbors(neighbor_set);
354 
355  num_neighbors += cast_int<unsigned int>(neighbor_set.size());
356  }
357 
358  // Check for any boundary neighbors
359  typedef map_type::iterator map_it_type;
360  std::pair<map_it_type, map_it_type>
361  bounds = interior_to_boundary_map.equal_range(elem);
362  num_neighbors += cast_int<unsigned int>
363  (std::distance(bounds.first, bounds.second));
364 
365  // Whether we enabled unique_id or had to fix any overlaps
366  // by hand, our global indices should be unique, so we
367  // should never see the same elem_global_index twice, so
368  // we should never be trying to edit an already-nonzero
369  // offset.
370  libmesh_assert(!csr_graph.offsets[elem_global_index+1]);
371 
372  csr_graph.prep_n_nonzeros(elem_global_index, num_neighbors);
373 #ifndef NDEBUG
374  graph_size += num_neighbors;
375 #endif
376  }
377 
378  csr_graph.prepare_for_use();
379 
380  // (2) second pass - fill the compressed adjacency array
381  for (const auto & elem : as_range(beg, end))
382  {
383  const dof_id_type elem_global_index =
384  global_index_map[elem->id()];
385 
386  unsigned int connection=0;
387 
388  // Loop over the element's neighbors. An element
389  // adjacency corresponds to a face neighbor
390  for (auto neighbor : elem->neighbor_ptr_range())
391  {
392  if (neighbor != nullptr)
393  {
394  // If the neighbor is active, but is not in the
395  // range of elements being partitioned, treat it
396  // as a nullptr neighbor.
397  if (neighbor->active() && !global_index_map.count(neighbor->id()))
398  continue;
399 
400  // If the neighbor is active treat it
401  // as a connection
402  if (neighbor->active())
403  csr_graph(elem_global_index, connection++) = global_index_map[neighbor->id()];
404 
405 #ifdef LIBMESH_ENABLE_AMR
406 
407  // Otherwise we need to find all of the
408  // neighbor's children that are connected to
409  // us and add them
410  else
411  {
412  // The side of the neighbor to which
413  // we are connected
414  const unsigned int ns =
415  neighbor->which_neighbor_am_i (elem);
416  libmesh_assert_less (ns, neighbor->n_neighbors());
417 
418  // Get all the active children (& grandchildren, etc...)
419  // of the neighbor.
420  neighbor->active_family_tree (neighbors_offspring);
421 
422  // Get all the neighbor's children that
423  // live on that side and are thus connected
424  // to us
425  for (const auto & child : neighbors_offspring)
426  {
427  // Skip neighbor offspring which are not in the range of elements being partitioned.
428  if (!global_index_map.count(child->id()))
429  continue;
430 
431  // This does not assume a level-1 mesh.
432  // Note that since children have sides numbered
433  // coincident with the parent then this is a sufficient test.
434  if (child->neighbor_ptr(ns) == elem)
435  {
436  libmesh_assert (child->active());
437 
438  csr_graph(elem_global_index, connection++) = global_index_map[child->id()];
439  }
440  }
441  }
442 
443 #endif /* ifdef LIBMESH_ENABLE_AMR */
444 
445  }
446  }
447 
448  if ((elem->dim() < LIBMESH_DIM) &&
449  elem->interior_parent())
450  {
451  // get all relevant interior elements
452  std::set<const Elem *> neighbor_set;
453  elem->find_interior_neighbors(neighbor_set);
454 
455  for (const Elem * neighbor : neighbor_set)
456  {
457  // Not all interior neighbors are necessarily in
458  // the same Mesh (hence not in the global_index_map).
459  // This will be the case when partitioning a
460  // BoundaryMesh, whose elements all have
461  // interior_parents() that belong to some other
462  // Mesh.
463  const Elem * queried_elem = mesh.query_elem_ptr(neighbor->id());
464 
465  // Compare the neighbor and the queried_elem
466  // pointers, make sure they are the same.
467  if (queried_elem && queried_elem == neighbor)
468  csr_graph(elem_global_index, connection++) =
469  libmesh_map_find(global_index_map, neighbor->id());
470  }
471  }
472 
473  // Check for any boundary neighbors
474  for (const auto & pr : as_range(interior_to_boundary_map.equal_range(elem)))
475  {
476  const Elem * neighbor = pr.second;
477  csr_graph(elem_global_index, connection++) =
478  global_index_map[neighbor->id()];
479  }
480  }
481 
482  // We create a non-empty vals for a disconnected graph, to
483  // work around a segfault from METIS.
484  libmesh_assert_equal_to (csr_graph.vals.size(),
485  std::max(graph_size, std::size_t(1)));
486  } // done building the graph
487 
488  Metis::idx_t ncon = 1;
489 
490  // Select which type of partitioning to create
491 
492  // Use recursive if the number of partitions is less than or equal to 8
493  if (n_pieces <= 8)
494  Metis::METIS_PartGraphRecursive(&n,
495  &ncon,
496  csr_graph.offsets.data(),
497  csr_graph.vals.data(),
498  vwgt.data(),
499  nullptr,
500  nullptr,
501  &nparts,
502  nullptr,
503  nullptr,
504  nullptr,
505  &edgecut,
506  part.data());
507 
508  // Otherwise use kway
509  else
510  Metis::METIS_PartGraphKway(&n,
511  &ncon,
512  csr_graph.offsets.data(),
513  csr_graph.vals.data(),
514  vwgt.data(),
515  nullptr,
516  nullptr,
517  &nparts,
518  nullptr,
519  nullptr,
520  nullptr,
521  &edgecut,
522  part.data());
523 
524  } // end processor 0 part
525 
526  // Broadcast the resulting partition
527  mesh.comm().broadcast(part);
528 
529  // Assign the returned processor ids. The part array contains
530  // the processor id for each active element, but in terms of
531  // the contiguous indexing we defined above
532  for (auto & elem : as_range(beg, end))
533  {
534  libmesh_assert (global_index_map.count(elem->id()));
535 
536  const dof_id_type elem_global_index =
537  global_index_map[elem->id()];
538 
539  libmesh_assert_less (elem_global_index, part.size());
540  const processor_id_type elem_procid =
541  static_cast<processor_id_type>(part[elem_global_index]);
542 
543  elem->processor_id() = elem_procid;
544  }
545 #endif
546 }
547 
548 
549 
551  const unsigned int n_pieces)
552 {
553  this->partition_range(mesh,
554  mesh.active_elements_begin(),
555  mesh.active_elements_end(),
556  n_pieces);
557 }
558 
559 } // namespace libMesh
virtual void partition_range(MeshBase &mesh, MeshBase::element_iterator it, MeshBase::element_iterator end, const unsigned int n) override
Called by the SubdomainPartitioner to partition elements in the range (it, end).
The definition of the element_iterator struct.
Definition: mesh_base.h:2198
void find_global_indices(const Parallel::Communicator &communicator, const libMesh::BoundingBox &, const ForwardIterator &, const ForwardIterator &, std::vector< dof_id_type > &) const
This method determines a globally unique, partition-agnostic index for each object in the input range...
bool single_partition_range(MeshBase::element_iterator it, MeshBase::element_iterator end)
Slightly generalized version of single_partition which acts on a range of elements defined by the pai...
Definition: partitioner.C:320
This utility class provides a convenient implementation for building the compressed-row-storage graph...
libMesh::BoundingBox create_bounding_box(const MeshBase &mesh)
Definition: mesh_tools.C:559
virtual void partition_range(MeshBase &mesh, MeshBase::element_iterator it, MeshBase::element_iterator end, const unsigned int n) override
Called by the SubdomainPartitioner to partition elements in the range (it, end).
This is the base class from which all geometric element types are derived.
Definition: elem.h:94
MeshBase & mesh
The libMesh namespace provides an interface to certain functionality in the library.
Real distance(const Point &p)
uint8_t processor_id_type
Definition: id_types.h:104
This is the MeshBase class.
Definition: mesh_base.h:75
ErrorVector * _weights
The weights that might be used for partitioning.
Definition: partitioner.h:281
std::vector< IndexType > offsets
PartitionerType
Defines an enum for mesh partitioner types.
This is the MeshCommunication class.
dof_id_type id() const
Definition: dof_object.h:828
SimpleRange< IndexType > as_range(const std::pair< IndexType, IndexType > &p)
Helper function that allows us to treat a homogenous pair as a range.
Definition: simple_range.h:57
libmesh_assert(ctx)
std::vector< IndexType > vals
virtual void _do_partition(MeshBase &mesh, const unsigned int n) override
Partition the MeshBase into n subdomains.
The SFCPartitioner uses a Hilbert or Morton-ordered space filling curve to partition the elements...
OStreamProxy out
void prepare_for_use()
Replaces the entries in the offsets vector with their partial sums and resizes the val vector accordi...
void prep_n_nonzeros(const libMesh::dof_id_type row, const libMesh::dof_id_type n_nonzeros_in)
Sets the number of non-zero values in the given row.
This vectormap templated class is intended to provide the performance characteristics of a sorted s...
Definition: vectormap.h:61
virtual PartitionerType type() const override
uint8_t dof_id_type
Definition: id_types.h:67