| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /*********************************************************************************/ | ||
| 2 | /* Copyright 2009-2026 Barcelona Supercomputing Center */ | ||
| 3 | /* */ | ||
| 4 | /* This file is part of the DLB library. */ | ||
| 5 | /* */ | ||
| 6 | /* DLB is free software: you can redistribute it and/or modify */ | ||
| 7 | /* it under the terms of the GNU Lesser General Public License as published by */ | ||
| 8 | /* the Free Software Foundation, either version 3 of the License, or */ | ||
| 9 | /* (at your option) any later version. */ | ||
| 10 | /* */ | ||
| 11 | /* DLB is distributed in the hope that it will be useful, */ | ||
| 12 | /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ | ||
| 13 | /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ | ||
| 14 | /* GNU Lesser General Public License for more details. */ | ||
| 15 | /* */ | ||
| 16 | /* You should have received a copy of the GNU Lesser General Public License */ | ||
| 17 | /* along with DLB. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 18 | /*********************************************************************************/ | ||
| 19 | |||
| 20 | #include "mngo/mngo_drom.h" | ||
| 21 | |||
| 22 | #include "LB_comm/shmem_procinfo.h" | ||
| 23 | #include "dlb_errors.h" | ||
| 24 | #include "mngo/mngo.h" | ||
| 25 | #include "mngo/mngo_balancer.h" | ||
| 26 | |||
| 27 | #include "LB_comm/shmem_mngo.h" | ||
| 28 | #include "support/mask_utils.h" | ||
| 29 | #include <math.h> | ||
| 30 | |||
| 31 | /* | ||
| 32 | * To decide how many cores DROM has to change we will solve for the number of | ||
| 33 | * processors that have to be removed from one process with *self* parallel | ||
| 34 | * efficiency to achieve *others* parallel efficiency, being this lower or | ||
| 35 | * greater than *self*. | ||
| 36 | */ | ||
| 37 | 10 | static int balance(const subprocess_descriptor_t *spd, float self, | |
| 38 | float others) { | ||
| 39 | |||
| 40 | 10 | mngo_info_t *mngo_info = spd->mngo_info; | |
| 41 | cpu_set_t mask; | ||
| 42 | 10 | shmem_procinfo__getprocessmask(shmem_mngo_get_pid(mngo_info->mid), &mask, | |
| 43 | DLB_DROM_FLAGS_NONE); | ||
| 44 | |||
| 45 | /* | ||
| 46 | * TODO: Implement also the cross-correlation of shared memory parallel | ||
| 47 | * efficiencies among different processes, to determine if reducing the | ||
| 48 | * number of active cores in the node (or activating EAR, in the future) | ||
| 49 | * will be useful or not. | ||
| 50 | * | ||
| 51 | * For now will activate DROM and reduce the amount of cores of the affected | ||
| 52 | * node with the hope that the load imbalance is persistent among nodes and | ||
| 53 | * DROM will be useful. And roll the decision back if performance in any | ||
| 54 | * node gets worse on the next observation. | ||
| 55 | */ | ||
| 56 | |||
| 57 | /* | ||
| 58 | * +--------------------------+---------------+ | ||
| 59 | * (ex. 1) SELF PE | GOOD | BAD | | ||
| 60 | * +--------------------------+---------------+ | ||
| 61 | * +------------------------------------+-----+ | ||
| 62 | * (ex. 2) SELF PE | GOOD | BAD | | ||
| 63 | * +------------------------------------+-----+ | ||
| 64 | * +--------------------------------+---------+ | ||
| 65 | * AVG. NODE PE | GOOD | BAD | | ||
| 66 | * +--------------------------------+---------+ | ||
| 67 | * | ||
| 68 | * With the ratio operation we convert it to that: | ||
| 69 | * | ||
| 70 | * +--------------------------+-----+ | ||
| 71 | * (ex. 1) SELF PE | GOOD | BAD | | ||
| 72 | * +--------------------------+-----+ | ||
| 73 | * +--------------------------------+---+ | ||
| 74 | * (ex. 2) SELF PE | GOOD | E | | ||
| 75 | * +--------------------------------+---+ | ||
| 76 | */ | ||
| 77 | 10 | float ratio_mpi_parallel_efficiency = ((float)self) / others; | |
| 78 | |||
| 79 | /* | ||
| 80 | * PE = (GOOD + EXTRA) / (GOOD + BAD) | ||
| 81 | * | ||
| 82 | * To reduce the load balance we need make the elapsed time of the GOOD part | ||
| 83 | * take the same amount of time for all processes. | ||
| 84 | * | ||
| 85 | * Assuming that the aggregated CPU time is constant no matter how many | ||
| 86 | * cores a process has (i.e. ideal thread scalability). That is, the area | ||
| 87 | * described by the GOOD elapsed time and the number of cores (N) is | ||
| 88 | * constant for all values of N. | ||
| 89 | * | ||
| 90 | * +----+ | ||
| 91 | * | | | ||
| 92 | * +----------+ | | | ||
| 93 | * N = 2 | | has the same area as N = 4 | | | ||
| 94 | * +----------+ | | | ||
| 95 | * GOOD = 12 +----+ | ||
| 96 | * GOOD = 6 | ||
| 97 | * | ||
| 98 | * This is obviously an hover-optimistic assumption and it will hold true | ||
| 99 | * depending on how good the application scales in OpenMP. But will | ||
| 100 | * generally hold true for scaling down the number of processes. | ||
| 101 | * | ||
| 102 | * TODO: When we have OpenMP metrics we can scale the number of processes to | ||
| 103 | * change taking into account the OpenMP parallel efficiency. | ||
| 104 | * | ||
| 105 | * This area is described by the product of the number of processes and the | ||
| 106 | * GOOD part. And we want to change the GOOD elapsed time to force the value | ||
| 107 | * to take the same as the average GOOD elapsed. From this objective we | ||
| 108 | * obtain the following equation: | ||
| 109 | * | ||
| 110 | * N * ( GOOD + DIFFERENCE ) = (N + dN) * GOOD | ||
| 111 | * | ||
| 112 | * Where dN is the change in number of cores. When dN is isolated we | ||
| 113 | * obtain: | ||
| 114 | * | ||
| 115 | * N * GOOD + dN * GOOD = N * ( GOOD + DIFFERENCE ) | ||
| 116 | * | ||
| 117 | * dN * GOOD = N * ( GOOD + DIFFERENCE ) - N * GOOD | ||
| 118 | * | ||
| 119 | * ( GOOD + DIFFERENCE ) | ||
| 120 | * dN = N * --------------------- - N = N * PE - N | ||
| 121 | * GOOD | ||
| 122 | * | ||
| 123 | * Due to that these operations are continuous but the dN has to be a | ||
| 124 | * discrete value. We ceil the multiplication to not release a little bit | ||
| 125 | * more cores than needed. | ||
| 126 | */ | ||
| 127 | 10 | int core_count = mu_count_cores_intersecting_with_cpuset(&mask); | |
| 128 | 10 | int core_change = | |
| 129 | 10 | (int)ceil((float)core_count * ratio_mpi_parallel_efficiency) - | |
| 130 | core_count; | ||
| 131 | |||
| 132 | /* | ||
| 133 | * Keep at least one core. | ||
| 134 | */ | ||
| 135 | 10 | int maximum_core_release = -core_count + 1; | |
| 136 | 10 | int bounded_core_change = max_int(core_change, maximum_core_release); | |
| 137 | |||
| 138 | 10 | return bounded_core_change; | |
| 139 | } | ||
| 140 | |||
| 141 | 10 | static void print_redistribution_report(const subprocess_descriptor_t *spd, | |
| 142 | int self_cpu_delta) { | ||
| 143 | 10 | mngo_info_t *mngo_info = spd->mngo_info; | |
| 144 | |||
| 145 | 10 | const char *region_name = ""; | |
| 146 | 10 | mngo_regions_manager_t *region_manager = &mngo_info->region_handler; | |
| 147 | dlb_mngo_region_t **current_region; | ||
| 148 |
2/2✓ Branch 1 taken 6 times.
✓ Branch 2 taken 4 times.
|
10 | if (queue__peek_head(region_manager->active_regions, |
| 149 | (void **)¤t_region) == DLB_SUCCESS) { | ||
| 150 | 6 | region_name = (*current_region)->name; | |
| 151 | } | ||
| 152 | 10 | shmem_mngo_drom__print_redistribution(mngo_info->mid, self_cpu_delta, | |
| 153 | region_name); | ||
| 154 | 10 | } | |
| 155 | |||
| 156 | /* | ||
| 157 | * Once each process has used its history of performance to decide what actions | ||
| 158 | * to take, we put all the actions from all processes together to decide which | ||
| 159 | * actions have preference over which other actions. | ||
| 160 | * | ||
| 161 | * To do that we will use MPI to execute an all to all communication and then | ||
| 162 | * apply the same logic to all the collected data to leave all the mngo | ||
| 163 | * decisions in a consistant state. | ||
| 164 | */ | ||
| 165 | 10 | void mngo_drom__balance(const subprocess_descriptor_t *spd, float self_pe, | |
| 166 | float node_pe, cpu_set_t *new_cpu_mask) { | ||
| 167 | |||
| 168 | 10 | mngo_info_t *mngo_info = spd->mngo_info; | |
| 169 | |||
| 170 | // 1. Decide how many CPU i would like to give/take | ||
| 171 | 10 | int self_cpu_delta = balance(spd, self_pe, node_pe); | |
| 172 | |||
| 173 | // 2. Communicate these to other processes | ||
| 174 | int num_cpu_deltas = | ||
| 175 | 10 | shmem_mngo_drom__alltoall_deltas_start(mngo_info->mid, self_cpu_delta); | |
| 176 | |||
| 177 | 10 | int *cpu_deltas = calloc(num_cpu_deltas, sizeof(int)); | |
| 178 | 10 | shmem_mngo_drom__alltoall_deltas_finish(mngo_info->mid, cpu_deltas); | |
| 179 | |||
| 180 | // 3. Update how many CPU to give/take based on availability | ||
| 181 | int coherent_self_cpu_delta = | ||
| 182 | 10 | mngo_balancer(mngo_info->mid, cpu_deltas, num_cpu_deltas); | |
| 183 | 10 | free(cpu_deltas); | |
| 184 | |||
| 185 | // 4. Exchange CPUs | ||
| 186 | 10 | shmem_mngo_drom__redistribute(mngo_info->mid, coherent_self_cpu_delta, | |
| 187 | new_cpu_mask); | ||
| 188 | |||
| 189 | // 5 Show CPU changes | ||
| 190 | 10 | print_redistribution_report(spd, coherent_self_cpu_delta); | |
| 191 | 10 | } | |
| 192 |