-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathsssp_smp.cpp
More file actions
5626 lines (5481 loc) · 248 KB
/
Copy pathsssp_smp.cpp
File metadata and controls
5626 lines (5481 loc) · 248 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#include "TopoManager.h"
#include "htram_group.h"
#include "sssp_smp.decl.h"
#include "graphlib/graphlib.h"
#include "process_work.h"
#include "live_slack.h"
#include "round_profile.h"
#include "graphlib/tile_layout.h"
#ifdef PAPI
#include "acic_prof.h"
#endif
#ifdef ACIC_COMM_SHARE
#include <x86intrin.h>
// Step 7.6o: how a PE's solve splits into the solver's own work and
// everything else (sends, network progress, the scheduler, waiting). Work is
// the time inside process_heap() and the TRAM delivery callback, less the
// htram sends made from inside them; one rdtsc pair per call, so cheap next
// to the per-item cost. Build with -DACIC_COMM_SHARE in CHARMC_SMP, so htram
// counts its sends too.
namespace comm_share {
thread_local unsigned long work_tsc = 0, work_send_tsc = 0;
thread_local int depth = 0;
struct Work {
unsigned long t0, s0;
Work() {
if (depth++ == 0) {
t0 = __rdtsc();
s0 = htram_send_tsc;
}
}
~Work() {
if (--depth == 0) {
work_tsc += __rdtsc() - t0;
work_send_tsc += htram_send_tsc - s0;
}
}
};
thread_local unsigned long window_tsc0 = 0;
thread_local double window_s0 = 0;
// Step 8e: time between the scheduler's begin-idle and end-idle conditions,
// less the solver work the idle callback did inside it (process_heap runs
// from there), so idle + work never counts a tick twice.
thread_local unsigned long idle_tsc = 0, idle_t0 = 0, idle_work0 = 0;
inline void idle_begin(void *) {
idle_t0 = __rdtsc();
idle_work0 = work_tsc;
}
inline void idle_end(void *) {
if (idle_t0) {
idle_tsc += (__rdtsc() - idle_t0) - (work_tsc - idle_work0);
idle_t0 = 0;
}
}
} // namespace comm_share
#define COMM_SHARE_WORK comm_share::Work comm_share_work_;
#else
#define COMM_SHARE_WORK
#endif
#include <algorithm>
#include <atomic>
#include <memory>
#include <mutex>
#include <cmath>
#include <fstream>
#include <iostream>
#include <limits>
#include <map>
#include <queue>
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <sstream>
#define INFO_PRINTS
// #define PRINT_HISTO //print histograms to file
#define LOCAL_TO_TRAM // add all outgoing updates (even local) to tram
// #define PQ_HOLD_ONLY
// #define PQ_EDGE_DIST //add cost of smallest edge when finding bucket
// #define VCOUNT
// #define ALL_TO_TRAM_HOLD //place all updates in the tram hold at first
// set data type for messages
using tram_proxy_t = CProxy_HTram;
using tram_t = HTram;
/* readonly */
// tram_proxy_t tram_proxy;
CProxy_HTramRecv nodeGrpProxy;
CProxy_HTramNodeGrp srcNodeGrpProxy;
CProxy_Main mainProxy;
CProxy_SsspChares arr;
CProxy_SharedInfo shared;
int N; // number of processors
long V; // number of vertices
int M = 1024; // divisor for dest_table (must be power of 2)
long num_global_edges; // number of global edges in the graph (used when graph
// is generated)
long average_degree; // average degree of graph
int generate_mode; // 0 = read from file, 1 = generate automatically
int S; // seed for randomization
cost lmax; // long maximum
#define HISTO_BUCKET_COUNT 2048 // needed macro for array init
// Degrees and arrival counts are binned by floor(log2(x)) + 1, with 0 in its
// own bin. 32 covers any count a long can hold that a run could produce.
#define DEGREE_CLASSES 32
// Expansion lead over the frontier, binned the same way (step 8a).
#define LEAD_CLASSES 12
int histo_reduction_width = HISTO_BUCKET_COUNT / 8;
double reduction_delay =
0.1; // each histogram reduction happens at this interval
int initial_threshold = 3; // initial histo threshold
bool verify_mode = false; // --verify: check the result against serial Dijkstra
bool certify_mode = false; // --certify: a distributed proof that the distances are exact
enum { PROCESS_SHARE_OFF = 0, PROCESS_SHARE_ON = 1, PROCESS_SHARE_AUTO = 2 };
int process_share_mode = PROCESS_SHARE_OFF;
enum { PROCESS_QUEUE_LOCAL = 0, PROCESS_QUEUE_NEAREST = 1 };
int process_queue_policy = PROCESS_QUEUE_LOCAL;
int process_queue_batch = 1;
// --process-drain-cap N: at most N workers of a process drain the shared
// queues at once (0 = no cap). The others keep receiving and relaxing, and
// retry on their next idle pass, so work is never stranded in their bins.
int process_drain_cap = 0;
// --heap-slice N: shared-queue entries one process_heap() call expands before
// yielding to the scheduler (1-100; default 100, the long-standing constant).
int heap_slice = 100;
long reader_tile_size = 0;
int reader_tile_owners = 1;
int slack_control_mode = PROCESS_SHARE_OFF;
static bool slack_control_active() {
return slack_control_mode == PROCESS_SHARE_ON ||
(slack_control_mode == PROCESS_SHARE_AUTO && num_global_edges < 8L * V);
}
static bool process_share_active() {
return process_share_mode == PROCESS_SHARE_ON ||
(process_share_mode == PROCESS_SHARE_AUTO && num_global_edges < 8L * V);
}
bool result_digest = false; // --result-digest: emit distances' digest after timing
// Every PE holds two ints per M vertices (dest_table, dest_uniform). At
// M = 1024 that is 128 MB per PE at 2^34 vertices, so M grows with V to keep
// the tables near 2^22 entries. Only the ranges that straddle a partition
// boundary are slower to look up, and there are at most N of them. Main calls
// this whenever it learns V, before the readonlies are sent.
static void scale_dest_table_divisor() {
while (V / M > (1L << 22))
M <<= 1;
}
// Everything needed to rebuild the graph from scratch, used by the serial
// reference. Filled in by Main; not a Charm readonly, because only PE 0 needs
// it and the chares get their slice through their own entry methods.
GraphSpec graph_spec;
// Flush the aggregation buffers once every this many controller rounds, on
// average -- each chare draws independently, so this is a rate and not a
// period, and --flush-interval makes it an A/B. Under the default
// --flush-policy adaptive (below) this draw is the floor, not the whole cadence.
//
// It matters more than it looks. htram's own idle-triggered flush is compiled
// out (IDLE_FLUSH at htram_group.h:7; idleFlush() returns true and does
// nothing) and the periodic timer is off by default, so a partly-filled
// aggregation buffer has exactly two ways out: fill to bufSize, or catch one of
// these draws. In the tail there is not enough traffic left to fill anything,
// which is the mechanism H4 of step 6 is about.
int flush_round_interval = 5;
// --flush-policy fixed|stale|adaptive. Step 6 found the cadence above is the
// whole story on a high-diameter graph -- flushing every round is 3.2x faster
// on the mesh -- and irrelevant or harmful on RMAT, where buffers fill on their
// own. Both non-fixed policies only ever add flushes to what the fixed draw
// does; the draw still runs.
//
// stale flush, every round, each destination whose buffer has not filled
// since the previous round. Local and per destination. Rejected:
// on two nodes a round is shorter than the time an RMAT stream
// takes to fill a buffer, so it fires on half of RMAT's streams,
// sends 26% more messages and costs 12%. Kept so that the A/B
// that rejected it can be rerun.
// adaptive the same per-destination rule, applied only in rounds where the
// controller sees too little work in flight to fill the buffers
// at all -- fewer items in the window than one bufSize per
// (sender PE, destination) stream. The mesh never has that much
// in flight, so it flushes every round; RMAT and the uniform graph
// have it for the middle of the run, so they flush as they did.
// The default: 3.6x faster on the mesh on one node and 3.8x on
// two, and no slower on RMAT or the uniform graph on either. See
// design/step7-flush-cadence.md.
//
// fixed is the pre-step-7 behaviour and what every step 6 number used.
enum { FLUSH_FIXED = 0, FLUSH_STALE = 1, FLUSH_ADAPTIVE = 2 };
int flush_policy = FLUSH_ADAPTIVE;
// --combine off|hold. hold turns on htram's source-side CombiningHold: an
// update waits in its destination's hold until a full buffer's worth has been
// admitted or a flush reaches it, and a later update for the same vertex folds
// into it, keeping the smaller distance. The loser never travels; absorb()
// below does the bookkeeping it would otherwise have done at the receiver.
// Step 6 measured RMAT's rejected updates at 1.14 per edge against a
// batch-local fold's reach of 9.9%; this is the part whose reach is not bounded
// by one message. See design/step7-combining.md.
enum { COMBINE_OFF = 0, COMBINE_HOLD = 1 };
int combine_mode = COMBINE_OFF;
// --batch-fold off|on: the receiver-side half. Before a delivered batch is
// processed, updates in it that repeat a destination vertex are folded to the
// one with the smallest distance, and the rest are accounted as processed.
// Step 6 measured what this can reach -- 41.6% of a batch at 2^14 on RMAT,
// 9.9% at 2^20 -- and the plan requires its contribution to be reported
// separately from the source hold's.
bool batch_fold = false;
// --send-filter-bits N (0 = off). 7.6j. Each PE remembers, in a direct-mapped
// table of 2^N entries, the last distance it created an update with for a
// vertex, and drops a new update to that vertex that is no shorter. The one it
// remembers is still on its way and will be retired at the destination, so the
// dropped one could only have been rejected there -- 98.8% of arrivals were --
// and dropping it before it is charged keeps the histogram exact. A collision
// only overwrites the entry, which costs a missed drop, never a wrong one.
int send_filter_bits = 0;
// --send-filter auto (step 7.6k, the default): the filter at 17 bits, on only
// while the
// buffer size says the input is in the redundant, scale-free regime (at least
// SEND_FILTER_MIN_BUFFER items; see --bufsize-policy). It is a loss where most
// arrivals improve a distance: road-usa 0.79x at 1024 items. An explicit
// --send-filter-bits N keeps it on for the whole run instead (0: off).
bool send_filter_auto = true;
// --lazy-heavy off|auto|<distance> (step 8d). Off: a vertex relaxes every edge
// each time process_heap() finds its distance current, as before. On: it
// relaxes its light edges (weight <= L) then, and leaves the rest behind one
// token per weight range (L, 2L], (2L, 4L], ... The token for range j is
// queued at d + L 2^j, below every update it can make, and charged to the
// histogram like an update at that distance, so the controller admits it
// exactly when it would admit the first update it will make. When it is
// admitted it relaxes its range only if d is still the vertex's distance, and
// queues the next range's token. A vertex that improves meanwhile never sends
// the heavy updates of the distance it left, which is where 8a found the
// growth: at 8 nodes rmat25 created 3.7 heavy updates per heavy edge, and
// RIKEN, which relaxes heavy edges once per settled vertex, sends 0.5.
// on: L is the natural bucket width. auto: the same, but only for inputs of
// average degree 8 or more -- the scale-free regime, the same line the send
// filter draws (SEND_FILTER_MIN_BUFFER). On mesh24 and road-usa at 2 nodes
// every edge is heavy, deferring them lengthens the improvement chains, and
// the solve was 2.1x slower (job 20820633). The token lives only in the local
// queue; it never reaches htram.
enum { LAZY_OFF = 0, LAZY_ON = -1, LAZY_AUTO = -2 };
// auto is the default from step 8e: at 8 nodes rmat25 0.78 -> 0.21 s and
// orkut 0.34 -> 0.20 s with the hold bitmap and the idle-flush interval, and
// it never turns on for the high-diameter graphs.
long lazy_cut = LAZY_AUTO;
// --lazy-heap P: the heap percentile while --lazy-heavy is active, in place of
// the positional one. Tokens make running ahead cheap -- a stale range is
// dropped rather than sent -- so fewer, wider rounds pay: rmat25 at 8 nodes
// went 0.81 s (0.005) -> 0.47 s (0.5) -> 0.42 s (0.95), job 20820559.
double lazy_heap_percentile = 0.95;
// --lazy-growth G: token range j is (L G^j, L G^(j+1)] (default 2). A larger G
// means fewer tokens per vertex -- fewer queue operations -- and a coarser
// deferral of the heavier edges (step 8f).
int lazy_growth = 2;
// --skip-empty auto|on|off (IPDPS ablation): whether a node delivery skips
// the PEs it has no items for (HTram::setSkipEmptyDeliveries). auto follows
// the lazy-relaxation regime, as it always has; on/off separate the two.
enum { SKIP_EMPTY_OFF = 0, SKIP_EMPTY_ON = 1, SKIP_EMPTY_AUTO = 2 };
int skip_empty_mode = SKIP_EMPTY_AUTO;
// --control reduction|node (step 8c). reduction: each round is a Charm++
// reduction to Main and an array broadcast back, as always. Both travel in
// the PEs' own queues, which Reconverse polls only when the node queue --
// where every htram delivery lands -- is empty, so under load a round waits
// for the backlog at every hop: 5-7 ms per round at 8 nodes once the rounds
// became the critical path (8d). node: the PEs of a process sum their
// contributions in shared memory and the last one sends the sum to process 0
// on the node queue; the thresholds come back the same way, into shared
// state that each PE picks up at its next delivery, heap pass or idle pass
// (and, failing those, from a message in its own queue).
//
// The reduction path is paced by the queue it waits in; this one is not, and
// a round per delivery would let the rounds' own messages outgrow the queue
// (a single PE livelocked that way). So a PE keeps at most one heap pass and
// one fallback message queued, and process 0 leaves at least
// --control-interval ms between broadcasts.
// --hub-hints off|D (default off). On rmat26 at 16 nodes 89% of the updates
// that arrive find their target already final, and 1.33B of the 1.81B land on
// vertices of degree 128 or more (acic_slice_diag, job 5538765): the targets
// are the hubs, settled early and hit by every process for the rest of the
// solve. With hints on, every vertex of degree >= D publishes its distance
// once per controller round, whenever it has fallen, to a table in every
// process (HubHints), and a sender drops an update whose distance is no better
// than its target's published one. That needs no notion of settled: distances
// only fall, so a target that has already reached d rejects anything at d or
// above wherever and whenever it arrives. RIKEN's settled-vertex bitmap is the
// same idea restricted to final vertices.
// auto (-1) is D = 256 wherever --lazy-heavy's auto rule holds (average
// degree 8 or more and skewed degrees): on uniform25 every edge paid a table
// probe for no filtering, 0.89-0.92x at 16 nodes (jobs 5538859/61).
enum { HUB_HINTS_AUTO = -1, HUB_HINTS_AUTO_DEGREE = 256 };
long hub_hint_degree = 0; // 0: off, -1: auto
int hub_hint_bits = 0; // table size per process, log2 entries; 0: from |V|
// The degree in force: auto resolves against the graph once it is read.
static bool lazy_active();
static long hub_hint_min_degree() {
if (hub_hint_degree == HUB_HINTS_AUTO)
return lazy_active() ? HUB_HINTS_AUTO_DEGREE : 0;
return hub_hint_degree;
}
enum { CONTROL_REDUCTION = 0, CONTROL_NODE = 1 };
int control_mode = CONTROL_REDUCTION;
double control_interval_ms = 0.25;
CProxy_ControlNode controlProxy;
CProxy_HubHints hintProxy;
class Main;
Main *main_instance = nullptr;
// --warm-links on|off (step 8e). Before the solve, every PE sends one small
// message to a PE of every other process and the solve starts at quiescence.
// ACIC reads its partition locally, so without this the first message
// between two processes is sent inside the timed solve: at 8 nodes rmat25's
// first round took 13.7 ms with 30 updates in flight, and orkut's first five
// rounds 2-6 ms each with almost none, against 0.1-0.2 ms for an idle round
// later on. The baselines' setup already exchanges data all-to-all. Off by
// default: it shortened the first round (11.8 -> 5.3 ms on rmat25 at 8 nodes)
// but not the solve, rmat25 0.250 -> 0.235 s and orkut 0.156 -> 0.169 s,
// within the noise (job 20823450).
bool warm_links_on = false;
// --lazy-skew CV (IPDPS change 2): auto also needs the degree distribution
// to be skewed, a coefficient of variation of at least CV over vertices with
// edges (default 1; 0 restores 8g's average-degree rule). Lazy relaxation
// pays by cutting the traffic that hub re-relaxations send: at 2 nodes it
// takes rmat25 from 1.25e9 to 8.7e8 updates and orkut from 4.0e8 to 1.9e8,
// but leaves uniform25 at 1.07e9 while adding rounds, and uniform25 ran 1.55x
// faster without it (job 20826260). Degree CV: rmat20-27 6.7-13.5, youtube
// 9.6, orkut 2.0; uniform 0.32, road-usa 0.39, meshes 0.01. PE 0 computes it
// from the offsets array it already reads for the partition (mode 4); -1
// (other modes) keeps the average-degree rule alone.
double degree_cv = -1.0;
double lazy_skew_min = 1.0;
static bool lazy_active() {
return lazy_cut > 0 || lazy_cut == LAZY_ON ||
(lazy_cut == LAZY_AUTO && num_global_edges >= 8L * V &&
(degree_cv < 0.0 || degree_cv >= lazy_skew_min));
}
const int SEND_FILTER_AUTO_BITS = 17;
const int SEND_FILTER_MIN_BUFFER = 2048;
// --idle-flush off|on|starved. The per-chare [whenidle] callback drains the
// heap; with this on, a PE that finds nothing admissible left also flushes
// every destination still holding admitted updates. An idle PE cannot add to
// its buffers until a message arrives, so a buffer it holds only waits for the
// next round boundary. --flush-policy acts at those boundaries; this acts
// between them.
// on whenever the PE goes idle.
// starved only while the last round was starved (the --flush-policy
// adaptive gate), so a PE that is momentarily idle in the middle
// of an RMAT run does not turn full buffers into partial ones.
enum { IDLE_FLUSH_OFF = 0, IDLE_FLUSH_ON = 1, IDLE_FLUSH_STARVED = 2 };
// starved by default: it is a 1.09x speedup on the one-node mesh and 1.12x on
// two-node RMAT, and within the harness's own position bias everywhere else,
// over 20 repetitions with a repeated-baseline control. Ungated (`on`) it is
// 1.12-1.16x slower on the uniform graph, where it doubles the message count
// by turning full buffers into partial ones. See design/step7-idle-flush.md.
int idle_flush_policy = IDLE_FLUSH_STARVED;
// --idle-flush-interval auto|<us> (step 8e): at least this long between two
// idle flushes of a PE that sent something; 0 lets every idle scheduler pass
// flush, as before. See HTram::setIdleFlushInterval. Once the hold bitmap
// made a flush cheap, idle PEs flushed 4x as often and road-usa at 2 nodes
// ran 2x slower (1.73 -> 3.48 s, job 20821099). auto (-1, the default) is
// 30 us at average degree 8 or more and 100 us below, the best of 0/10/30/100
// for each class (job 20821256-57): rmat25 at 8 nodes 0.212 s at 30 against
// 0.259 at 100; mesh24 0.441 s at 100 against 0.547 at 30.
double idle_flush_interval_us = -1.0;
static double idle_flush_interval_seconds() {
if (idle_flush_interval_us >= 0.0)
return idle_flush_interval_us * 1e-6;
return num_global_edges >= 8L * V ? 30e-6 : 100e-6;
}
// --bucket-policy fixed|adaptive, --bucket-target <buckets>. The histogram's
// bucket width is set once from |V| (log V, or sqrt V for the mesh) or by
// --bucket-width. Step 7.3's rerun of step 6's width sweep, on top of the 7.1
// flush policy, found that rule too fine on every graph class: 4x coarser is
// a 1.22x speedup on the mesh and 16x coarser is 1.39x on RMAT, while 1/4 of
// the rule is 1.2-1.6x slower on all three. Speedups here and in the design
// notes are baseline/variant, so above 1 is faster.
// See design/step7-bucketing.md.
//
// adaptive each round, Main measures the band holding the middle 90% of
// the reduced histogram's mass. When that band spans at least
// 2 * target buckets it merges every k = band / target adjacent
// buckets into one, on every PE and in htram's holds. Merging by
// an integer factor is exact -- floor(floor(x) / k) is
// floor(x / k) -- so no in-flight update is ever counted in one
// bucket and uncounted from another. Coarsening only: the finer
// resolution cannot be recovered, because the histogram counts
// updates that are in flight and no PE holds them.
//
// adaptive with target 8 is the default: a 1.14x speedup on the mesh and 1.11x
// on RMAT at 2^20 on one node, against a same-configuration control at 1.03x
// and 0.95x, and never measurably slower at 2^20 or 2^22 on one or two nodes.
// fixed is the pre-7.3 behaviour and what every step 6, 7.1 and 7.2 number
// used. --combine hold falls back to fixed unless adaptive is asked for.
enum { BUCKET_FIXED = 0, BUCKET_ADAPTIVE = 1 };
int bucket_policy = BUCKET_ADAPTIVE;
int bucket_target = 8;
bool bucket_policy_given = false; // Main only: set by --bucket-policy
// --two-tier-per-pe <n>, --two-tier-absolute <n>. A round holding fewer than
// this many updates in its reduced window is treated as too small to describe
// a distribution: both admission percentiles go to 0.9999 and coarsening is
// skipped. The rule has always been N * 100 with N the total PE count, which
// makes the same graph on more PEs enter the branch at a proportionally larger
// population -- a scale dependence nobody chose. --two-tier-absolute pins the
// count instead, so the two can be told apart. Negative keeps the per-PE rule.
int two_tier_per_pe = 100;
long two_tier_absolute = -1;
// --coarsen-clamped block|allow|strict. The clamp bucket is the one bucket a
// merge cannot place: its contents mean "at or past the top of the range", not
// an index, so a merge sends them somewhere no retirement will look. block is
// the shipped rule -- refuse while the reduced clamp count is positive -- and
// it reads that count as of the previous contribution, which leaves a gap.
// strict closes the gap by refusing for the rest of the run once any update
// has been charged there. allow removes the guard, for measuring what it costs.
enum { CLAMP_BLOCK = 0, CLAMP_ALLOW = 1, CLAMP_STRICT = 2 };
int coarsen_clamp_policy = CLAMP_BLOCK;
// --window-follow off|on. The reduced window starts at the lowest bucket last
// seen occupied and never moves to anything it cannot see, so work that jumps
// past its right edge leaves it stranded: every round from then on admits
// everything, which is safe but is no admission control at all. on slides the
// window up by one width whenever it is empty and work is outstanding, which
// finds the frontier in at most HISTO_BUCKET_COUNT / width rounds.
int window_follow = 0;
// --timeout <seconds>: abandon a run that has not converged. 0 disables it.
// There is deliberately no default: a truncated run is a failed run, and the
// old behaviour was to give up after 30 s and print the partial distances with
// the same banner and the same exit status as a converged run.
double timeout_seconds = 0.0;
// --bucket-width <w>: distance units per histogram bucket, replacing whichever
// rule --bucket-width-rule selects. 0 keeps the derived rule, so this is inert
// unless passed. It is what makes H1 of step 6 an A/B rather than a rebuild.
double bucket_width_override = 0.0;
// --bucket-width-rule logv|weight. Which rule derives the width when
// --bucket-width does not replace it wholesale.
//
// logv is what every measurement before this was taken with: log(V) for the
// file and random-graph modes, sqrt(V) for the generated mesh. It reads
// nothing about the distances being bucketed, and 7.6b priced that. The
// histogram spans 2048 * width, so a graph is representable only while the
// derived width exceeds max_distance / 2048, and the three campaign graphs
// that fail that test -- road-ny by 50x, mesh22 by 16x, mesh20 by 9x -- are
// exactly the three where pinning the width was worth anything. See
// design/step76-controller.md.
//
// weight spans the histogram over 2048 maximum-weight edges instead: the width
// is the heaviest edge in the graph, which is a distance, and is already
// reduced at build time. It is sufficient whenever the deepest branch of the
// shortest-path tree averages at most max_weight * 2048 / depth per hop, which
// holds with a factor of four to spare on every graph measured here. It is a
// rule and not a bound -- no cheap bound on the distance range is available:
// max_sum, the one global the solver already computed, is the sum of every
// vertex's heaviest out-edge and so is three orders of magnitude too loose to
// divide by 2048.
//
// **logv is the default, and weight is not, because weight was measured and
// lost.** Job 22061958 ran both over all nine campaign graphs at one node and
// 120 workers: weight is 2.73x faster on road-ny on every source, and 2.4x to
// 2.9x slower on rmat20 and rmat20-s2 on every source, with rmat22 and youtube
// losing on three sources of four. The reason is visible in the bucket scale:
// the graphs weight loses on are exactly the ones whose adaptive coarsening
// already reaches scale 7-10 by itself, so log(V) times that scale is an
// effective width near 112 and weight overrides it with 1000. The graphs it
// wins on are exactly the ones stuck at scale 1. The width rule helps only
// where the controller cannot help itself. See design/step76-width-repair.md.
enum { WIDTH_RULE_LOGV = 0, WIDTH_RULE_WEIGHT = 1 };
int bucket_width_rule = WIDTH_RULE_LOGV;
// --clamp-freeze on|off. Whether the clamp threshold moves when buckets merge.
//
// off is the shipped behaviour: coarsen_buckets raises bucket_limit alongside
// bucket_scale, so an update clamped at creation is not clamped at retirement
// and its decrement lands in an ordinary bucket while its increment sits on
// 2047. That is the one thing that breaks the merge identity
// floor(floor(x/s)/k) == floor(x/(s*k)), and --coarsen-clamped exists only to
// stop merging before it can happen -- which is why a run that once reaches
// the clamp bucket never coarsens again.
//
// on freezes the threshold in distance space and leaves bucket 2047 out of the
// merge, making it an overflow slot rather than an index: increment and
// decrement both land there for the life of the run, and conservation holds
// for every bucket. The price was that coarsening could no longer extend the
// representable range, and that was said here to be tolerable because the
// width rule above would keep the range from needing extension. 7.6d refuted
// that: the width rule is a per-case setting, so on a graph running the
// default rule the frozen range is all the range there is. --range-extend
// below buys it back, using the same freeze -- a fixed overflow index -- as
// the thing that makes a creation-time flag mean something.
bool clamp_freeze = true;
// --range-extend on|off. Raise the clamp threshold when the overflow slot
// starts holding a real share of the live population, so that a graph whose
// distances run past 2048 * width does not spend most of its rounds with
// everything in one bucket and no ordering left to schedule by. Needs the
// frozen clamp underneath it -- the overflow slot has to stay at a fixed index
// for an update's creation-time flag to mean anything -- so it does nothing
// when --clamp-freeze is off. It is also compiled out of a VCOUNT build:
// vcount is indexed by a vertex's current distance and re-derived at each
// change, so a clamp that moves between two of those lookups unbalances it.
// VCOUNT is diagnostic only, and it is the one accounting the bit cannot
// carry.
//
// Default off. It is worth 10x on mesh20 and 3.6x less work on road-ny in
// eight-PE runs on a login node, and zero extensions fire on a graph already
// in range -- but 7.6d made a width rule the default on a prior and then
// measured it losing 2.9x, and the order of those two steps was the mistake.
// This ships as a flag with a campaign arm behind it.
bool range_extend = false;
// --skew-defer off|on. 7.6g. At bucket scale 1, index HISTO_BUCKET_COUNT - 1 is
// two things at once: the overflow slot, and the top real bucket, holding
// distances in [2047, 2048) widths. charge_new_update() flags both at creation,
// so a creator never disagrees with itself about it. A receiver that has not
// yet applied a coarsening its creator already has does: the creator, at scale
// k, charges such an update to 2047 / k and leaves it unflagged, and the
// receiver, still at scale 1, computes 2047 -- the one index coarsen_buckets()
// leaves out of the merge. The update is then either parked in pq_hold[2047],
// where no threshold below 2047 ever releases it, or rejected against
// histogram[2047], leaving +1 at the creator's 2047 / k that no retirement will
// take away. Either way the window pins at floor(2047 / scale), which is where
// every recorded stall pinned.
//
// An unflagged update this PE would charge to 2047 can only have been created
// under a different scale, so it is counted (skew_top_arrivals) always, and
// with this on it waits one broadcast in the same deferral created_beyond_my_
// clamp() uses. Off reproduces the shipped behaviour exactly.
//
// Not the cause of the 7.6g deadlock, and never observed. It was the first
// explanation tested, because it predicts the same pin; but skew_top_arrivals
// was 0 in every one of 160 mesh20/mesh22 runs at 8 x 15 and in every
// single-process sweep, including all the runs that hung. The deadlock was
// queue order (--pq-overflow-last). The counter stays because it is one
// comparison on a path that already computes the bucket, and it is the receipt
// that the race it describes is not happening; the flag stays off.
bool skew_defer = false;
// The overflow slot must hold this share of the live population before the
// clamp is raised. It is the one tuned number in the rule: a single freak edge
// should not cost every bucket half its resolution, and a graph that is
// genuinely out of range passes an eighth within a few rounds.
const double RANGE_EXTEND_SHARE = 0.125;
// --round-delay <ms>: wait this long between the end of one controller round
// and the start of the next. The cycle is otherwise back-to-back -- every chare
// calls contribute_histogram() at the end of current_thresholds() -- so the
// cadence is whatever a reduction plus a broadcast costs, and is not a knob at
// all. 0 keeps that, which is what every measurement so far was taken with.
double round_delay_ms = 0.0;
// --partition-jitter <percent>: how far a PE's share of the vertices may
// deviate from V/N in the uniform mode, which draws its partition sizes at
// random rather than dividing evenly.
//
// This is not a cosmetic option. Mode 1 is the only mode that does it: the mesh
// and RMAT both hand each PE exactly V/N vertices. So a comparison of load
// balance between the uniform graph and a scale-free one is, at the default,
// comparing a deliberately skewed partition against an even one. H3 of step 6
// is that comparison, so it runs every mode at 0. The default stays 20 because
// that is what every measurement to date used and what the golden digests were
// recorded with.
//
// The digests do not move either way. A uniform graph's adjacency is a function
// of the global vertex id alone, so which PE owns a vertex changes who
// generates it and nothing about what is generated -- which is also why the
// gate can require the same digest at ppn 1 and ppn 4.
int partition_jitter_percent = 20;
// --diag <prefix>: write the controller's own time series to
// <prefix>.rounds.csv, and in the ACIC_DIAG build the bucket, vertex-count,
// degree and arrival profiles beside it. Empty disables all of them.
// design/scale-free-diagnosis.md lists what each file holds.
std::string diag_prefix;
// tram constants
// Aggregation buffer size in items, settable with --bufsize (at most htram's
// BUFSIZE). The best size depends on the graph class (step 7.6j, compact
// items, two nodes): rmat25 was fastest at 6144 (1.3x over 2048) while mesh24
// and road-usa were 2x slower there and fastest at 1024 (1.2x and 1.4x over
// 2048). 2048 is the one size that regressed neither class against the
// pre-7.6j build. Under --bufsize-policy fixed it is the size for the whole
// run; under acceptance it is only the size the run starts with.
int buffer_size = 2048;
// --bufsize-policy fixed|acceptance (step 7.6k; acceptance is the default
// from 7.6k on, and an explicit --bufsize without a policy means fixed, as it
// did before). acceptance lets the
// controller choose the size from the share of arriving updates that improve
// a distance. That share separates the graph classes by 5-10x and hardly
// moves with the buffer size itself (mesh24 0.28 at 1024, 2048 and 6144;
// road-usa 0.43; rmat25 0.05; orkut 0.03), so steering by it cannot chase its
// own tail. A high share means long improvement chains, where every item held
// back in a buffer delays the next link and multiplies work (mesh24 created
// 4.7x more updates at 6144 than at 1024). A low share means most arrivals
// are redundant, the delay costs little, and fewer, larger sends pay.
// The size is --bufsize-acc-items / share, clamped to --bufsize-range: 288
// puts mesh24 at about 1024 and rmat25 at about 5700, the two measured optima.
//
// The share cannot pick the size a run starts with. On rmat25 at two nodes
// the first round that has seen enough updates to judge arrives after 40% of
// all the run's updates were created, so a run that starts at 2048 does its
// ramp at 2048 whatever the policy says later (1.38x slower than a fixed
// 6144, job 20767814). The starting size is therefore 256 items per unit of
// average degree, which the solver knows before the first edge moves:
// scale-free inputs have high degree and low acceptance, high-diameter inputs
// low degree and high acceptance, so the two estimates agree on every graph
// measured, and the share only corrects the start.
// The ceiling is 6144 because 8192 was slower than 6144 on rmat25 (7.6j).
enum { BUFSIZE_FIXED = 0, BUFSIZE_ACCEPTANCE = 1 };
int bufsize_policy = BUFSIZE_ACCEPTANCE;
double bufsize_acc_items = 288.0;
int bufsize_min = 512;
int bufsize_max = 6144;
// Multiples of 256 items within --bufsize-range, so nearby estimates name the
// same size and every PE computes the same one.
static int quantize_buffer_size(double want) {
int items = (int)std::min(want, (double)bufsize_max);
items = std::max(256, (items + 128) / 256 * 256);
return std::max(bufsize_min, std::min(bufsize_max, items));
}
// The size every PE starts the solve with.
static int initial_buffer_size() {
if (bufsize_policy != BUFSIZE_ACCEPTANCE)
return buffer_size;
// The exact ratio: average_degree is truncated, which reads a mesh's 3.99
// as 3.
return quantize_buffer_size(256.0 * (double)num_global_edges /
(double)std::max(V, 1L));
}
double flush_timer = 0.01; // milliseconds
bool enable_buffer_flushing =
false; // true = buffer flushes at interval specified by flush_timer
tram_proxy_t tram_proxy;
void fast_exit(void *obj, double time);
/**
* Layout of the msg_stats reduction that print_distances() sends to done().
* The array grew by accretion with its indices open-coded at both ends, as
* `3 + HISTO_BUCKET_COUNT` and friends; naming them is the only way to append
* to it without arithmetic errors that a reduction would never report.
*/
enum {
STAT_WASTED = 0,
STAT_REJECTED,
STAT_VCOUNT, // HISTO_BUCKET_COUNT + 1 entries: one per bucket, plus infinity
STAT_NOTED = STAT_VCOUNT + HISTO_BUCKET_COUNT + 1,
STAT_EDGES,
STAT_DISTANCE_CHANGES,
STAT_GRAPH_BYTES,
STAT_ABSORBED, // updates folded away at the source by --combine
STAT_FOLDED, // updates folded away in a delivered batch, --batch-fold
STAT_SKEW_TOP, // unflagged arrivals charged to the overflow index, 7.6g
STAT_SEND_FILTERED, // updates dropped by --send-filter-bits, 7.6j
STAT_TOKENS, // --lazy-heavy tokens queued, 8d
STAT_TOKENS_STALE, // ... and dropped because their vertex had moved
STAT_HINT_FILTERED, // updates dropped by --hub-hints
STAT_HINTS_PUBLISHED, // hub distances published by --hub-hints
STAT_INSTRUCTIONS, // PAPI builds only
STAT_BATCH_ITEMS, // ACIC_DIAG builds only, from here down
STAT_BATCH_ABSORBABLE,
STAT_HISTO_CREATED, // HISTO_BUCKET_COUNT entries
// Vertices binned by out-degree, and the traffic that lands on each bin.
// H2 is a claim about *where* the redundant updates go, not how many there
// are, and a total cannot answer it.
STAT_DEG_VERTICES = STAT_HISTO_CREATED + HISTO_BUCKET_COUNT,
STAT_DEG_EDGES = STAT_DEG_VERTICES + DEGREE_CLASSES,
STAT_DEG_ARRIVALS = STAT_DEG_EDGES + DEGREE_CLASSES,
STAT_DEG_REJECTS = STAT_DEG_ARRIVALS + DEGREE_CLASSES,
// The same traffic binned by how much of it each vertex received, which is
// the concentration figure: what share of all arrivals lands on what share
// of the vertices. That is what a combining table has to exploit.
STAT_ARR_VERTICES = STAT_DEG_REJECTS + DEGREE_CLASSES,
STAT_ARR_ARRIVALS = STAT_ARR_VERTICES + DEGREE_CLASSES,
// The live histogram at the end, HISTO_BUCKET_COUNT entries. Summed over
// PEs every bucket must be zero: each update is counted where it was created
// and uncounted where it was processed, and --bucket-policy adaptive merges
// buckets while updates are in flight. A nonzero bucket is a merge, or a
// bucket computation, that placed the two ends differently.
STAT_HISTO_LIVE = STAT_ARR_ARRIVALS + DEGREE_CLASSES,
// Largest disagreement, over every round, between htram's admitted counters
// and a recount of what they describe (HTram::admittedDrift), summed over
// PEs. Coarsening moves items across the threshold, so this is the check
// that it moved the counters with them.
STAT_ADMITTED_DRIFT = STAT_HISTO_LIVE + HISTO_BUCKET_COUNT,
// Step 8a: where the work per edge goes as the PE count grows. A vertex's
// edges are relaxed each time process_heap() finds its queued distance still
// current; the last such expansion is at its final distance, so every other
// one is speculation that lost (H5). Heavy edges are those longer than one
// natural bucket width, RIKEN's light/heavy cut (H8).
STAT_EXPANSIONS, // generate_updates() calls
STAT_HEAVY_CREATED, // updates created over heavy edges
STAT_REACHED_EXPANDABLE,// reached vertices with an edge: the least expansions
STAT_REACHED_EDGES, // their edges: the least updates any run creates
STAT_REACHED_HEAVY, // of which heavy
STAT_ARRIVAL_SETTLED, // arrivals at a vertex below the frontier, so final
// Expansions by how far the vertex was above the frontier the PE last heard
// of, in natural bucket widths (0, 1, 2-3, 4-7, ...), and of those the ones
// at the vertex's final distance.
STAT_LEAD_EXPANSIONS,
STAT_LEAD_FINAL = STAT_LEAD_EXPANSIONS + LEAD_CLASSES,
// Arrivals at final vertices by the target's degree class: what a filter
// on settled hubs could remove at the sender.
STAT_SETTLED_DEG = STAT_LEAD_FINAL + LEAD_CLASSES,
// ACIC_COMM_SHARE builds only: TSC ticks summed over PEs.
STAT_WORK_TSC = STAT_SETTLED_DEG + DEGREE_CLASSES,
// inside process_heap() or the delivery callback
STAT_WORK_SEND_TSC, // of which htram sends
STAT_SEND_TSC, // every htram send in the window
STAT_WINDOW_TSC, // each PE's window, start_papi() to print_distances()
STAT_WINDOW_US, // the same windows in microseconds, to convert ticks
STAT_IDLE_TSC, // scheduler idle, less the work idle callbacks did
STAT_SAME_PE_CHANGES,
STAT_CROSS_PE_CHANGES,
STAT_COST_METRICS,
STAT_END = STAT_COST_METRICS + work_cost::COUNT
};
#if defined(ACIC_DIAG) || defined(ACIC_COMM_SHARE) || defined(ACIC_WORK_COST)
const int stat_count = STAT_END;
#elif defined(PAPI)
const int stat_count = STAT_INSTRUCTIONS + 1;
#else
const int stat_count = STAT_INSTRUCTIONS;
#endif
void start_reductions(void *obj, double time) { arr.contribute_histogram(0); }
// --pq-overflow-last off|on. 7.6g's second candidate. process_heap() stops at
// the first queue top whose bucket is above the heap threshold, which is only
// correct if bucket never decreases along the queue's order. Ordered by
// distance alone it can: at bucket scale 1, charge_new_update() flags the
// in-range slice [2047, 2048) widths as overflow, so such an update keeps
// bucket 2047 for life, while an update created after a coarsening by k with a
// slightly larger distance gets floor(2047 / k). If the flagged one was admitted
// to the queue while the threshold was 2047 and the threshold then fell with the
// coarsening, it sits on top, above the threshold, and every admissible update
// behind it waits -- at exactly floor(2047 / scale), where the stalls pinned.
// on sorts every flagged update after every unflagged one, which makes bucket
// monotone along the queue again. Read once at startup, so the heap's order is
// fixed for the life of the run.
//
// Default on. On Anvil, mesh22 at the recorded configuration (8 x 15, logv,
// frozen clamp, rescue disabled) hung 12 of 80 runs with it off and 0 of 80
// with it on, every digest correct, and was not slower where both finished
// (on faster in 49 of 68 pairs, median 1.02x). Every stall off pinned the window
// at floor(2047 / scale) with a bucket-2047 update on top of some PE's queue
// and admissible updates behind it. See design/step76-default-deadlock.md.
bool pq_overflow_last = true;
// The order is "flagged after unflagged, then by distance" when
// pq_overflow_last is on, and "by distance" when it is off. Both are one
// unsigned comparison of a key that puts the overflow flag above every
// distance bit: distances are non-negative, so they fit below bit 63. This is
// the same strict order the two-branch comparison defined, so the heap makes
// the same moves; it only stops paying for the branches on every sift step.
// The mask is taken from the readonly when the heap is built, which is after
// the readonlies have arrived.
struct ComparePairs {
unsigned long flag_mask = pq_overflow_last ? ~0UL : 0UL;
unsigned long key(const Update &u) const {
return (unsigned long)u.distance |
(((unsigned long)u.dest_vertex << 1) & (1UL << 63) & flag_mask);
}
bool operator()(const Update &lhs, const Update &rhs) const {
return key(lhs) > key(rhs); // '>' for min heap
}
};
struct ProcessDistanceKey {
long operator()(const Update &u) const { return u.distance; }
};
struct histoInstance {
public:
int fnz;
int width;
int *reducedValues;
};
class histogramSequence {
private:
int maxBuckets;
std::vector<histoInstance> histos;
public:
histogramSequence(int _maxBuckets) {
maxBuckets = _maxBuckets;
histoInstance h;
h.fnz = 0;
h.width = 10;
h.reducedValues = new int[10];
histos.push_back(h);
}
void insert(int fnz, int width, long *histo) {
histoInstance h;
h.fnz = fnz;
h.width = width;
h.reducedValues = new int[width];
for (int i = 0; i < width; i++)
h.reducedValues[i] = histo[i];
histos.push_back(h);
}
void putout() {
// using cout instead of ckout to avoid buffer overflow. (should be output
// to a file)
std::ofstream out_file;
out_file.open("histos.txt");
for (int i = 0; i < histos.size(); i++) {
for (int j = 0; j < histos[i].fnz; j++)
out_file << "0 ";
for (int j = 0; j < histos[i].width; j++)
out_file << histos[i].reducedValues[j] << " ";
for (int j = 0; j < (maxBuckets - histos[i].fnz - histos[i].width); j++)
out_file << "0 ";
out_file << endl;
}
}
};
/**
* One row of the controller's own time series, kept by Main and written out at
* the end by --diag. Everything here is already computed inside
* reduce_histogram(); recording it costs a push_back on PE 0 per round and
* nothing at all on the worker path, which is what makes it safe to leave in
* the timed build.
*
* `occupied` and `span` are the measurement H1 turns on. The reduction only
* carries a window of histo_reduction_width buckets, so both describe the
* window rather than the whole 2048-bucket range -- but the window is also
* exactly what the percentile cut has to work with, so it is the right
* denominator for asking whether the controller has any resolution to use.
*/
// Why a round did or did not merge buckets. Item 2 of the step 7.5 next-work
// list asks for this directly: "record why each round can or cannot coarsen".
enum {
COARSEN_MERGED = 0, // merged, by the factor in coarsen_k
COARSEN_POLICY_OFF = 1, // --bucket-policy fixed
COARSEN_TWO_TIER = 2, // too little in flight to describe a distribution
COARSEN_EMPTY = 3, // nothing live in the reduced window
COARSEN_CLAMP_LIVE = 4, // updates charged to the clamp bucket right now
COARSEN_CLAMP_SEEN = 5, // --coarsen-clamped strict: some were, earlier
COARSEN_NO_BAND = 6, // the middle 90% of the mass has no two ends
COARSEN_BAND_NARROW = 7, // band < 2 * target: nothing to gain
COARSEN_TOP_UNREACHABLE = 8, // the merged window would not reach the top
COARSEN_NOT_ASKED = 9, // the round ended before the question came up
COARSEN_EXTEND = 10 // merged to raise the clamp, not to narrow the band
};
struct RoundRecord {
double t; // seconds since compute_begin
long histogram_sum;
int window_first; // bucket index the reduced window starts at
int first_nonzero; // the frontier: lowest bucket still holding work
int occupied; // buckets inside the window holding a positive count
int span; // last occupied bucket - first occupied + 1, or 0
int heap_threshold;
int tram_threshold;
// Whether this round took the histogram_sum <= N*100 branch, which abandons
// the configured percentiles for 0.9999 -- that is, admits everything. The
// plan calls this the two-tier hack; knowing which round it fires on is the
// difference between a tail that is cadence-bound and one that is bound by a
// controller that has stopped controlling.
int two_tier;
// Whether this round told the chares that too little is in flight to fill
// the aggregation buffers, which is what --flush-policy adaptive acts on.
int starved;
// Buckets merged into one since the start, under --bucket-policy adaptive.
// Bucket indices in this row are in units of that many original buckets.
int bucket_scale;
// Why this round did or did not merge buckets, and by what factor if it did.
int coarsen_reason;
int coarsen_k;
// What is sitting in the overflow slot, and how many arrived there this
// round. --range-extend reads the second: the first does not fall when the
// clamp rises, because a flagged update retires from the overflow slot
// whatever the clamp becomes.
long clamped;
long clamped_arrivals;
long updates_created;
long updates_processed;
long updates_noted;
long distance_changes;
long done_vertices;
// Items per aggregation buffer the chares ran this round with.
int buffer_size;
long active_pes;
double round_seconds;
double slack_widths, slack_ratio, slack_idle;
int slack_action;
};
class Main : public CBase_Main {
private:
long start_vertex;
long *partition_index;
double start_time;
DistanceDigest parallel_digest; // this source's, kept across --certify
double certify_begin = 0.0;
// Every timer below is -1 until something assigns it, so an unmeasured
// phase reports -1 rather than a plausible zero. Before this, read_time was
// an uninitialized double that only MODE_CSV and modes 1-2 ever wrote, so
// MODE_RMAT and MODE_GAPBS -- the whole benchmark campaign -- printed
// whatever the allocation happened to hold. It read 0.0, which is why the
// 7.5 note could say the field was unassigned but not that it was wrong.
double read_time = -1.0; // input only, where the mode can separate it
double index_time = -1.0; // MODE_GAPBS: header and offsets, read on PE 0
double setup_time = -1.0; // start_time -> solve start: input + build
double stats_time = -1.0; // solve end -> total: the statistics reduction
double total_time = -1.0;
// The heaviest edge in the graph, reduced at build time. The bucket width
// is derived from it, so a run that reports one reports the other.
cost graph_max_weight = 0;
long max_index;
int threshold_change_counter;
int previous_threshold;
int reduction_counts = 0;
int no_incoming = 0;
std::vector<double> reduction_times;
long round_active_pes = 0;
LiveSlack live_slack;
std::vector<RoundRecord> rounds; // --diag; see RoundRecord
bool first_qd_done = false;
bool second_qd_done = false;
int activeBucketMax = 10;
int current_phase = 0; // 0=initial, 1=bfs, 2=converged_bfs
int last_first_nonzero = 0;
int bucket_scale = 1; // --bucket-policy adaptive: original buckets per bucket
int coarsenings = 0;
long previous_updates_created = 0;
long previous_updates_processed = 0;
// Consecutive rounds in which no update was created or retired anywhere.
// Reported on a geometric schedule: a stalled run turns rounds over as fast
// as the reduction allows, so a fixed period would bury the log.
long stall_rounds = 0;
long stall_report_at = 256;
long stall_rescues = 0;
// Consecutive no-progress reductions before the controller admits every
// bucket to break a stall the window cannot describe; 0 disables it. The
// stall report fires at 256, so this is deliberately far earlier: a global
// standstill with live work outstanding is already pathological. Not 1,
// because updates in flight are created but not yet retired, so a run under
// heavy buffering can legitimately hold both counters still for a few
// rounds.
long stall_rescue_rounds = 32;
int stall_reports = 0;
// Rounds whose reduced window held no live update while work was still
// outstanding, i.e. the frontier had moved past the window's right edge and
// the controller could not see it. Such a round has to admit everything to
// stay safe, so it is a round with no admission control at all. One
// comparison per round to count, and the count is the difference between a
// controller that is steering and one that is only along for the ride.
long rounds_window_empty = 0;
long max_above_window = 0;
int range_extensions = 0; // --range-extend: times the clamp was raised
long last_clamped_created = 0; // overflow arrivals as of the previous round
long last_updates_created_seen = 0; // creations as of the previous round
long extend_ready_at = 0; // no extension before this round; see below
long windows_slid = 0; // --window-follow on: windows advanced past empty
// Consecutive rounds whose reduced window claimed more live updates than
// exist. A reduction is not a global instant -- each PE contributes its own
// state when the round reaches it -- so one such round can be an artifact of
// that skew and is not worth a word. A real accounting error does not go
// away, hence the run of rounds before anything is said.
int above_negative_rounds = 0;
bool conservation_warned = false;
// Whether the reduced clamp count has ever been positive. --coarsen-clamped
// strict refuses to merge from then on: the live count is read as of the
// previous contribution, and a chare keeps creating updates between
// contributing and receiving the broadcast that carries the merge, so a
// count that is zero now does not mean none will be charged before the merge
// lands. Monotone, so the answer cannot flap.
bool clamp_ever_live = false;
long previous_distance_changes = 0;
// --bufsize-policy acceptance: the size the chares were last told, the
// counters at the start of the sample being gathered, the smoothed share,
// and how often the size changed.
int current_buffer_size = 0;
long acc_noted_mark = 0;
long acc_changes_mark = 0;
double smoothed_acceptance = -1.0;
int buffer_size_changes = 0;
std::vector<long> sources;
std::string source_spec, base_diag_prefix;
std::vector<unsigned long long> previous_tram_stats;
double tram_percentile = 0.01;
double heap_percentile = 0.01;
#ifdef PRINT_HISTO
histogramSequence *histoSeq;
#endif
public:
double compute_begin;
double compute_time = -1.0; // set on convergence, or by the timeout handler
bool run_truncated = false; // set by fast_exit; forces a nonzero exit
size_t source_epoch = 0;
bool source_running = false;
int control_generation = 0; // --control node: rounds broadcast so far
/**
* Read in graph from csv (currently sequential)
*/
Main(CkArgMsg *m) {
N = CkNumPes();
// Separate option flags from the positional arguments so flags may appear
// anywhere on the command line.
std::vector<std::string> args;
bool bufsize_given = false, bufsize_policy_given = false;
for (int i = 1; i < m->argc; i++) {
if (m->argv[i] == NULL)
continue;
std::string arg = m->argv[i];
if (arg == "--verify")