Distribute halos to ranks balancing particle count - #117
Conversation
|
Improving the domain decomposition is definitely a good idea, thanks for kickstarting the discussion! For the failed test you get, I believe it is indeed independent from your changes. See #102. |
|
Was mulling it over on my way home and I think that a better starting point would be a sorted list of halos by particle number, then assigning them in descending order in a round robin to the ranks. Once a rank is "full" it can be skipped in the round robin. That should end up quite close to being memory-balanced in most cases and won't do the worst possible job of cpu balance. |
|
I agree. This way the biggest haloes are guaranteed to be in separate ranks. This is similar to what subfind in gadget4 does. |
|
Something else that will help with testing this implementation: we are planning to add (debug-enabled) timers to each step in the unbinding of individual subhaloes and haloes, and which MPI rank is analysing them. This will help identify bottlenecks, and we plan on adding it sooner rather than later. |
33782f5 to
05da6bc
Compare
|
Hi Victor - could I bring this back onto the radar? I've rebased on current main and I think I've proposed a sane round robin-based scheme now. If this looks sensible to you, could you offer some guidance on testing and what else would need to be done to make this a complete PR? |
|
Sure, I will check your changes. I will probably want to run on a few other simulations on my side to check the performance hit/gain of your changes. |
|
"Other simulations" implies that I've tried running this on a simulation already 😅 |
|
Ah! Well, I meant in io vs unbinding limited simulations. We can also check if it indeed works. The test I was thinking about before is to check how expensive is to do the round-robin communication at each snapshot. Perhaps some big haloes will continuously change ranks because of random ordering and hence introduce unnecessary communication overhead. Just speaking out loud here, it could be that your implementation prevents this (still need to check the code) |
|
Ah ok, I don't have anything that will try to keep groups on the same ranks. It's easy to do (just start by putting each group on its previous rank as the default option), but since the default will be set by the rank ordering of halos at the time they appear this probably messes up the general concept of the round robin. There are probably tweaks that can help with reducing comms though, like you could do the assignment then swap ranks so that the biggest halo on each rank stays on the same rank as it was at the previous snapshot. First thing is to get a baseline comparison of the existing implementation vs this naive round robin, I guess. |
|
Great, thanks! Been continuing to think about tweaks - currently halos are assigned in the order (for 4 ranks) 1 2 3 4 1 2 3 4 1 2 3 4 ... A variant would be 1 2 3 4 4 3 2 1 1 2 3 4 ... That would tend to "balance out" rank 1 getting the most massive halo by giving it the least massive in the 2nd round, etc. I think it makes sense to get a baseline comparison to start with but this could be a variant to try out, maybe. |
|
I think the round-robin assignment will need to be written in a more careful manner, as it is currently done on a local MPI rank level. This means that when assigning MPI ranks based on particle count, there is no information about what assignment other MPI ranks have made. This was not a problem with the spatial assignment. |
|
Is every rank not holding all halos (e.g. |
|
No, I do not think so. What happens varies depending on the simulation format. For SWIFT, each MPI rank reads a given section of a snapshot group. It then uses the FoF membership of the particles it loaded to create FoF fragments because they are not guaranteed to have been read by a single MPI only. These fragments are subsequently grouped and joined in a given MPI rank by The end result is that Of course, this has an easy solution: make |
|
Relating to the comment above, and more to with the formatting of the code before this PR was created, I find the fact that there are several |
|
Ah ok - indeed gathering first is the solution. Feel free to push to this branch if that's useful at any point. |
|
Actually, I misread an if condition, so the second step in my last comment does not trigger for swiftsimio. Hence, the changes you made have had no effect on the domain decomposition. Will move them to the ExchangeAndMerge function call, which is the only relevant part for domain decomposition. |
|
The basic assignment of tasks to haloes is done. I used @kyleaoman's round robin assignment code within the relevant files for swift and apostle. Below is a bar plot showing the particle load across ranks for the COLIBRE test box (25m7).
The maximum particle difference between all ranks is just 16. Of course, using the number of particles as cost estimator is a simplification. A halo with 10 subgroups of 100 particles (1000 particles total in the halo) each is more expensive to analyse than 10 haloes of 100 particles each. Note that I still need to solve a few issues that cause crashes with MPI communication of the haloes themselves (I modified that part of the code to make it more readable, but now needs verification). |
|
I've had a look through the changes, and have a few thoughts before I implement anything. My understanding is that the current implementation discards all spatial information. The FoFs are sorted globally by particle count and assigned to ranks with no regard for position. This achieves good particle balance, but since the assignment is recomputed from scratch each snapshot, large FoFs can end up on completely different ranks between snapshots. An alternative that would require relatively minor changes to what's already here: start with the spatial COM-based assignment from master (assign each FoF to the rank corresponding to its spatial cell), then greedily rebalance. After the initial assignment, iteratively move the largest FoF from the most overloaded rank to the most underloaded rank until all ranks are within some tolerance of the mean. I think there is a better option, but it would require considerably more work. The FoF catalogues contain the size and COM of every group, and are small enough that all ranks can read or receive them before touching the particle data. This means you could compute a load-balanced spatial partition before doing any I/O. Each rank could then read only the snapshot cells covering its assigned region. This would eliminate the post-I/O redistribution exchange (almost) entirely, since FoFs would land on the correct rank directly from I/O. The partition would be roughly stable between snapshots because most FoFs don't cross partition boundaries from one output to the next. This is probably out of scope for the current PR though. |
|
I think that your "greedy rebalancing" probably helps quite a bit. A possible follow-on optimization would be to adjust the mapping between partition segments and MPI rank IDs until the minimum number of particles need to be moved. Will illustrate what I mean... |
* Update SnapshotOfLastIsolation description (#130) * Update SnapshotOfLastIsolation description * Typo * Property table typos * Update subhalo_properties.md * Minor docs clarification (#131) * Add link to HBT+ wiki (#134) * Document host halo identification. (#135) * Add page on how host haloes are assigned to subhaloes. * Remove section from unfinished list. * Add docs for subhalo sinking (#133) * Create doc file and rename some pages * Motivation of the page and definition of subhalo cores * Rephrase bits and bobs * New subsection in parameter file. * Typo in parameter name * Rephrase intro * Define overlap metric * Subhalo sinking theory page done * Typo fixes, rephrasing and specify M.B. TRACER particles. * Document the case for orphans * Subhaloes cannot sink twice. * Consistent naming for unbinding and sinking header titles * Add same licence as HBT+ (#136) * Add docs explaining central subhalo selection and subhalo hierarchy. (#137) * Add page for subhalo hierarchy. * Fix typo * another typo * Remove documented section from TODO --------- Co-authored-by: robjmcgibbon <36136863+robjmcgibbon@users.noreply.github.com> * Document merger trees and weird cases (#138) * Finish page on subhalo merger trees * Address Rob's useful comments * Update documentation: code examples (#139) * Fix typo from last PR * Restructuring files and folders * Update description of files * Correct download instructions * Text for population level statistics * Add code for pop. level properties * Add unit information to HBTReader * Add example code for HBTReader * Update merger tree page * Internal halo population examples * Update links * Doc naming suggestions by Rob * Update unit-retrieving function names * HBTReader update (#140) * Get file paths for ordered catalogues * Remove options class It was only used for raw catalogues, but the only benefit was to tell if a HBT-HERONS run was done. * Implement LoadSubhaloes for sorted catalogues * GetTrackSnapshot updated for sorted catalogues * Load number of subfiles for raw catalogues * Remove broken and unused function * Reorder class methods * Add functions to identify progenitors * Make raw LoadSubhalos output consistent with sorted version Before, loading a single property in the raw LoadSubhalos would not return an array with custom dtypes. * Make progenitor finding output the same for raw and sorted catalogues * Add warning for ParticleID loading * Merge sorted and unsorted catalogue code examples * Add missing example from previous PR * Fixes in raw catalogue indexing and loading single TrackId * Load several TrackIds at the same time (sorted catalogue implementation) * Load several subhalo entries at the same time (unsorted catalogue implementation) * Move parameter handling to more appropiate place * Fix a couple of bugs in unsorted catalogue loading * GetTrackSnapshot for multiple requested TrackIds * Get evolution of multiple tracks on a single method call (sorted catalogues) * Return each subhalo as an individual array. This implementation also works to replace GetTrackEvolution, but only for sorted catalogues for now. * Make unsorted catalogues handle missing TrackId * Handle list inputs * Remove old GetTrackEvolution * Update existing examples of getting evolution of progenitors * Return sorted list of direct progenitors * Add new code examples for merger trees * Fix example code * Update example overview * Update overview and add method description. * Fix deprecated warning * Remove duplicate functions and more consistent naming * Remove stray line * Docs: clearer mention of comoving positions and peculiar velocities (#141) * Specify comoving coordinates and peculiar velocities * Remove typo in potential energy definition * Small spacing fix * Gadget4 compatibility [DMO] (#77) * restructure particle snapshot load func to delay exchanging particles so that halos can be load before particle exchange * initial add of gadget4 io, not complete yet * almost done adding gadget4io * half done * isolate halo_patch_exchanger for ExchangeAndMerge * finish coding gadget4io ; waiting to be tested * change halosnap.Load() api * add test file * more consistent snap and group format names * restructure and bugfix - reduce total number of particles in snapshot_io rather than in particle_exchanger - set GroupLoadedFullParticle in individual io - bugfix in gadget4_io * minor bugfix * tested and working * minor polishment * add config * add more timing ticks * minor format change * v1.17.MPI-G4: update version number * bugfix in gadget4_hdf group format * identified known issue with hdf5 write * minor env change * remove unused matplotlib from reader add jiutian config * add config * minor change in a conf and script. initialize a copy of halo_virial halo_virial.cpp just copied from hydro branch; not working yet * improve compatibility with intel compiler * compatiblity improvement: replace deprecated omp_set_nested * init change * revise particle_exchanger to use a fixed buffer according to ParticleExchangerBufferSize * finished revising particle_exchanger HBTConfig.ParticleExchangerBufferSize=1e7 now determines the buffer size for particle query and subhalo host query * increment version number controllable memory size during particle exchange * bugfix in halo_io.cpp * trivial compiler change * remove un-used var * safety improvement also set default buffersize to 1e6 * bugfix in DecideCentrals fixed incorrect datatype of MassLimit (int->float) used for deciding major progenitors. Previous runs with particle mass below 5e8 solar mass could be affected, so that the centrals for very low mass subhalos could be incorrectly selected * increment version to 1.18.1.MPI-G4 to account for bugfix in decide centrals * add SplitSubSnap.py for splitting subsnap into tab and particles * minor performance bugfix (a barely used omp typo) * Prevent communication of particles before assigning FoF groups. * Handle different ways of reading the FoF particle information. * Handle GADGET-4 FoF or Subfind catalogues. * Formatting * Handle gadget-4 outputs that been split over several files * Automatically load and assign GADGET-4 units * Get boxsize * Load softening of DM particles automatically * Update H0 and G based on GADGET-4 units * Handle snapshots split over several files * Remove sanity check since it is no longer required. We do not need to specify box size for GADGET-4 snapshots * Remove commented out lines * Handle split or single snapshot files * Handle FoF groups split over several files * Remove assert inherited from HBT+ merge that was not in HBT-HERONS master. * Fix confusing macro expansion error on if statement * Do not track test output files * Clean-up and comment gadget4 reader I have removed the alternative gadget4_hdf2 to remove confusion * Make halo segments based on particle information * Remove unrequired functions Made redundant by my changes to how GADGET4 groups are create.d * Exchange particles in GADGET4. * Add NOTE on current implementation. * Unique constructor for HaloSnapshot_t * Fix bug concerning hostless subhaloes * Load velocity scaling from snapshot directly. * Annotate compilation blocks * Add example parameter file * Shorten gadget4 I/O option * Remove unused function --------- Co-authored-by: Jiaxin Han <jiaxin.han@sjtu.edu.cn> * Gadget4 io hydro (#142) * restructure particle snapshot load func to delay exchanging particles so that halos can be load before particle exchange * initial add of gadget4 io, not complete yet * almost done adding gadget4io * half done * isolate halo_patch_exchanger for ExchangeAndMerge * finish coding gadget4io ; waiting to be tested * change halosnap.Load() api * add test file * more consistent snap and group format names * restructure and bugfix - reduce total number of particles in snapshot_io rather than in particle_exchanger - set GroupLoadedFullParticle in individual io - bugfix in gadget4_io * minor bugfix * tested and working * minor polishment * add config * add more timing ticks * minor format change * v1.17.MPI-G4: update version number * bugfix in gadget4_hdf group format * identified known issue with hdf5 write * minor env change * remove unused matplotlib from reader add jiutian config * add config * minor change in a conf and script. initialize a copy of halo_virial halo_virial.cpp just copied from hydro branch; not working yet * improve compatibility with intel compiler * compatiblity improvement: replace deprecated omp_set_nested * init change * revise particle_exchanger to use a fixed buffer according to ParticleExchangerBufferSize * finished revising particle_exchanger HBTConfig.ParticleExchangerBufferSize=1e7 now determines the buffer size for particle query and subhalo host query * increment version number controllable memory size during particle exchange * bugfix in halo_io.cpp * trivial compiler change * remove un-used var * safety improvement also set default buffersize to 1e6 * bugfix in DecideCentrals fixed incorrect datatype of MassLimit (int->float) used for deciding major progenitors. Previous runs with particle mass below 5e8 solar mass could be affected, so that the centrals for very low mass subhalos could be incorrectly selected * increment version to 1.18.1.MPI-G4 to account for bugfix in decide centrals * add SplitSubSnap.py for splitting subsnap into tab and particles * minor performance bugfix (a barely used omp typo) * Prevent communication of particles before assigning FoF groups. * Handle different ways of reading the FoF particle information. * Handle GADGET-4 FoF or Subfind catalogues. * Formatting * Handle gadget-4 outputs that been split over several files * Automatically load and assign GADGET-4 units * Get boxsize * Load softening of DM particles automatically * Update H0 and G based on GADGET-4 units * Handle snapshots split over several files * Remove sanity check since it is no longer required. We do not need to specify box size for GADGET-4 snapshots * Remove commented out lines * Handle split or single snapshot files * Handle FoF groups split over several files * Remove assert inherited from HBT+ merge that was not in HBT-HERONS master. * Fix confusing macro expansion error on if statement * Do not track test output files * Clean-up and comment gadget4 reader I have removed the alternative gadget4_hdf2 to remove confusion * Make halo segments based on particle information * Remove unrequired functions Made redundant by my changes to how GADGET4 groups are create.d * Exchange particles in GADGET4. * Add NOTE on current implementation. * Unique constructor for HaloSnapshot_t * Fix bug concerning hostless subhaloes * Load velocity scaling from snapshot directly. * Annotate compilation blocks * Add example parameter file * Shorten gadget4 I/O option * Remove unused function * Formatting * Remove another unused function * Comment AssignTasks * Remove more unused fuctions * Clean up variable names * Start implementation of PartTypeLen loading Required for hydro simulations. Also, clean up the code * Communicate GroupLenType * Finalised the loading of particle offsets per type. * Assign memberships to distinct PartType * Nasty bug. Triggered by incorrect argument ordering. * Initialise Particle_t.HostId to be NullGroupId * Handle non-existing PartTypes. * Enable debug test only if hydro simulation. * Do not assign halo offsets to non-existing ranks. * Unify creation of INT_ARRAY mpi type. * Consistent use of HBTInt and int * Clean up * Move check of all sizes per halo to debug mode Doing this check involves an extra read and communication, so let's enable it only in debug. * Move sanity check further up Also make it use halo sizes per type, because total halo sizes is only available in debug mode --------- Co-authored-by: Jiaxin Han <jiaxin.han@sjtu.edu.cn> * NO MORE using namespace std; (#143) * Remove using namespace std and add std:: * Missing change for when hydro is enabled * Another missing std:: * Remove commented using * HBTReader: backwards compatibility between SnapshotOf and SnapshotIndexOf (#145) * Backwards compatibility * Name mangling bug caused multiple loads * Subhalo progenitor look up speedup (#146) * More incorrect hasattr calls * Use dicts for faster progenitor retrieval * Rename variable to be more explicit (#148) * Rename variable to be more explicit * Squash bug loading catalogues * Add note on generating docs * Fix OMP race condition (#149) * Update to cloudflare polyfill-io (#150) * Greedy balance * Add snap_min & snap_max to HBTreader * Ordering changes * Update comments * Minor changes * Remove extra sort --------- Co-authored-by: Victor J. Forouhar Moreno <60343271+VictorForouhar@users.noreply.github.com> Co-authored-by: Jiaxin Han <jiaxin.han@sjtu.edu.cn>
|
I've implemented the "greedy" rebalancing, here's what I found. TestingThe small colibre box seems to work without issues. I then ran it on snapshots 56-63 (around And here is from this branch The time spent unbinding is considerably reduced. The imbalance also gets worse as we go to lower redshifts, so we'll probably save even more time there. The bound mass function looks fine.
@kyleaoman could you please try the current version of this branch on your run? Overview of how exchange_and_merge works (so I don't forget)Each rank computes the particle count and centre of mass of each of its local fragments. These are sent to a temporary "accumulation rank" determined by hashing the HaloId, so all fragments of the same FoF arrive at the same rank and can be summed. Rank 0 then gathers these totals from all accumulation ranks and assigns each FoF a target rank using the spatial COM-based assignment from master, followed by greedy rebalancing (see below). Rank 0 scatters the assignments back to the accumulation ranks, again using the hash routing so each assignment lands on the rank holding that FoF's accumulated data. At this point, each accumulation rank knows the target for the FoFs that hashed to it, but the ranks that actually own the fragments (and will need to send the particles) don't know yet. Therefore each rank sends fragment metadata to the same hash-based accumulation rank, which stamps each with its target rank and sends them back. Now every rank knows where to send each of its fragments. Fragments are sorted by target rank and exchanged via MPI, first the halo metadata then the particles. After the exchange, each rank holds all fragments of its assigned FoFs and merges them by concatenating particle lists. Greedy rebalancingWe start with the spatial cell assignment (used on master already). We then find the ranks with the most and least particles. We select the largest FoF from the overloaded rank that satisfies Future changesAs mentioned above I think it would be better to use the FoF catalogues to determine the domain decomposition in advance of loading the particles. We could then make use of the fact the the swift snapshots are sorted to reduce the amount of exchanges required between ranks. However, unbinding dominates the runtime (at least for colibre), meaning the exchanges aren't a major problem, so I'm not planning on implementing this now. The output from the L50m5h still shows an imbalance. I guess this is because the time to unbind is not linear with the number of particles. I think we should try this balancing for a bit to see how it works for low redshift on the big runs. Also a perfect cpu-time balance probably will not correspond to a perfect memory balance, and I'm not sure which would be better to optimise for. |




This is an initial attempt (thus: draft) to distribute halos across MPI ranks in a more balanced way. The current approach is a spatial partitioning of the simulation box where each halo is assigned to the "cell" containing its centre. In terms of load balancing, this assumes that the distribution is not clustered, which is inevitably not true (for a worst case consider a zoom snapshot).
Ideally we want a good balance of CPU load that still fits in the memory available to each rank. This PR has smaller ambitions: the goal is just to achieve a reasonable memory balance.
My initial proposal is to take the total particle count, divide that up amongst the available ranks, then iterate the halo list assigning halos to rank 0 until it has reached its share of the particles, then start assigning to rank 1, and so on until all halos are assigned.
Because halos have various particle counts we can't expect a perfect partition from this scheme. My initial approach is to set a tolerance, so each rank is allowed to have a bit (for now hard-coded to 15%) more particles than its nominal share. There is also a rule that the first halo assigned to a rank is allowed regardless of size, this way a single halo that is "too big" for any one rank is not an automatic failure. These rules don't guarantee a successful distribution, so some refinement will be wanted here, but I'd rather have some confidence that a simple scheme is working before making it more complex.
I've got as far as making sure the code compiles and running the test suite. I get a test failure:
That also fails on branch
non_consecutive_outputsthat I've based off of though, so perhaps unrelated to my changes, but some reassurance would be welcome. Would also be good to know whether the test suite tests anything that would pick up issues in the halo assignment, or whether I should think of it as just a sanity check that I haven't completely broken things.