Coverage Report

Created: 2026-10-29 22:51

/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(&current->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
/*@}*/