Scalable point location - #5396
Conversation
more logging
one more
doing this fixes test errors...
| nleaves += count | ||
|
|
||
| if not sends_complete: | ||
| CHKERRMPI(MPI_Testall(nranks_to, requests, &sends_complete, MPI_STATUSES_IGNORE)) |
There was a problem hiding this comment.
| CHKERRMPI(MPI_Testall(nranks_to, requests, &sends_complete, MPI_STATUSES_IGNORE)) | |
| CHKERRMPI(MPI_Testall(nranks_to, send_requests, &sends_complete, MPI_STATUSES_IGNORE)) |
connorjward
left a comment
There was a problem hiding this comment.
I am still quite confused by this PR. There is a lot going on.
| 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) |
There was a problem hiding this comment.
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:
| 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) |
There was a problem hiding this comment.
I think inheritance will be easier. AbstractMeshTopology calls a bunch of cython routines which expect a PETSc object.
There was a problem hiding this comment.
Alternatively make it a DMShell?
There was a problem hiding this comment.
That seems like an awful lot of effort for little gain
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Made FiredrakeDMSwarm inherit from PETSc.DMSwarm.
| mesh cell order. | ||
| """ | ||
| with self.topology_dm.field("parentcellnum") as parentcellnum_field: | ||
| with self.swarm.field("parentcellnum") as parentcellnum_field: |
There was a problem hiding this comment.
And if you did my suggestion then all of these changes would not be needed
There was a problem hiding this comment.
Undone these now
| """Initialize a Firedrake DMSwarm. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| dm : PETSc.DMSwarm | ||
| The underlying PETSc DMSwarm. | ||
| extruded : bool | ||
| Whether the swarm is embedded in an extruded mesh. | ||
| """ |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
should this be inferred from whether parentcellbasenum exists?
There was a problem hiding this comment.
But we create parentcellbasenum based on whether extruded is true or false...
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Oh right that is extruded and not self.extruded...
| return rtree.build_from_aabb(coords_min, coords_max) | ||
|
|
||
| @PETSc.Log.EventDecorator() | ||
| def bounding_boxes_total_volume(self, bounding_boxes: np.ndarray): |
There was a problem hiding this comment.
make a private attribute? can't imagine users wanting this
| return self._rtree | ||
| return rtree.build_from_aabb(coords_min, coords_max) | ||
|
|
||
| @PETSc.Log.EventDecorator() |
There was a problem hiding this comment.
| @PETSc.Log.EventDecorator() |
looks trivial, doesn't need to be profiled
| owner_ranks_data, | ||
| npoints, | ||
| cells_ignore.shape[1], | ||
| cells_ignore, |
There was a problem hiding this comment.
I thought this was going?
There was a problem hiding this comment.
The locate_cells_.. methods are all public API that need this functionality
There was a problem hiding this comment.
I can't imagine anyone using cells_ignore in their own code, but fine. Maybe raise a deprecation warning?
There was a problem hiding this comment.
I will open another PR to do this.
There was a problem hiding this comment.
OK. Once you open that PR (or create an issue) then this can be resolved
Co-authored-by: Connor Ward <c.ward20@imperial.ac.uk>
connorjward
left a comment
There was a problem hiding this comment.
Right, read the whole thing now. This is really heroic.
| 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. |
| 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; |
There was a problem hiding this comment.
Is this a backwards compatibility thing? (cell_owner_ranks may be undefined?) If so a comment wouldn't go amiss.
There was a problem hiding this comment.
Yes. Added a comment
| /* 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)) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Will it work if both ranks think that the other rank is the owner?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
| ): | ||
| 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) |
There was a problem hiding this comment.
I'm surprised that this needs any global information at all. This numbering is normally done with only local information.
| owner_ranks_data, | ||
| npoints, | ||
| cells_ignore.shape[1], | ||
| cells_ignore, |
There was a problem hiding this comment.
OK. Once you open that PR (or create an issue) then this can be resolved
| root_values: np.ndarray, | ||
| leaf_values: np.ndarray, | ||
| ) -> None: | ||
| # TODO: make these collective |
There was a problem hiding this comment.
I worry that that would be quite expensive
There was a problem hiding this comment.
Maybe we could add a collective: bool argument which is False by default? I found these checks quite useful for debugging purposes.
| 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) |
There was a problem hiding this comment.
Why are some fields set in create_with_fields but not all?
There was a problem hiding this comment.
create_with_fields registers our fields but doesn't set them.
| 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I think you only need rank information here, not there as well.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
fix typo Co-authored-by: Connor Ward <c.ward20@imperial.ac.uk>
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_sfwhich 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.