|
Peano
|
The load balancing API provides tools for offline decomposition of adaptive mesh trees in Peano.
This API enables analysis and optimisation of tree structures to achieve better load distribution across compute resources through strategic tree splitting operations.
The offline load balancing approach transforms the traditional online load balancing problem into an offline optimisation problem. Instead of making splitting decisions during mesh construction without knowledge of the final structure, we:
This approach allows us to make informed splitting decisions that minimise both computational cost and load imbalance, as described in Load balancing strategies.
The API is organised into four phases:
toolbox::loadbalancing::strategies::Hardcoded.The GridPatchFileReader module in src/toolbox/loadbalancing/api/Input/GridPatchFileReader.py enables users to analyse existing simulation data by converting Peano grid checkpoint files into the Tree structure format. This module allows you to analyse real simulation data, handle complex adaptive patterns, support multi-domain decompositions, enable offline optimisation. For complete implementation details, data representation examples, and coordinate systems, see the Tree class documentation in src/toolbox/loadbalancing/api/Tree.py.
This input method is ideal for optimising load balancing strategies based on real application behaviour and mesh patterns.
The RegularGridGenerator module (src/toolbox/loadbalancing/api/Input/RegularGridGenerator.py) creates controlled test environments for algorithm development and benchmarking. This module enables you to develop splitting algorithms, benchmark performance, validate implementations, compare strategies.
This input method is essential for algorithm development and provides a baseline for comparing optimisation strategies.
The SplitTrees module (src/toolbox/loadbalancing/api/strategies/SplitTrees.py) implements the core optimisation algorithms for redistributing computational work across multiple compute resources. These strategies enable you to achieve balanced workload distribution, handle adaptive mesh challenges, and maintain mesh topology.
The splitting phase is now a configurable staged workflow executed inside split_tree(). Stages can be enabled/disabled via flags, and Stage I traversal can be selected explicitly.
split_tree() modifies the tree in place. Stage IV is a separate call: create_constructor_initialisation_string() (see Phase 3) turns the split tree into the constructor arguments of the hardcoded load balancing.
| Stage | Function | Objective | Complexity |
|---|---|---|---|
| I | distribute_leaves_bottom_up() or distribute_leaves_top_down() | Distribute leaves across subtrees | O(Ncells) |
| II | _propagate_bottom_up() | Propagate subtree ids bottom-up | O(Ncells) single pass |
| III | _optimize_hierarchy() | Minimise split operation count through hierarchy lifting | Iterative multi-pass; data-dependent |
| IV | create_constructor_initialisation_string() (internally uses extract_split_instructions()) | Build hardcoded constructor config and infer split mode | O(subtrees × Ncells) |
Stage I always executes when splitting is requested. Stage II, Stage III, and Stage IV are optional.
Here, Ncells denotes the total number of represented cells across all stored levels. For Stage I, BFS and DFS traversal have the same asymptotic complexity O(Ncells); they differ only in visitation order and constant-factor overhead.
Stage I runs through the leaves along Peano's space-filling curve, as implemented in src/toolbox/loadbalancing/api/strategies/SFC.py. This curve is exactly the order in which Peano's spacetree traversal runs through the mesh, i.e. Peano later hands out cells in the same order in which the offline decomposition plans them. Without this match, the subdomains that Peano builds would differ from the planned ones.
Peano inverts its traversal in every other mesh sweep, i.e. it runs through the same curve forth and back. The offline decomposition ignores this inversion and always plans along the non-inverted curve. If all split-off trees request the same number of cells, the inversion does not change the subdomains anyway. Otherwise, a subdomain might end up on the other end of the curve than planned, but it still holds the planned number of cells.
Peano never hands over cells along a periodic boundary to another tree, i.e. these cells always remain on tree 0. Pass the periodic boundary conditions of the experiment through split_tree(..., periodic_BC=[...]). Stage I then keeps all leaves along periodic boundaries on subtree 0 and counts them towards subtree 0's share. If the frame of periodic boundary cells is larger than subtree 0's share, subtree 0 holds more cells than planned and the last subtree fewer.
The core loop of split_tree() identifies all leaf nodes across every refinement level and distributes them as evenly as possible across the requested number of subtrees. A node is a leaf if it resides on the finest level or has no children on the next level. After Stage I, only leaf nodes carry updated subtree assignments; all non-leaf nodes still belong to their original subtree and are updated by Stage II.
Stage I now supports three traversal modes:
traversal="bfs" uses distribute_leaves_bottom_up(..., finest_level_first=False) and visits leaves in level-order from coarsest to finest level.traversal="bfs_finest_first" uses distribute_leaves_bottom_up(..., finest_level_first=True) and visits leaves in level-order within each level while processing levels from finest to coarsest.traversal="dfs" uses distribute_leaves_top_down() and visits leaves in depth-first order.Both variants preserve the same balancing policy; they differ only in the order in which eligible leaves are discovered and therefore assigned to target subtrees.
The _propagate_bottom_up() function propagates subtree assignments upward through the tree hierarchy using simple parent-child inheritance.
This bottom-up propagation (Stage II) ensures that parent nodes whose children all belong to the same subtree are themselves assigned to that subtree (strict parent-child consistency), whilst preserving splitting of leaves obtained in Stage I.
The following scenarios aim to illustrate expected and non-expected cases of multi-level splitting of leaves with bottom-up propagation for adaptive mesh refinement. Green nodes indicate leaves assigned to Subtree 1, whilst yellow nodes denote leaves assigned to Subtree 0. In Scenarios 1-2, the two-subtree split is filled from left to right starting with Subtree 0 (yellow), and the remaining right-hand leaves belong to Subtree 1 (green). In figures below, image (I) on the left shows the output of the initial leaf distribution (Stage I), whereas image (II) on the right shows the result of a bottom-up propagation.
After Stage II, Subtree 1 (green) occupies the right-hand part of the tree and propagation reaches Level 1:
L2C2 and L2C3 (children of L1C) belong to Subtree 1 from Stage I.L2C1 (child of L1C) is a non-leaf that is promoted to Subtree 1 by Stage II (all of its Level-3 children belong to Subtree 1).L1C are now in Subtree 1, L1C is promoted to Subtree 1 as well.All other nodes remain in Subtree 0 (gold).
In this scenario, continuing propagation by one more level is the expected outcome: introducing Subtree 1 at Level 1 via L1C preserves the same final leaf mapping while reducing split bookkeeping.
If propagation is stopped one level too early in Stage II, L1C is not promoted even though all its children are already in Subtree 1. The result is a Level-2 split that introduces L2C1, L2C2, and L2C3 instead of a single Level-1 introduction via L1C. The leaf-to-subtree mapping is unchanged, but bookkeeping is worse (3 introduced cells instead of 1), so this illustrates an incorrect Stage II application.
After Stage II, Subtree 1 (green) remains represented on Level 2:
L2B2, L2C1, L2C2, L2C3).L2B3 is a non-leaf that is promoted to Subtree 1 by Stage II (all of its Level-3 children belong to Subtree 1).This yields one compact Level-2 introduction for Subtree 1 while keeping Level-1 parents in Subtree 0. In this scenario, this stopping point is expected and desirable: it realises the same final leaf mapping without creating an additional split on Level 1.
If propagation continues one level further than desired, L1C is promoted to Subtree 1 because all of its children are already green. However, L1B remains mixed, so the cells in Subtree 1 under L1B still require a Level-2 introduction. This creates two split levels (Level 1 and Level 2) instead of a single Level-2 split, i.e. more split operations for the same final leaf assignment.
For the fine-first BFS ordering, Stage I already yields contiguous Level-3 blocks for Subtrees 1 and 2, while Subtree 3 is composed of two childless Level-2 leaves and one refined Level-2 block. After Stage II, this structure is compacted on Level 2 by promoting uniform refined parents:
L2B1 and L2B2 (6 Level-3 leaves total)L2C1 and L2C2 (6 Level-3 leaves total)L2A3 and L2B3, plus refined L2C3 (3 Level-3 leaves) → 5 leaves totalL2A1 and L2A2 → 6 leaves totalThe bottom-up propagation ensures strict parent-child consistency: only internal nodes whose full descendant set belongs to one subtree are promoted. Consequently, L2B1, L2B2, L2C1, L2C2, and L2C3 are promoted on Level 2, while L1A, L1B, and L1C are still resolved in Stage III.
The _optimize_hierarchy() function minimises the total number of split operations whilst preserving load balance established by Stage II. It does so by promoting a parent to the dominant child subtree when promotion is beneficial.
3^d children in dimension d),The process repeats until a full pass makes no changes.
_propagate_bottom_up() to run first)weight)The following diagrams show the same tree structure before and after applying hierarchical optimisation. This algorithm transforms the split structure to reduce the number of split operations whilst maintaining identical load distribution.
In both scenarios, the Stage II result does not contain a set of introduced subtrees on some level \(L\) that forms a complete coarse block under a single common parent \(p\) on level \(L-1\). Any such promotion would either (i) recolour additional cells (changing the leaf-to-subtree mapping established by Stage II) or (ii) require additional corrective splits, i.e. it would not reduce split introductions.
More precisely, Stage III (“hierarchy lifting”) is only applicable if we can replace \(M\) split introductions of children on level \(L\) by a single split introduction of their parent on level \(L-1\) while preserving the leaf mapping. This is the “one parent’s worth”/complete-block condition (sum of children equals \(3^{d\cdot L}\) for one parent).
L1C). There is no lower-level set of introductions left that can be lifted further while reducing split count.L1C alone does not eliminate the Level-2 introduction needed for Subtree-1 cells under L1B; it would add another split level instead of reducing introductions.Consequently, Scenarios 1 and 2 do not admit a correctness-preserving hierarchy lift that reduces split introductions.
In the fine-first BFS Scenario 3, Stage III lifts two compact Level-2 regions from Stage II to Level 1:
L1B is promoted to Subtree 1 (replacing the pair L2B1, L2B2)L1C is promoted to Subtree 2 (replacing the pair L2C1, L2C2)L2A3, L2B3, and L2C3This preserves the Stage-II leaf mapping while reducing hierarchy depth for Subtrees 1 and 2.
_optimize_hierarchy() in split_tree() as outlined aboveStage IV translates the split tree into the constructor arguments of toolbox::loadbalancing::strategies::Hardcoded:
The hardcoded strategy works with an enumerated list of splits. The _k_th split reads
Tree
splitting_trees[k]handsnumber_of_cells[k]cells over to a new tree on rankdestination_ranks[k], and it usesmodes[k]to select these cells.
There is no notion of mesh sweeps. The C++ strategy hands out the splits in the order given and issues the _k_th split as soon as its splitting tree exists locally and holds more than number_of_cells[k] cells. See the class documentation of toolbox::loadbalancing::strategies::Hardcoded for all rules.
The lower-level helper extract_split_instructions() remains available for analysis/debugging and returns the raw lists (splitting_trees, number_of_cells, subtrees, bottom_up_flags):
splitting_trees: unique parent subtree id for all introduced cells of the split, or 0 if ambiguous.number_of_cells: number of cells introduced by the split, i.e. cells with the target subtree id whose parent exists (level>0) and has a different subtree id. For level 0, it is the number of cells on level 0 that already belong to the target subtree.subtrees: subtree ids introduced by the splits (not ranks).bottom_up_flags: whether the split is a bottom-up split, i.e. whether the subtree is introduced on several levels.The splits are ordered from coarse to fine levels and, within a level, by subtree id.
Each split is either a top-down split (peano4::SplitInstruction::Mode::AggressiveTopDown) or a bottom-up split (peano4::SplitInstruction::Mode::BottomUp). Top-down splits hand over whole refined cells including their children as long as cells are still owed, i.e. the new tree might get a few cells more than requested. Bottom-up splits hand over fine grid cells one by one along the space-filling curve and hence reproduce the offline plan cell by cell. By default, create_constructor_initialisation_string() infers the modes from bottom_up_flags. The argument modes overwrites them with a single marker Hardcoded.TopDown or Hardcoded.BottomUp, or with one marker per split.
The constructor string is the same on all ranks. Each rank only keeps the splits whose splitting tree it hosts and drops all others, so splits of trees on other ranks never block a rank.
To tell which rank hosts a tree, the strategy relies on Peano's tree numbering, which is round robin: peano4::parallel::Node::getId() gives the n_th tree on rank _r the number n_·_R+_r_ for R ranks, and peano4::parallel::Node::getRank() maps tree number t onto rank t mod R. The offline decomposition refers to trees through their subtree numbers. We therefore always assume that subtree s lives on rank s mod R, i.e. that the subtrees are assigned to the ranks round robin, as assign_trees_to_ranks_round_Robin() does. Only then does a split of subtree s in the offline plan split tree s in Peano. create_constructor_initialisation_string() checks this and raises an error for any other assignment. It takes the number of ranks from the largest destination rank, unless you pass number_of_ranks.
Equivalent constructor usage in C++:
The generate_tree_yaml function in src/toolbox/loadbalancing/api/strategies/SplitTrees.py creates comprehensive YAML files for Peano integration, containing:
These YAML files serve as the output format for integrating optimised decompositions back into Peano simulations.
For detailed YAML format specifications and generation parameters, see the generate_tree_yaml function documentation in src/toolbox/loadbalancing/api/strategies/SplitTrees.py.
The API integrates with visualisation tools to generate graphical representations of tree structures in multiple formats:
The visualisation system automatically:
For detailed implementation, customisation options, and technical specifications, see the TreeVisualizer module documentation in src/toolbox/loadbalancing/api/visualize/TreeVisualizer.py.
The OfflineDecomposition.py script serves as the main interface for offline decomposition operations, providing command-line access to all API functionality.
The script integrates all API components into a complete workflow:
GridPatchFileReader or RegularGridGenerator, represented through the Tree data structure.SplitTrees, including Stage I (leaf distribution), Stage II (bottom-up propagation), and optional Stage III (hierarchical optimisation).create_constructor_initialisation_string(), to obtain (hardcoded_config, hardcoded_mode) for toolbox::loadbalancing::strategies::Hardcoded.generate_tree_yaml) plus optional visual artefacts for inspection and integration.For complete command-line options, parameter details, and usage examples, see the OfflineDecomposition.py script documentation.
The script generates several output files for analysis and integration:
The API tracks and reports performance metrics for:
This enables analysis of scaling behaviour and identification of computational bottlenecks in the offline optimisation process.