Skip to content

Scalable point location - #5396

Open
leo-collins wants to merge 154 commits into
mainfrom
leo/distributed-rtree-refactor
Open

Scalable point location#5396
leo-collins wants to merge 154 commits into
mainfrom
leo/distributed-rtree-refactor

Conversation

@leo-collins

Copy link
Copy Markdown
Contributor

Introduces the partition_rtree, which is an R-tree of bounding boxes covering each partition of the mesh. This R-tree is replicated on all ranks by allgathering the bounding boxes. The bounding boxes on each rank are obtained by traversing down the local R-tree. Each rank may submit more than one bounding box to the partition_rtree (we want to do this to minimise candidates since the mesh partitions may not be nice and convex, and might even be disconnected).

We choose the number of bounding boxes via a simple heuristic (we stop traversing when the total volume of the bounding boxes stop decreasing - see _box_ratio_heuristic). This should probably be improved to take into account the total number of bounding boxes so we don't blow up the allgather.

Constructing the partition rtree uses an allgather, although each rank normally submits O(10s) of boxes so I have not seen this kill us yet when testing up to 2048 ranks. If doing huge number of cores we might want to consider implementing some sort of hierarchy of partition rtrees.

Each rank queries this R-tree with the points local to that rank to determine the 'candidate' ranks for each point. We then build the candidate_sf which is a PETScSF mapping input points to their candidates. The local cell location process proceeds as usual, solving parallel conflicts with broadcasts/reductions via the candidate_sf.

This current implementation gets around 80% weak scaling efficiency in my tests up to 2048 cores.

doing this fixes test errors...
@leo-collins leo-collins added ci:complex Run the test suite in complex mode ci:macos Run the test suite on the Mac runners ci:int64 Do a 64 bit integer build labels Aug 27, 2026
@leo-collins
leo-collins marked this pull request as ready for review August 29, 2026 17:55
Comment thread firedrake/cython/rtree.pyx
Comment thread firedrake/cython/rtree.pyx Outdated
nleaves += count

if not sends_complete:
CHKERRMPI(MPI_Testall(nranks_to, requests, &sends_complete, MPI_STATUSES_IGNORE))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
CHKERRMPI(MPI_Testall(nranks_to, requests, &sends_complete, MPI_STATUSES_IGNORE))
CHKERRMPI(MPI_Testall(nranks_to, send_requests, &sends_complete, MPI_STATUSES_IGNORE))

Comment thread firedrake/mesh.py Outdated

@connorjward connorjward left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still quite confused by this PR. There is a lot going on.

Comment thread requirements-build.txt
Comment thread firedrake/mesh.py
Comment thread firedrake/mesh.py Outdated
self._parent_mesh = parentmesh

super().__init__(swarm, name, reorder, None, perm_is, distribution_name, permutation_name, parentmesh.comm)
super().__init__(swarm.dm, name, reorder, None, perm_is, distribution_name, permutation_name, parentmesh.comm)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now means that we have to keep track of the Firedrake swarm object as well as the PETSc one. I would prefer it if they were the same, either by keeping the current inheritance pattern or by adding PETSc.DMSwarm methods to FiredrakeDMSwarm. I think arguably the latter makes more sense because then (e.g.) swarm.clone() will give you back something with the right type.

Therefore I propose:

Suggested change
super().__init__(swarm.dm, name, reorder, None, perm_is, distribution_name, permutation_name, parentmesh.comm)
super().__init__(swarm, name, reorder, None, perm_is, distribution_name, permutation_name, parentmesh.comm)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think inheritance will be easier. AbstractMeshTopology calls a bunch of cython routines which expect a PETSc object.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively make it a DMShell?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems like an awful lot of effort for little gain

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't mind what approach you take. I just really think that swarm and topology_dm should be the same object. I would eventually like to drop VertexOnlyMesh and fold everything into a single Mesh class. We shouldn't be adding additional differences between the classes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made FiredrakeDMSwarm inherit from PETSc.DMSwarm.

Comment thread firedrake/mesh.py Outdated
mesh cell order.
"""
with self.topology_dm.field("parentcellnum") as parentcellnum_field:
with self.swarm.field("parentcellnum") as parentcellnum_field:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And if you did my suggestion then all of these changes would not be needed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Undone these now

Comment thread firedrake/mesh.py
Comment thread firedrake/mesh.py Outdated
Comment on lines +4016 to +4024
"""Initialize a Firedrake DMSwarm.

Parameters
----------
dm : PETSc.DMSwarm
The underlying PETSc DMSwarm.
extruded : bool
Whether the swarm is embedded in an extruded mesh.
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put this in the main class docstring (that's what numpydoc suggests). It also means you don't have to write "Initialize a Firedrake DMSwarm.", which is trivially obvious.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread firedrake/mesh.py Outdated
if not isinstance(dm, PETSc.DMSwarm):
raise TypeError(f"`dm` must be a `PETSc.DMSwarm`, not a {type(dm).__name__}")
self.dm = dm
self.extruded = extruded

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be inferred from whether parentcellbasenum exists?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But we create parentcellbasenum based on whether extruded is true or false...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that true? In https://github.com/firedrakeproject/firedrake/pull/5396/changes#diff-7b656fa2d868bdf9f07513bd2565b7b30f1896c30352e3d8b92ae435ec3befd2R4115 it looks like you create the field independently.

As an alternative phrasing, do you ever actually inspect FiredrakeDMSwarm.extruded? A quick search makes me think that you don't.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh right that is extruded and not self.extruded...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread firedrake/mesh.py Outdated
return rtree.build_from_aabb(coords_min, coords_max)

@PETSc.Log.EventDecorator()
def bounding_boxes_total_volume(self, bounding_boxes: np.ndarray):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make a private attribute? can't imagine users wanting this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread firedrake/mesh.py Outdated
return self._rtree
return rtree.build_from_aabb(coords_min, coords_max)

@PETSc.Log.EventDecorator()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@PETSc.Log.EventDecorator()

looks trivial, doesn't need to be profiled

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread firedrake/mesh.py
owner_ranks_data,
npoints,
cells_ignore.shape[1],
cells_ignore,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought this was going?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The locate_cells_.. methods are all public API that need this functionality

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't imagine anyone using cells_ignore in their own code, but fine. Maybe raise a deprecation warning?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will open another PR to do this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. Once you open that PR (or create an issue) then this can be resolved

@connorjward connorjward left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, read the whole thing now. This is really heroic.

Comment thread firedrake/cython/rtree.pyx Outdated
send_requests = <MPI_Request *>malloc(nranks_to * sizeof(MPI_Request))

# Post non-blocking synchronous sends. 'Synchronous' means that the send is not
# considered as complete by MPI_Test until the destination starts the matching receive.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this important?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment

Comment thread firedrake/cython/rtree.pyx
Comment thread firedrake/locate.c
else {
current_ref_cell_dist_l1 = (*try_candidate)(temp_ref_coords, f, candidate, x);
distance = (*try_candidate)(temp_ref_coords, f, candidate, x);
owner = cell_owner_ranks ? cell_owner_ranks[candidate] : 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a backwards compatibility thing? (cell_owner_ranks may be undefined?) If so a comment wouldn't go amiss.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Added a comment

Comment thread firedrake/locate.c
/* Found cell! */
*cell_out = candidate;
/* Select owning cell by minimum L1 distance, with ties broken by highest owning rank. */
if (distance < best_distance || (distance == best_distance && owner > best_owner)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still worry about this. Consider an interval mesh like

global:
x-----x-----x
      o

rank 0:
x-----x
      o

rank 1:
      x-----x
      o

(which is allowed since we don't always need a full cell of overlap)

If o is the point we want to locate then rank 0 and rank 1 both have one entry in cell_owner_ranks - they will each think that they own the point.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A rank doesn't decide at this point that they own the point. locate.c just determines whether the point lies in one of their owned cells (and L^1 distances and ref coords, etc.).

The 'global' selection in _parent_mesh_embedding is what determines the owning rank. In this case the points both have a zero L^1 distance, so the owner is going to be rank 1 since we break ties by choosing the highest owning rank.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it work if both ranks think that the other rank is the owner?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm reading your example correctly, rank 0 can see no cells of rank 1 and vice-versa. So I can't see how a rank could think the point lived in a cell owned by the other rank in this case.

If we do have halos then this can happen, but by selecting the single highest owning rank we can never end up that situation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't apply to the example I gave earlier. Only if there are halos.

I guess my concern comes down to my mental model of floating point in parallel. Can we be sure that two ranks will compute the distance of a point on the cell boundary as exactly zero under any circumstance?

Comment thread firedrake/mesh.py
):
parent_order = parent_renum_inv[swarm_parent_cell_nums.ravel() - pStart]
# sort by parent cell order, with ties broken by point global index
perm = np.lexsort((swarm_global_indices.ravel(), parent_order)).astype(IntType)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised that this needs any global information at all. This numbering is normally done with only local information.

Comment thread firedrake/mesh.py
owner_ranks_data,
npoints,
cells_ignore.shape[1],
cells_ignore,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. Once you open that PR (or create an issue) then this can be resolved

Comment thread firedrake/mesh.py
root_values: np.ndarray,
leaf_values: np.ndarray,
) -> None:
# TODO: make these collective

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I worry that that would be quite expensive

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could add a collective: bool argument which is False by default? I found these checks quite useful for debugging purposes.

Comment thread firedrake/mesh.py
swarm.set_field("inputindex", embedded_sf.input_indices[swarm_indices].astype(IntType))
if parent_mesh.extruded:
swarm.set_field("parentcellbasenum", swarm_base_cells)
swarm.set_field("parentcellextrusionheight", swarm_extrusion_heights)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are some fields set in create_with_fields but not all?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create_with_fields registers our fields but doesn't set them.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that the best API?

Comment thread firedrake/mesh.py
keep &= ref_cell_dists == candidate_sf.broadcast(root_distance_min)

# multiple ranks may claim the minimum L1 distance. Break ties
# by choosing the highest numbered rank.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're doing this then I wonder if the parallel issue I flagged above is applicable. Could owned_cell_ranks just go? The C code could just see if a rank claims to own the point or not.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you only need rank information here, not there as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to have rank information inside locate.c as long as we support exclude_halos=False (which is what VertexOnlyMesh uses).

In the future I want to get rid of this behaviour (i.e. always exclude halos from the final VOM) because a) the idea of a halo cell in a vom doesn't really make sense to me and we don't use this anywhere, and b) it substantially simplifies the vom construction code.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I have always felt that halo points in a VoM was the wrong thing to do.

You need halos in order to build the correct stencils, but VoMs have no connectivity and so it's really pointless.

Happy to punt on this for now, I just always think it's worth discussing the design of these things.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:complex Run the test suite in complex mode ci:int64 Do a 64 bit integer build ci:macos Run the test suite on the Mac runners

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants