/home/runner/work/charm/charm/src/ck-ldb/GreedyRefineCentralGPULB.C
Line | Count | Source (jump to first uncovered line) |
1 | | /** |
2 | | * \addtogroup CkLdb |
3 | | */ |
4 | | /*@{*/ |
5 | | |
6 | | /** |
7 | | * Two-level GPU-aware greedy-refine load balancer. |
8 | | * |
9 | | * Cross-group (per-GPU/per-process) balancing uses GPU load (gpuTime); |
10 | | * within-group (per-PE) balancing uses CPU load (wallTime). See the header |
11 | | * for the full description. Derived from GreedyRefineCentralLB |
12 | | * (jjgalvez@illinois.edu). |
13 | | */ |
14 | | |
15 | | #include "charm++.h" |
16 | | #include "ckgraph.h" |
17 | | #include "GreedyRefineCentralGPULB.h" |
18 | | |
19 | | #include <float.h> |
20 | | #include <limits.h> |
21 | | #include <algorithm> |
22 | | #include <math.h> |
23 | | #if CMK_CUDA |
24 | | #include <unordered_map> |
25 | | #endif |
26 | | |
27 | | extern int quietModeRequested; |
28 | | |
29 | | // a solution is feasible if num migrations <= user-specified limit |
30 | | // LOAD_MIG_BAL is used to control tradeoff between maxload and migrations |
31 | | // when selecting solutions from the feasible set |
32 | 0 | #define LOAD_MIG_BAL 1.003 |
33 | | |
34 | | using namespace std; |
35 | | |
36 | | class GreedyRefineCentralGPULB::Solution { |
37 | | public: |
38 | 0 | Solution() {} |
39 | 0 | Solution(int pe, double maxLoad, int nmoves) : pe(pe), max_load(maxLoad), migrations(nmoves) {} |
40 | | int pe; // pe who produced this solution |
41 | | float max_load; |
42 | | int migrations; |
43 | | |
44 | 0 | void pup(PUP::er &p) { |
45 | 0 | p|pe; |
46 | 0 | p|max_load; |
47 | 0 | p|migrations; |
48 | 0 | } |
49 | | }; |
50 | | |
51 | | // custom heap to allow removal of processors from any position |
52 | | class GreedyRefineCentralGPULB::PHeap { |
53 | | public: |
54 | 0 | PHeap(int numpes) { |
55 | 0 | Q.reserve(numpes+1); |
56 | 0 | Q.push_back(NULL); // first element of the array is NULL |
57 | 0 | } |
58 | | |
59 | 0 | void addProcessors(std::vector<GreedyRefineCentralGPULB::GProc> &procs, bool bgLoadZero, bool insert=true) { |
60 | 0 | for (int i=0; i < procs.size(); i++) { |
61 | 0 | GreedyRefineCentralGPULB::GProc &p = procs[i]; |
62 | 0 | if (p.available) { |
63 | 0 | p.load = p.bgload; |
64 | 0 | if (insert) { |
65 | 0 | Q.push_back(&p); |
66 | 0 | p.pos = Q.size()-1; |
67 | 0 | } |
68 | 0 | } |
69 | 0 | } |
70 | 0 | if (!bgLoadZero) buildMinHeap(); |
71 | 0 | } |
72 | | |
73 | 0 | inline GreedyRefineCentralGPULB::GProc *top() const { |
74 | 0 | CkAssert(Q.size() > 1); |
75 | 0 | return Q[1]; |
76 | 0 | } |
77 | | |
78 | 0 | inline void push(GreedyRefineCentralGPULB::GProc *p) { |
79 | 0 | Q.push_back(p); |
80 | 0 | p->pos = Q.size()-1; |
81 | 0 | siftUp(p->pos); |
82 | 0 | } |
83 | | |
84 | 0 | inline GreedyRefineCentralGPULB::GProc *pop() { |
85 | 0 | if (Q.size() == 1) return NULL; |
86 | 0 | GreedyRefineCentralGPULB::GProc *retval; |
87 | 0 | if (Q.size() == 2) { |
88 | 0 | retval = Q[1]; |
89 | 0 | Q.pop_back(); |
90 | 0 | return retval; |
91 | 0 | } |
92 | 0 | retval = Q[1]; |
93 | 0 | Q[1] = Q.back(); |
94 | 0 | Q.pop_back(); |
95 | 0 | Q[1]->pos = 1; |
96 | 0 | siftDown(1); |
97 | 0 | return retval; |
98 | 0 | } |
99 | | |
100 | | // remove processor from any position in the heap |
101 | 0 | void remove(GreedyRefineCentralGPULB::GProc *p) { |
102 | 0 | int pos = p->pos; |
103 | 0 | if ((Q.size() == 2) || (pos == Q.size()-1)) return Q.pop_back(); |
104 | 0 | if (pos == 1) { pop(); return; } |
105 | 0 | Q[pos] = Q.back(); |
106 | 0 | Q.pop_back(); |
107 | 0 | Q[pos]->pos = pos; |
108 | 0 | if (Q[pos/2]->load > Q[pos]->load) siftUp(pos); |
109 | 0 | else siftDown(pos); |
110 | 0 | } |
111 | | |
112 | 0 | inline void clear() { |
113 | 0 | Q.clear(); |
114 | 0 | Q.push_back(NULL); |
115 | 0 | } |
116 | | |
117 | | private: |
118 | | |
119 | 0 | void min_heapify(int i) { |
120 | 0 | const int left = 2*i; |
121 | 0 | const int right = 2*i + 1; |
122 | 0 | int smallest = i; |
123 | 0 | if ((left < Q.size()) && (Q[left]->load < Q[smallest]->load)) smallest = left; |
124 | 0 | if ((right < Q.size()) && (Q[right]->load < Q[smallest]->load)) smallest = right; |
125 | 0 | if (smallest != i) { |
126 | 0 | swap(i,smallest); |
127 | 0 | Q[i]->pos = i; |
128 | 0 | Q[smallest]->pos = smallest; |
129 | 0 | min_heapify(smallest); |
130 | 0 | } |
131 | 0 | } |
132 | | |
133 | 0 | void inline buildMinHeap() { |
134 | 0 | for (int i=Q.size()/2; i > 0; i--) min_heapify(i); |
135 | 0 | } |
136 | | |
137 | 0 | inline void swap(int pos1, int pos2) { |
138 | 0 | GreedyRefineCentralGPULB::GProc *t = Q[pos1]; |
139 | 0 | Q[pos1] = Q[pos2]; |
140 | 0 | Q[pos2] = t; |
141 | 0 | } |
142 | | |
143 | 0 | void siftUp(int pos) { |
144 | 0 | if (pos == 1) return; // reached root |
145 | 0 | int ppos = pos/2; |
146 | 0 | if (Q[ppos]->load > Q[pos]->load) { |
147 | 0 | swap(ppos,pos); |
148 | 0 | Q[ppos]->pos = ppos; |
149 | 0 | Q[pos]->pos = pos; |
150 | 0 | siftUp(ppos); |
151 | 0 | } |
152 | 0 | } |
153 | | |
154 | 0 | inline int minChild(int pos) const { |
155 | 0 | int c1 = pos*2; |
156 | 0 | int c2 = pos*2 + 1; |
157 | 0 | if (c1 >= Q.size()) return -1; |
158 | 0 | if (c2 >= Q.size()) return c1; |
159 | 0 | if (Q[c1]->load < Q[c2]->load) return c1; |
160 | 0 | else return c2; |
161 | 0 | } |
162 | | |
163 | 0 | void siftDown(int pos) { |
164 | 0 | int cpos = minChild(pos); |
165 | 0 | if (cpos == -1) return; |
166 | 0 | if (Q[pos]->load > Q[cpos]->load) { |
167 | 0 | swap(pos,cpos); |
168 | 0 | Q[cpos]->pos = cpos; |
169 | 0 | Q[pos]->pos = pos; |
170 | 0 | siftDown(cpos); |
171 | 0 | } |
172 | 0 | } |
173 | | |
174 | | std::vector<GreedyRefineCentralGPULB::GProc*> Q; |
175 | | }; |
176 | | |
177 | | CreateLBFunc_Def(GreedyRefineCentralGPULB, "Two-level GPU-aware greedy refinement-based algorithm") |
178 | | |
179 | 0 | GreedyRefineCentralGPULB::GreedyRefineCentralGPULB(const CkLBOptions &opt): CBase_GreedyRefineCentralGPULB(opt), migrationTolerance(1.0) |
180 | 0 | { |
181 | 0 | lbname = "GreedyRefineCentralGPULB"; |
182 | 0 | if ((CkMyPe() == 0) && !quietModeRequested) |
183 | 0 | CkPrintf("CharmLB> GreedyRefineCentralGPULB created.\n"); |
184 | | #if CMK_HIP |
185 | | // This balancer's cross-GPU stage balances on LDObjData::gpuTime, which is |
186 | | // filled in from CUPTI activity records -- a CUDA-only facility with no HIP |
187 | | // equivalent wired up yet. On HIP every object's gpuTime stays zero, so that |
188 | | // stage sums zeros and only the within-group CPU pass does any work. Warn |
189 | | // rather than balance silently on a dimension that is not being measured. |
190 | | // Printed even under quiet mode: it reports degraded behaviour, not a banner. |
191 | | if (CkMyPe() == 0) |
192 | | CkPrintf("CharmLB> Warning: GPU load measurement on HIP is not implemented " |
193 | | "yet. GreedyRefineCentralGPULB will see zero GPU load for every " |
194 | | "object and balance on CPU load alone.\n"); |
195 | | #endif |
196 | 0 | if (_lb_args.percentMovesAllowed() < 100) { |
197 | 0 | migrationTolerance = float(_lb_args.percentMovesAllowed())/100.0; |
198 | 0 | } |
199 | | // A tolerance (the default, 1.1) says what to aim for, so one candidate is |
200 | | // built. Asking for 0 or less asks for the search instead: every PE builds a |
201 | | // candidate from a different (A,B) pair and receiveSolutions picks between |
202 | | // them, at the cost of a reduction per balancing step. |
203 | 0 | concurrent = (_lb_args.greedyRefineTolerance() <= 0.0); |
204 | 0 | } Unexecuted instantiation: _ZN24GreedyRefineCentralGPULBC2ERK11CkLBOptions Unexecuted instantiation: _ZN24GreedyRefineCentralGPULBC1ERK11CkLBOptions |
205 | | |
206 | 0 | GreedyRefineCentralGPULB::GreedyRefineCentralGPULB(CkMigrateMessage *m): CBase_GreedyRefineCentralGPULB(m), migrationTolerance(1.0) { |
207 | 0 | lbname = "GreedyRefineCentralGPULB"; |
208 | 0 | if (_lb_args.percentMovesAllowed() < 100) |
209 | 0 | migrationTolerance = float(_lb_args.percentMovesAllowed())/100.0; |
210 | 0 | concurrent = (_lb_args.greedyRefineTolerance() <= 0.0); |
211 | 0 | } Unexecuted instantiation: _ZN24GreedyRefineCentralGPULBC2EP16CkMigrateMessage Unexecuted instantiation: _ZN24GreedyRefineCentralGPULBC1EP16CkMigrateMessage |
212 | | |
213 | | // ------------------------------------------------ |
214 | | |
215 | | // regular greedy lb algorithm (CPU-only / non-CUDA path) |
216 | | double GreedyRefineCentralGPULB::greedyLB(const std::vector<GreedyRefineCentralGPULB::GObj*> &pobjs, |
217 | | GreedyRefineCentralGPULB::PHeap &procHeap, |
218 | | const BaseLB::LDStats *stats) const |
219 | 0 | { |
220 | 0 | double max_load = 0; |
221 | 0 | int nmoves = 0; |
222 | 0 | for (int i=0; i < pobjs.size(); i++) { |
223 | 0 | const GreedyRefineCentralGPULB::GObj *obj = pobjs[i]; |
224 | 0 | GreedyRefineCentralGPULB::GProc *p = procHeap.pop(); // least loaded processor |
225 | | // update processor load |
226 | 0 | p->load += (obj->load / p->speed); |
227 | 0 | procHeap.push(p); |
228 | |
|
229 | 0 | if (p->id != obj->oldPE) nmoves++; |
230 | 0 | if (p->load > max_load) max_load = p->load; |
231 | 0 | } |
232 | |
|
233 | 0 | if ((CkMyPe() == cur_ld_balancer+1) && (_lb_args.debug() > 1)) { |
234 | 0 | CkPrintf("[%d] %f : Greedy strategy nmoves=%d, max_load=%f\n", CkMyPe(), |
235 | 0 | CkWallTimer() - strategyStartTime, nmoves, max_load); |
236 | 0 | } |
237 | 0 | return max_load; |
238 | 0 | } |
239 | | |
240 | | // ----------------------------------------------- |
241 | | #if __DEBUG_GREEDY_REFINE_GPU_ |
242 | | #include <fstream> |
243 | | void GreedyRefineCentralGPULB::dumpObjLoads(std::vector<GreedyRefineCentralGPULB::GObj> &objs) { |
244 | | std::ofstream outfile("objloads.txt"); |
245 | | outfile << objs.size() << std::endl; |
246 | | for (int i=0; i < objs.size(); i++) { |
247 | | GreedyRefineCentralGPULB::GObj &obj = objs[i]; |
248 | | if ((i > 0) && (i % 100 == 0)) outfile << obj.load << std::endl; |
249 | | else outfile << obj.load << " "; |
250 | | } |
251 | | outfile.close(); |
252 | | } |
253 | | void GreedyRefineCentralGPULB::dumpProcLoads(std::vector<GreedyRefineCentralGPULB::GProc> &procs) { |
254 | | std::ofstream outfile("proc_bg_loads.txt"); |
255 | | outfile << procs.size() << std::endl; |
256 | | for (int i=0; i < procs.size(); i++) { |
257 | | GreedyRefineCentralGPULB::GProc &p = procs[i]; |
258 | | if ((i > 0) && (i % 100 == 0)) outfile << p.load << std::endl; |
259 | | else outfile << p.load << " "; |
260 | | } |
261 | | outfile.close(); |
262 | | } |
263 | | #endif |
264 | | |
265 | | double GreedyRefineCentralGPULB::fillData(LDStats *stats, |
266 | | std::vector<GreedyRefineCentralGPULB::GObj> &objs, |
267 | | std::vector<GreedyRefineCentralGPULB::GObj*> &pobjs, |
268 | | std::vector<GreedyRefineCentralGPULB::GProc> &procs, |
269 | | PHeap &procHeap) |
270 | 0 | { |
271 | 0 | const int n_pes = stats->nprocs(); |
272 | | // Walk every object, not just the migratable ones: the loop below indexes |
273 | | // stats->objData[i] directly, and non-migratable objects must still be charged |
274 | | // as background load on their PE. |
275 | | // |
276 | | // n_migrateobjs is a *count* of migratable objects (CentralLB.C:485 computes it |
277 | | // with count_if over the whole array), not an upper bound on their indices -- |
278 | | // objData is filled in per-PE registration order and is never partitioned, so |
279 | | // migratable and non-migratable objects interleave. Using the count as a loop |
280 | | // bound silently truncates objData: the tail is neither balanced nor charged as |
281 | | // background, so its load vanishes from the objective entirely. Latent whenever |
282 | | // every chare is migratable (n_migrateobjs == objData.size()), which is why |
283 | | // pic2d and jacobi2d never exposed it. |
284 | 0 | const int n_objs = stats->objData.size(); |
285 | | // most of these variables are just for printing stats when _lb_args.debug() |
286 | 0 | int unmigratableObjs = 0; |
287 | 0 | availablePes = 0; totalObjLoad = 0; |
288 | 0 | double minBGLoad = DBL_MAX; double avgBGLoad = 0; double maxBGLoad = 0; |
289 | 0 | double minSpeed = DBL_MAX; double maxSpeed = 0; double avgSpeed = 0; |
290 | 0 | double minOload = DBL_MAX; double maxOload = 0; |
291 | |
|
292 | 0 | for (int pe=0; pe < n_pes; pe++) { |
293 | 0 | GreedyRefineCentralGPULB::GProc &p = procs[pe]; |
294 | 0 | p.id = pe; |
295 | 0 | p.available = stats->procs[pe].available; |
296 | 0 | p.speed = stats->procs[pe].pe_speed; |
297 | 0 | if (p.available) { |
298 | 0 | availablePes++; |
299 | | // CMK_CUDA is always defined -- 0 in a non-CUDA build -- so this must |
300 | | // test its value, not whether it exists. |
301 | 0 | #if !CMK_CUDA |
302 | 0 | p.bgload = stats->procs[pe].bg_walltime; |
303 | 0 | if (p.bgload > maxBGLoad) maxBGLoad = p.bgload; |
304 | | #else |
305 | | // bgload is the background GPU load (aggregated per group below). |
306 | | // cpuLoad is the background CPU/wall load, used for within-group |
307 | | // PE-level balancing; seed it with this PE's background walltime. |
308 | | p.bgload = 0.0; |
309 | | p.bg_walltime = stats->procs[pe].bg_walltime; |
310 | | p.cpuLoad = stats->procs[pe].bg_walltime; |
311 | | #endif |
312 | 0 | if (_lb_args.debug() > 1) { |
313 | 0 | double &speed = stats->procs[pe].pe_speed; |
314 | 0 | if (speed < minSpeed) minSpeed = speed; |
315 | 0 | if (speed > maxSpeed) maxSpeed = speed; |
316 | 0 | avgSpeed += speed; |
317 | 0 | } |
318 | 0 | } |
319 | 0 | } |
320 | 0 | if (!availablePes) CkAbort("GreedyRefineCentralGPULB: No available processors\n"); |
321 | |
|
322 | 0 | for (int i=0; i < n_objs; i++) { |
323 | 0 | LDObjData &oData = stats->objData[i]; |
324 | 0 | GreedyRefineCentralGPULB::GObj &obj = objs[i]; |
325 | 0 | int pe = stats->from_proc[i]; |
326 | 0 | obj.id = i; |
327 | 0 | obj.oldPE = pe; |
328 | 0 | CkAssert(pe >= 0 && pe <= n_pes); |
329 | 0 | if (pe == n_pes) obj.oldPE = -1; // this can happen in HybridLB if object comes from outside group. mark oldPE as -1 in this situation |
330 | 0 | if (!oData.migratable) { |
331 | 0 | CkAssert(pe < n_pes); |
332 | 0 | unmigratableObjs++; |
333 | 0 | GreedyRefineCentralGPULB::GProc &p = procs[pe]; |
334 | 0 | if (!p.available) |
335 | 0 | CkAbort("GreedyRefineCentralGPULB: nonmigratable object on unavailable processor\n"); |
336 | | #if CMK_CUDA |
337 | | // GPU load is background for the PE's GPU group; CPU load is background |
338 | | // for the PE itself. |
339 | | p.bgload += oData.gpuTime; |
340 | | p.cpuLoad += oData.wallTime; |
341 | | if (p.bgload > maxBGLoad) maxBGLoad = p.bgload; |
342 | | #else |
343 | 0 | double nmObjLoad = oData.wallTime; |
344 | 0 | p.bgload += nmObjLoad; // take non-migratable object load as background load |
345 | 0 | if (p.bgload > maxBGLoad) maxBGLoad = p.bgload; |
346 | 0 | #endif |
347 | 0 | } else { |
348 | | #if CMK_CUDA |
349 | | // load -> GPU work (cross-group objective); cpuLoad -> CPU work (intra-group) |
350 | | obj.load = oData.gpuTime; |
351 | | obj.cpuLoad = oData.wallTime; |
352 | | #else |
353 | 0 | obj.load = oData.wallTime * stats->procs[pe].pe_speed; |
354 | 0 | obj.cpuLoad = oData.wallTime; |
355 | 0 | #endif |
356 | 0 | pobjs.push_back(&obj); |
357 | 0 | totalObjLoad += obj.load; |
358 | 0 | if (_lb_args.debug() > 1) { |
359 | 0 | if (obj.load < minOload) minOload = obj.load; |
360 | 0 | if (obj.load > maxOload) maxOload = obj.load; |
361 | 0 | } |
362 | 0 | } |
363 | 0 | } |
364 | |
|
365 | 0 | procHeap.addProcessors(procs, (maxBGLoad <= 0.001), true); |
366 | | |
367 | | // ---- print some stats ---- |
368 | 0 | if ((_lb_args.debug() > 1) && (!concurrent || (CkMyPe() == cur_ld_balancer))) { |
369 | 0 | for (int pe=0; pe < n_pes; pe++) { |
370 | 0 | GreedyRefineCentralGPULB::GProc &p = procs[pe]; |
371 | 0 | if (!p.available) continue; |
372 | 0 | if (p.bgload < minBGLoad) minBGLoad = p.bgload; |
373 | 0 | avgBGLoad += p.bgload; |
374 | 0 | } |
375 | 0 | CkPrintf("[%d] GreedyRefineCentralGPULB: num pes=%d, num objs=%d (%d migratable, %d background)\n", |
376 | 0 | CkMyPe(), n_pes, n_objs, n_objs - unmigratableObjs, unmigratableObjs); |
377 | 0 | CkPrintf("[%d] Unavailable processors=%d, Unmigratable objs=%d\n", CkMyPe(), n_pes - availablePes, unmigratableObjs); |
378 | 0 | CkPrintf("[%d] min_bgload=%f mean_bgload=%f max_bgload=%f\n", CkMyPe(), minBGLoad, (avgBGLoad / availablePes), maxBGLoad); |
379 | 0 | CkPrintf("[%d] min_oload=%f mean_oload=%f max_oload=%f\n", CkMyPe(), minOload, (totalObjLoad / (n_objs - unmigratableObjs)), maxOload); |
380 | 0 | CkPrintf("[%d] min_speed=%f mean_speed=%f max_speed=%f\n", CkMyPe(), minSpeed, (avgSpeed / availablePes), maxSpeed); |
381 | 0 | } |
382 | |
|
383 | 0 | return maxBGLoad; |
384 | 0 | } |
385 | | |
386 | | static const float Avals[] = {1.0, 1.005, 1.01, 1.015, 1.02, 1.03, 1.04, 1.05, 1.06, 1.07, 1.08, 1.16, 1.20, 1.30}; |
387 | | static const float Bvals[] = {FLT_MAX, 1.0, 1.05, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, 2.1, 2.2, 2.3}; |
388 | 0 | #define Avals_len 14 |
389 | 0 | #define Bvals_len 16 |
390 | 0 | #define NUM_SOLUTIONS Avals_len*Bvals_len+1 |
391 | 0 | static void getGreedyRefineParams(int rank, float &A, float &B) { |
392 | 0 | if (rank == 0) { A = 0; B = -1; return; } // causes PE0 to run regular greedy |
393 | 0 | rank--; |
394 | 0 | int x = rank / Bvals_len; |
395 | 0 | if (x >= Avals_len) { |
396 | 0 | A = B = -1; |
397 | 0 | } else { |
398 | 0 | A = Avals[x]; |
399 | 0 | B = Bvals[rank % Bvals_len]; |
400 | 0 | } |
401 | 0 | } |
402 | | |
403 | | void GreedyRefineCentralGPULB::sendSolution(double maxLoad, int migrations) |
404 | 0 | { |
405 | | // gather results in central PE, who will decide which solution is the best |
406 | | // only the objective values of the solutions are sent, not the whole solutions |
407 | |
|
408 | 0 | GreedyRefineCentralGPULB::Solution sol(CkMyPe(), maxLoad, migrations); |
409 | 0 | size_t buf_size = sizeof(GreedyRefineCentralGPULB::Solution); |
410 | 0 | void *buffer = malloc(buf_size); |
411 | 0 | PUP::toMem pd(buffer); |
412 | 0 | pd|sol; |
413 | |
|
414 | 0 | CkCallback cb(CkIndex_GreedyRefineCentralGPULB::receiveSolutions((CkReductionMsg*)NULL), thisProxy[cur_ld_balancer]); |
415 | 0 | contribute(buf_size, buffer, CkReduction::set, cb); |
416 | |
|
417 | 0 | if ((_lb_args.debug() > 1) && (CkMyPe() == cur_ld_balancer)) { |
418 | 0 | CkPrintf("[%d] %f : Called gather/reduction\n", CkMyPe(), CkWallTimer() - strategyStartTime); |
419 | 0 | } |
420 | |
|
421 | 0 | free(buffer); |
422 | 0 | } |
423 | | |
424 | | void GreedyRefineCentralGPULB::work(LDStats *stats) |
425 | 0 | { |
426 | 0 | strategyStartTime = CkWallTimer(); |
427 | | // Greedy's max host load, the CPU twin of greedyMaxLoad. Set in the GPU path |
428 | | // only; the CPU-only path below has a single dimension and uses greedyMaxLoad. |
429 | 0 | double greedyMaxCpuLoad = 0.0; |
430 | | // maxCpuLoad itself is scoped to the GPU branch; carry it out for the report. |
431 | 0 | double maxCpuLoadReported = 0.0; |
432 | | |
433 | | // A is the tolerance: the refine target is plain greedy's max load times A |
434 | | // (M *= A below), and an object stays where it is while its PE is under that. |
435 | | // B is the stay-put band against the lightest PE; FLT_MAX means the target is |
436 | | // the only thing holding an object back, which is the documented behaviour. |
437 | 0 | float A = 1.001, B = FLT_MAX; // Use A=0, B=-1 to imitate regular Greedy (ignore migrations) |
438 | 0 | if (_lb_args.greedyRefineTolerance() > 0.0) |
439 | 0 | A = (float)_lb_args.greedyRefineTolerance(); |
440 | 0 | if (concurrent) { |
441 | 0 | getGreedyRefineParams(CkMyPe(), A, B); |
442 | 0 | if (A < 0) { |
443 | 0 | sendSolution(-1,-1); // send empty response to PE0 |
444 | 0 | return; |
445 | 0 | } |
446 | 0 | } |
447 | | |
448 | 0 | const int n_pes = stats->nprocs(); |
449 | 0 | totalObjs = stats->n_migrateobjs; |
450 | | |
451 | | // Sized to every object, because fillData indexes this by LDStats object index. |
452 | | // totalObjs stays the migratable count -- it bounds how many objects may move |
453 | | // (migrationsAllowed below), which is a different quantity from how many exist. |
454 | 0 | std::vector<GreedyRefineCentralGPULB::GObj> objs(stats->objData.size()); |
455 | | // will sort pobjs instead of objs (faster swapping). will only contain pointers |
456 | | // to migratable objects |
457 | 0 | std::vector<GreedyRefineCentralGPULB::GObj*> pobjs; |
458 | 0 | pobjs.reserve(totalObjs); |
459 | |
|
460 | 0 | std::vector<GreedyRefineCentralGPULB::GProc> procs(n_pes); |
461 | 0 | PHeap procHeap(n_pes); |
462 | | |
463 | | // fill data structures used by algorithm |
464 | 0 | double maxLoad = fillData(stats, objs, pobjs, procs, procHeap); |
465 | | |
466 | | // ------------ apply greedy refine algorithm -------------- |
467 | | |
468 | | // Sort by GPU load (GObj::load) — the cross-group objective. |
469 | 0 | std::sort(pobjs.begin(), pobjs.end(), GreedyRefineCentralGPULB::ObjLoadGreater()); |
470 | |
|
471 | 0 | int nmoves = 0; |
472 | 0 | double greedyMaxLoad = 0; |
473 | |
|
474 | | #if CMK_CUDA |
475 | | // ---- Two-level GPU-aware path ---- |
476 | | // |
477 | | // Outer: balance GPU load (GObj::load) across GPU groups. A GPU group is the |
478 | | // set of available PEs that share a gpu_device_id (the PEs of one |
479 | | // process bound to one GPU). |
480 | | // Inner: within the chosen group, balance CPU load (GObj::cpuLoad / GProc::cpuLoad) |
481 | | // across the group's PEs. |
482 | | |
483 | | struct GPUGrp { |
484 | | uint64_t gpu_id; |
485 | | double load; // aggregate GPU load across PEs in this group |
486 | | std::vector<int> peIds; // indices into procs[] |
487 | | }; |
488 | | |
489 | | std::vector<GPUGrp> gpuGroups; |
490 | | std::unordered_map<uint64_t, int> gpuIdToIdx; |
491 | | |
492 | | for (int pe = 0; pe < n_pes; pe++) { |
493 | | GreedyRefineCentralGPULB::GProc &p = procs[pe]; |
494 | | if (!p.available) continue; |
495 | | uint64_t devId = stats->procs[pe].gpu_device_id; |
496 | | |
497 | | auto it = gpuIdToIdx.find(devId); |
498 | | if (it == gpuIdToIdx.end()) { |
499 | | gpuIdToIdx[devId] = gpuGroups.size(); |
500 | | GPUGrp g; |
501 | | g.gpu_id = devId; |
502 | | g.load = p.bgload; // background GPU load |
503 | | g.peIds.push_back(pe); |
504 | | gpuGroups.push_back(std::move(g)); |
505 | | } else { |
506 | | gpuGroups[it->second].load += p.bgload; |
507 | | gpuGroups[it->second].peIds.push_back(pe); |
508 | | } |
509 | | } |
510 | | int nGroups = gpuGroups.size(); |
511 | | |
512 | | if ((_lb_args.debug() > 1) && (CkMyPe() == cur_ld_balancer)) { |
513 | | CkPrintf("[%d] GreedyRefineCentralGPULB: %d GPU group(s), %d available PEs, %d migratable objs\n", |
514 | | CkMyPe(), nGroups, availablePes, (int)pobjs.size()); |
515 | | for (auto &g : gpuGroups) |
516 | | CkPrintf("[%d] GPU %llu: %d PEs, bgGpuLoad=%.6f\n", |
517 | | CkMyPe(), (unsigned long long)g.gpu_id, (int)g.peIds.size(), g.load); |
518 | | } |
519 | | |
520 | | // What a step is actually made of, per PE, so the objective can be checked |
521 | | // against reality: if the measured step time is not tracked by whichever of |
522 | | // the CPU and GPU terms the balancer minimises, then balancing that term |
523 | | // cannot make the step shorter no matter how well it succeeds. |
524 | | if ((_lb_args.debug() > 1) && (CkMyPe() == cur_ld_balancer)) { |
525 | | std::vector<double> peCpu(n_pes, 0.0), peGpu(n_pes, 0.0); |
526 | | std::vector<int> peObjs(n_pes, 0); |
527 | | for (size_t i = 0; i < stats->objData.size(); i++) { |
528 | | const int pe = stats->from_proc[i]; |
529 | | if (pe < 0 || pe >= n_pes) continue; |
530 | | peCpu[pe] += stats->objData[i].wallTime; |
531 | | peGpu[pe] += stats->objData[i].gpuTime; |
532 | | peObjs[pe]++; |
533 | | } |
534 | | CkPrintf("[%d] --- step composition (s) ---\n", CkMyPe()); |
535 | | for (int pe = 0; pe < n_pes; pe++) { |
536 | | const double tot = stats->procs[pe].total_walltime; |
537 | | const double idle = stats->procs[pe].idletime; |
538 | | CkPrintf("[%d] PE %d objs=%d total=%.6f idle=%.6f (%.0f%%) bg=%.6f " |
539 | | "objCPU=%.6f objGPU=%.6f busy=%.6f\n", |
540 | | CkMyPe(), pe, peObjs[pe], tot, idle, |
541 | | tot > 0 ? 100.0 * idle / tot : 0.0, |
542 | | stats->procs[pe].bg_walltime, peCpu[pe], peGpu[pe], tot - idle); |
543 | | } |
544 | | } |
545 | | |
546 | | // --- Greedy preprocessing at GPU-group level to establish target M --- |
547 | | double M = 0; |
548 | | { |
549 | | std::vector<double> grpLoad(nGroups); |
550 | | for (int gi = 0; gi < nGroups; gi++) grpLoad[gi] = gpuGroups[gi].load; |
551 | | |
552 | | for (int i = 0; i < (int)pobjs.size(); i++) { |
553 | | int lightest = 0; |
554 | | for (int gi = 1; gi < nGroups; gi++) |
555 | | if (grpLoad[gi] < grpLoad[lightest]) lightest = gi; |
556 | | grpLoad[lightest] += pobjs[i]->load; |
557 | | if (grpLoad[lightest] > M) M = grpLoad[lightest]; |
558 | | } |
559 | | greedyMaxLoad = M; |
560 | | } |
561 | | M *= A; |
562 | | |
563 | | // --- The same preprocessing at PE level, for the CPU target Mcpu --- |
564 | | // |
565 | | // The inner decision balances host load across the PEs of a group, and until |
566 | | // now had no ceiling at all: only B held an object in place, so B = FLT_MAX |
567 | | // meant nothing ever moved whatever the tolerance said. Give it the twin of |
568 | | // M so the tolerance governs both decisions in the same terms. |
569 | | // |
570 | | // On a copy: procs[].cpuLoad is the real background load the assignment pass |
571 | | // starts from, and unlike the GPU groups it is never reset. |
572 | | double Mcpu = 0; |
573 | | { |
574 | | std::vector<double> peLoad(n_pes); |
575 | | for (int pe = 0; pe < n_pes; pe++) |
576 | | peLoad[pe] = procs[pe].available ? procs[pe].cpuLoad : 0.0; |
577 | | for (int i = 0; i < (int)pobjs.size(); i++) { |
578 | | int lightest = -1; |
579 | | for (int pe = 0; pe < n_pes; pe++) { |
580 | | if (!procs[pe].available) continue; |
581 | | if (lightest < 0 || peLoad[pe] < peLoad[lightest]) lightest = pe; |
582 | | } |
583 | | if (lightest < 0) break; // no available PE: nothing to target |
584 | | peLoad[lightest] += pobjs[i]->cpuLoad / procs[lightest].speed; |
585 | | if (peLoad[lightest] > Mcpu) Mcpu = peLoad[lightest]; |
586 | | } |
587 | | greedyMaxCpuLoad = Mcpu; |
588 | | } |
589 | | Mcpu *= A; |
590 | | |
591 | | // Reset GPU group loads back to bg-only for the real assignment pass. |
592 | | for (int gi = 0; gi < nGroups; gi++) { |
593 | | gpuGroups[gi].load = 0; |
594 | | for (int pe : gpuGroups[gi].peIds) |
595 | | gpuGroups[gi].load += procs[pe].bgload; |
596 | | } |
597 | | |
598 | | // Reverse map: PE index -> GPU group index |
599 | | std::unordered_map<int, int> peToGrpIdx; |
600 | | for (int gi = 0; gi < nGroups; gi++) |
601 | | for (int pe : gpuGroups[gi].peIds) |
602 | | peToGrpIdx[pe] = gi; |
603 | | |
604 | | // Two dimensions are tracked separately. maxLoad follows the GPU-group |
605 | | // aggregate and keeps driving the refine target M below; maxCpuLoad follows |
606 | | // the busiest PE's CPU load. Only their combination is reported as the |
607 | | // solution objective -- see the note after the assignment loop. |
608 | | maxLoad = 0; |
609 | | for (int gi = 0; gi < nGroups; gi++) |
610 | | if (gpuGroups[gi].load > maxLoad) maxLoad = gpuGroups[gi].load; |
611 | | |
612 | | double maxCpuLoad = 0; |
613 | | for (int pe = 0; pe < n_pes; pe++) |
614 | | if (procs[pe].available && procs[pe].cpuLoad > maxCpuLoad) |
615 | | maxCpuLoad = procs[pe].cpuLoad; |
616 | | |
617 | | // --- Main assignment loop --- |
618 | | for (int i = 0; i < (int)pobjs.size(); i++) { |
619 | | const GreedyRefineCentralGPULB::GObj *obj = pobjs[i]; |
620 | | double obj_gpu = obj->load; |
621 | | double obj_cpu = obj->cpuLoad; |
622 | | |
623 | | // ----- Outer: choose a GPU group by GPU load ----- |
624 | | int lightest_gi = 0; |
625 | | for (int gi = 1; gi < nGroups; gi++) |
626 | | if (gpuGroups[gi].load < gpuGroups[lightest_gi].load) lightest_gi = gi; |
627 | | |
628 | | int chosen_gi = lightest_gi; |
629 | | if (obj->oldPE >= 0) { |
630 | | auto curIt = peToGrpIdx.find(obj->oldPE); |
631 | | if (curIt != peToGrpIdx.end()) { |
632 | | int cur_gi = curIt->second; |
633 | | GPUGrp &curGrp = gpuGroups[cur_gi]; |
634 | | // Keep on current GPU group if it is within B tolerance of the lightest |
635 | | // group and adding this object keeps it under the target M. |
636 | | if ((curGrp.load <= (gpuGroups[lightest_gi].load + 0.01) * B) && |
637 | | (curGrp.load + obj_gpu <= M)) |
638 | | chosen_gi = cur_gi; |
639 | | } |
640 | | } |
641 | | |
642 | | GPUGrp &g = gpuGroups[chosen_gi]; |
643 | | |
644 | | // ----- Inner: choose a PE within the group by CPU load ----- |
645 | | int bestPe = g.peIds[0]; |
646 | | for (int pe : g.peIds) |
647 | | if (procs[pe].cpuLoad < procs[bestPe].cpuLoad) bestPe = pe; |
648 | | |
649 | | // Keep the object on its old PE if that PE is in the chosen group and its |
650 | | // CPU load is within B tolerance of the lightest PE — reduces migrations. |
651 | | if (obj->oldPE >= 0 && peToGrpIdx.count(obj->oldPE) && |
652 | | peToGrpIdx[obj->oldPE] == chosen_gi && |
653 | | procs[obj->oldPE].cpuLoad <= (procs[bestPe].cpuLoad + 0.01) * B && |
654 | | procs[obj->oldPE].cpuLoad + obj_cpu / procs[obj->oldPE].speed <= Mcpu) |
655 | | bestPe = obj->oldPE; |
656 | | |
657 | | GreedyRefineCentralGPULB::GProc *p = &procs[bestPe]; |
658 | | |
659 | | // Update GPU group aggregate (GPU load) and PE CPU load. |
660 | | g.load += obj_gpu; |
661 | | p->cpuLoad += obj_cpu / p->speed; |
662 | | |
663 | | if (g.load > maxLoad) { |
664 | | maxLoad = g.load; |
665 | | if (maxLoad > M) M = maxLoad; |
666 | | } |
667 | | if (p->cpuLoad > maxCpuLoad) { |
668 | | maxCpuLoad = p->cpuLoad; |
669 | | if (maxCpuLoad > Mcpu) Mcpu = maxCpuLoad; |
670 | | } |
671 | | |
672 | | if (bestPe != obj->oldPE) { |
673 | | nmoves++; |
674 | | stats->to_proc[obj->id] = bestPe; |
675 | | } |
676 | | } |
677 | | |
678 | | // A step is not over until both resources are done with it, and because |
679 | | // kernels are launched asynchronously the two overlap, so a group's step time |
680 | | // is set by whichever dimension is busier: its GPU or its busiest PE. |
681 | | // Reporting only the GPU term hides every intra-group difference -- with a |
682 | | // single GPU group that term is the same for every (A,B) candidate, so |
683 | | // receiveSolutions falls through to its fewest-migrations tie-break and picks |
684 | | // the do-nothing solution. That is why a one-GPU run never migrated anything. |
685 | | maxCpuLoadReported = maxCpuLoad; |
686 | | maxLoad = std::max(maxLoad, maxCpuLoad); |
687 | | |
688 | | if ((_lb_args.debug() > 1) && (CkMyPe() == cur_ld_balancer)) { |
689 | | CkPrintf("[%d] --- Per-GPU-group GPU load after LB ---\n", CkMyPe()); |
690 | | for (int gi = 0; gi < nGroups; gi++) |
691 | | CkPrintf("[%d] GPU %llu: aggregate gpuLoad=%.6f\n", |
692 | | CkMyPe(), (unsigned long long)gpuGroups[gi].gpu_id, gpuGroups[gi].load); |
693 | | } |
694 | | |
695 | | #else |
696 | | // ---- Original PE-level greedy refine (non-GPU path, CPU only) ---- |
697 | |
|
698 | 0 | double M = 0; |
699 | 0 | if (B > 0) { |
700 | 0 | M = greedyLB(pobjs, procHeap, stats); |
701 | 0 | greedyMaxLoad = M; |
702 | 0 | procHeap.addProcessors(procs, (maxLoad <= 0.001), false); |
703 | 0 | } |
704 | |
|
705 | 0 | M *= A; |
706 | 0 | for (int i=0; i < pobjs.size(); i++) { |
707 | 0 | const GreedyRefineCentralGPULB::GObj *obj = pobjs[i]; |
708 | 0 | GreedyRefineCentralGPULB::GProc *llp = procHeap.top(); |
709 | 0 | GreedyRefineCentralGPULB::GProc *prevPe = NULL; |
710 | 0 | if (obj->oldPE >= 0) prevPe = &(procs[obj->oldPE]); |
711 | |
|
712 | 0 | GreedyRefineCentralGPULB::GProc *p = llp; |
713 | 0 | if (prevPe && (prevPe->load <= (llp->load+0.01)*B) && (prevPe->load + obj->load <= M) && (prevPe->available)) |
714 | 0 | p = prevPe; |
715 | |
|
716 | 0 | procHeap.remove(p); |
717 | 0 | p->load += (obj->load / p->speed); |
718 | 0 | procHeap.push(p); |
719 | |
|
720 | 0 | if (p->id != obj->oldPE) { |
721 | 0 | nmoves++; |
722 | 0 | stats->to_proc[obj->id] = p->id; |
723 | 0 | } |
724 | 0 | if (p->load > maxLoad) { |
725 | 0 | maxLoad = p->load; |
726 | 0 | if (maxLoad > M) M = maxLoad; |
727 | 0 | } |
728 | 0 | } |
729 | 0 | #endif |
730 | | // ---------------------------------------------- |
731 | |
|
732 | 0 | if (concurrent) { |
733 | 0 | sendSolution(maxLoad, nmoves); |
734 | |
|
735 | | #if __DEBUG_GREEDY_REFINE_GPU_ |
736 | | CkCallback cb(CkReductionTarget(GreedyRefineCentralGPULB, receiveTotalTime), thisProxy[cur_ld_balancer]); |
737 | | contribute(sizeof(double), &strategyStartTime, CkReduction::sum_double, cb); |
738 | | #endif |
739 | 0 | } else if (_lb_args.debug() > 0) { |
740 | | // Single-candidate mode, so this PE's result is the result. Report what the |
741 | | // tolerance bought: the ratio to greedy says how much max load was given up, |
742 | | // the migration count says what that saved. |
743 | 0 | double gpuRatio = 1.0, cpuRatio = 1.0; |
744 | 0 | if (greedyMaxLoad > 0) gpuRatio = maxLoad / greedyMaxLoad; |
745 | 0 | if (greedyMaxCpuLoad > 0) cpuRatio = maxCpuLoadReported / greedyMaxCpuLoad; |
746 | 0 | CkPrintf("CharmLB> %s: after lb, migrations=%d(%.2f%%), tolerance=%.3f, " |
747 | 0 | "gpu max=%.4f (%.2fx greedy), cpu max=%.4f (%.2fx greedy)\n", |
748 | 0 | lbname, nmoves, 100.0 * nmoves / double(pobjs.size()), A, |
749 | 0 | maxLoad, gpuRatio, maxCpuLoadReported, cpuRatio); |
750 | 0 | } |
751 | 0 | } |
752 | | |
753 | | void GreedyRefineCentralGPULB::receiveTotalTime(double time) |
754 | 0 | { |
755 | 0 | CkPrintf("Avg start time of GreedyRefineCentralGPULB strategy is %f\n", time / CkNumPes()); |
756 | 0 | } |
757 | | |
758 | | // decide which solution among all PEs is best and apply it |
759 | | void GreedyRefineCentralGPULB::receiveSolutions(CkReductionMsg *msg) |
760 | 0 | { |
761 | 0 | std::vector<GreedyRefineCentralGPULB::Solution> results(NUM_SOLUTIONS); |
762 | |
|
763 | 0 | int migrationsAllowed = totalObjs * migrationTolerance; |
764 | | // feasible solutions are those satistying user's migration constraint |
765 | 0 | bool feasibleSolutions = false; |
766 | 0 | float lowest_max_load = FLT_MAX; // lowest max load of all solutions |
767 | 0 | float lowest_max_load_f = FLT_MAX; // lowest max load of feasible solution set |
768 | 0 | float highest_max_load = 0; // highest max load of all solutions |
769 | 0 | int lowestMigrations = INT_MAX; // lowest num migrations of all solutions |
770 | 0 | const GreedyRefineCentralGPULB::Solution *bestSol = NULL; // best solution |
771 | | |
772 | | // first pass. Will record solution with lowest migrations as the best, in case |
773 | | // there is no feasible solution |
774 | 0 | CkReduction::setElement *current = (CkReduction::setElement*)msg->getData(); // Get the first element in the set |
775 | 0 | int numSolutions = 0; |
776 | 0 | for ( ; current && (numSolutions < NUM_SOLUTIONS); current = current->next()) { |
777 | 0 | PUP::fromMem pd(¤t->data); |
778 | 0 | pd|results[numSolutions]; // store result |
779 | 0 | if (results[numSolutions].migrations >= 0) { // valid result |
780 | 0 | const GreedyRefineCentralGPULB::Solution &r = results[numSolutions++]; |
781 | 0 | if ((r.migrations <= migrationsAllowed) && (r.max_load < lowest_max_load_f)) { |
782 | 0 | lowest_max_load_f = r.max_load; |
783 | 0 | feasibleSolutions = true; |
784 | 0 | } |
785 | |
|
786 | 0 | if ((r.migrations < lowestMigrations) || |
787 | 0 | ((r.migrations == lowestMigrations) && (r.max_load < bestSol->max_load))) { |
788 | 0 | lowestMigrations = r.migrations; |
789 | 0 | bestSol = &r; |
790 | 0 | } |
791 | |
|
792 | 0 | if (r.max_load < lowest_max_load) lowest_max_load = r.max_load; |
793 | 0 | if (r.max_load > highest_max_load) highest_max_load = r.max_load; |
794 | 0 | } |
795 | 0 | } |
796 | 0 | results.resize(numSolutions); // for cases where CkNumPes() < NUM_SOLUTIONS |
797 | 0 | CkAssert(numSolutions > 0); |
798 | |
|
799 | 0 | if (feasibleSolutions) { |
800 | | // second pass, get solution with low max load and migrations from feasible set |
801 | 0 | int bestMigrations = INT_MAX; // num migrations of best solution |
802 | 0 | for (int i=0; i < results.size(); i++) { |
803 | 0 | const GreedyRefineCentralGPULB::Solution &r = results[i]; |
804 | 0 | if ((r.migrations < bestMigrations && r.max_load <= lowest_max_load_f*LOAD_MIG_BAL) || |
805 | 0 | (r.migrations == bestMigrations && r.max_load < bestSol->max_load)) { |
806 | 0 | bestMigrations = r.migrations; |
807 | 0 | bestSol = &r; |
808 | 0 | } |
809 | 0 | } |
810 | 0 | } |
811 | | // else: can't satisfy user migration constraint (for this lb step), |
812 | | // so just use solution with lowest num migrations |
813 | |
|
814 | 0 | if (_lb_args.debug() > 1) { |
815 | 0 | CkPrintf("GreedyRefineCentralGPULB: Lowest max_load is %f, worst max_load is %f, lowest migrations=%d\n", |
816 | 0 | lowest_max_load, highest_max_load, lowestMigrations); |
817 | 0 | CkPrintf("GreedyRefineCentralGPULB: Got %d solutions at %f\nBest one is from PE %d with max_load=%f, migrations=%d\n", |
818 | 0 | numSolutions, CkWallTimer(), bestSol->pe, bestSol->max_load, bestSol->migrations); |
819 | 0 | float A, B; |
820 | 0 | getGreedyRefineParams(bestSol->pe, A, B); |
821 | 0 | CkPrintf("Best PE used params A=%f B=%f\n", A, B); |
822 | 0 | } |
823 | | |
824 | | // notify PE that produced the best solution |
825 | 0 | thisProxy[bestSol->pe].ApplyDecision(); |
826 | 0 | } |
827 | | |
828 | | #include "GreedyRefineCentralGPULB.def.h" |
829 | | |
830 | | /*@}*/ |