-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmodel.tex
1787 lines (1670 loc) · 76.6 KB
/
model.tex
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
% Due Date: 6/11/14
\chapter{The Legion Programming Model}
\label{chapter:model}
In this chapter we outline the design of the
Legion programming model. We describe the
important features of Legion as well as
their interactions to create a composable programming
infrastructure. We emphasize that the Legion
programming model is general enough to be
implemented in most modern programming languages
and is not tied to any particular language.
In this chapter we focus solely on the features
of the programming model and do not describe
how they are implemented. Our discussion of
the implementation of the Legion runtime begins
in Chapter~\ref{chapter:arch}. We start with
a motivating example to illustrate some of the
features of Legion, and how they address many
of the problems encountered when writing code
for distributed heterogeneous architectures.
\section{Motivating Example: Circuit Simulation}
\label{sec:circuit}
To make our introduction of the features of
Legion concrete, we use a circuit simulation as
a running example. Listing~\ref{lst:code_ex}
shows pseudo-code for an electrical circuit simulation
written in a C-like language. The circuit simulation
represents the circuit as a graph consisting of nodes
with different voltage and edges representing various
circuit components. At each time step the simulation
calculates currents, distributes charge between different
nodes, and updates the voltage at each node. We provide
a brief overview of the code here. The details of the
pertinent Legion abstractions are covered in the
following sections.
\lstset{
captionpos=b,
language=C++,
basicstyle=\scriptsize,
numbers=left,
numberstyle=\tiny,
columns=fullflexible,
stepnumber=1,
escapechar=\#,
keepspaces=true,
literate={<}{{$\langle$}}1 {>}{{$\rangle$}}1,
morekeywords={region,coloring,partition,disjoint,aliased,tunable,future,predicate,spawn},
deletekeywords=float,
}
\begin{lstlisting}[float,floatplacement=H,label={lst:code_ex},caption={Circuit simulation}]
struct Node { float voltage, new_charge, capacitance; };
struct Wire<rn> { Node@rn in_node, out_node; float current, ... ; };
struct Circuit { region r_all_nodes; /* contains all nodes for the circuit */
region r_all_wires; /* contains all circuit wires */ };
struct CircuitPiece {
region rn_pvt, rn_shr, rn_ghost; /* private, shared, ghost node regions */
region rw_pvt; /* private wires region */ };
void simulate_circuit(Circuit c, float dt) : RWE(c.r_all_nodes, c.r_all_wires)
{
// The construction of the colorings is not shown. The colorings wire_owner_map,
tunable int num_pieces;
// node_owner_map, and node_neighbor_map have num_pieces _PIECES colors
// 0..num_pieces #$-$# 1. The coloring node_sharing map has two colors 0 and 1.
//
// Partition of wires into num_pieces pieces
partition<disjoint> p_wires = c.r_all_wires.partition(wire_owner_map);
// Partition nodes into two parts for all-private vs. all-shared
partition<disjoint> p_nodes_pvs = c.r_all_nodes.partition(node_sharing map);
// Partition all-private into num_pieces disjoint circuit pieces
partition<disjoint> p_pvt_nodes = p_nodes_pvs[0].partition(node_owner_map);
// Partition all-shared into num_pieces disjoint circuit pieces
partition<disjoint> p_shr_nodes = p_nodes_pvs[1].partition(node_owner_map);
// Partition all-shared into num_pieces ghost regions, which may be aliased
partition<aliased> p_ghost_nodes = p_nodes_pvs[1].partition(node_neighbor_map);
CircuitPiece pieces[num_pieces];
for(i = 0; i #$<$# num_pieces; i++)
pieces[i] = { rn_pvt: p_pvt_nodes[i], rn_shr: p_shr_nodes[i],
rn_ghost: p_ghost_nodes[i], rw_pvt: p_wires[i] };
for (t = 0; t #$<$# TIME_STEPS; t++) {
for (i = 0; i #$<$# num_pieces; i++) calc_new_currents(pieces[i]);
for (i = 0; i #$<$# num_pieces; i++) distribute_charge(pieces[i], dt);
for (i = 0; i #$<$# num_pieces; i++) update_voltages(pieces[i]);
}
}
// ROE = Read-Only-Exclusive
void calc_new_currents(CircuitPiece piece):
RWE(piece.rw_pvt), ROE(piece.rn_pvt, piece.rn_shr, piece.rn_ghost) {
foreach(w : piece.rw_pvt)
w#$\rightarrow$#current = (w#$\rightarrow$#in_node#$\rightarrow$#voltage - w#$\rightarrow$#out_node#$\rightarrow$#voltage) / w#$\rightarrow$#resistance;
}
// RdA = Reduce-Atomic
void distribute_charge(CircuitPiece piece, float dt):
ROE(piece.rw_pvt), RdA(piece.rn_pvt, piece.rn_shr, piece.rn_ghost) {
foreach(w : piece.rw_pvt) {
w#$\rightarrow$#in_node#$\rightarrow$#new_charge += -dt * w#$\rightarrow$#current;
w#$\rightarrow$#out_node#$\rightarrow$#new_charge += dt * w#$\rightarrow$#current;
}
}
void update_voltages(CircuitPiece piece): RWE(piece.rn_pvt, piece.rn_shr) {
foreach(n : piece.rn_pvt, piece.rn_shr) {
n#$\rightarrow$#voltage += n#$\rightarrow$#new_charge / n#$\rightarrow$#capacitance;
n#$\rightarrow$#new_charge = 0;
}
}
\end{lstlisting}
\begin{figure}[t]
\centering
\subfloat[Node region tree.]{
\label{sfig:part_fig:tree}
%\begin{tikzpicture}[scale=0.8]
%\partitiontree
%\end{tikzpicture}
\includegraphics[scale=0.60]{figs/CircuitPartition.pdf}
}
\subfloat[$p\_nodes\_pvs$]{
\label{sfig:part_fig:pvs}
\includegraphics[scale=0.60]{figs/Private_vs_Shared.pdf}
}
\subfloat[$p_i$]{
\label{sfig:part_fig:p_i}
\includegraphics[scale=0.60]{figs/Private_Local.pdf}
}
\subfloat[$s_i$]{
\label{sfig:part_fig:s_i}
\includegraphics[scale=0.60]{figs/Shared_Local.pdf}
}
\subfloat[$g_i$]{
\label{sfig:part_fig:g_i}
\includegraphics[scale=0.60]{figs/Ghost_Local.pdf}
}
\caption{Partitions of $r{\_}all{\_}nodes$\label{fig:part_fig}}
\end{figure}
A {\tt Circuit} structure contains handles to the
names of two logical regions: a collection of nodes
and a collection of wires (lines 3-4 of
Listing~\ref{lst:code_ex}).\footnote{Note that all
pointers declare the region to which they point. For
example, the definition of {\tt Wire} (line 2) is
parametrized on the region {\tt rn} to which the
{\tt Node} pointers in fields {\tt in\_nodes}
and {\tt out\_nodes} point.} It is important to note
that logical regions simply describe collections of data
and do not imply any placement or layout in the
memory hierarchy. We discuss how logical regions are
created and used starting in Section~\ref{sec:logicalreg}.
\subsection{Circuit Regions and Partitions}
\label{subsec:circuitregpart}
In order to run the circuit simulation in parallel,
the circuit graph must be partitioned into {\em pieces}
that can be distributed throughout the memory hierarchy.
The number of pieces to create is ultimately chosen
by the mapper based on application and architecture
specific considerations; the mapper's decision is
communicated by a {\em tunable} variable\cite{Sequoia06}.
Tunable values are necessary for abiding by our second
design principle of decoupling specification from mapping.
The choice of how many pieces to create is machine specific
and therefore must be determined as part of the mapping
process.
Traditionally, a graph, such as the one for our circuit
simulation, is only partitioned a single time into
different pieces based on the number of nodes
for the target machine. This single partitioning scheme,
however, fails to capture all of the different relationships
between nodes and wires within the circuit graph. Instead,
our Legion circuit simulation employs a two-level
partitioning scheme to more precisely capture information
about the different sets of the circuit graph necessary
for implementing the circuit simulation.
Figure~\ref{sfig:part_fig:tree} depicts a {\em logical
region tree} data structure that demonstrates how
the nodes for the circuit graph can be better represented
using a multi-level partitioning into logical sub-regions.
Intuitively, the logical region tree data structure
shows how the nodes within the graph our broken down
into subsets represented by logical sub-regions.
Our multi-level partitioning is still based on the
idea of creating circuit pieces, but we describe the
pieces as a union of different subsets. We first partition
all the nodes into two distinct subsets, the
{\em all-private} and {\em all-shared} nodes. These
different sets are depicted in Figure~\ref{sfig:part_fig:pvs}.
The all-private nodes are colored in blue (light-grey)
and have no edges which cross a piece boundary. The
all-shared nodes are colored in red (dark-grey), and
have at least one edge that crosses a piece boundary.
This {\em coloring} of the nodes is reflected in the
code by the {\tt node\_sharing\_map} (line 19) that
is passed to Legion to create a partition of the
logical region containing all nodes. The resulting
partition {\tt p\_nodes\_pvs} has exactly two logical
sub-regions (one for each color), with each node
belonging to the logical sub-region based on how it
was colored. We discuss the details of partitioning
further in Section~\ref{sec:partitioning}.
After our first level of partitioning, we again
partition the two logical sub-regions for the all-private
and all-shared sub-regions again to describe the
subsets for different circuit pieces. Lines 22 and 24
show the calls to partition each of these two sub-regions
into further sub-regions describing the subsets for
different circuit pieces. The coloring for the upper-left
circuit piece for the private and shared nodes is
shown in Figures~\ref{sfig:part_fig:p_i} and
\ref{sfig:part_fig:s_i} respectively.
Another feature of the Legion programming model is
that it allows multiple partitions of logical regions
to be made, creating multiple views onto the same set
of data. For example, in the circuit simulation, we also
need to create an additional partition for describing
the {\em ghost nodes} necessary for each circuit piece.
Figure~\ref{sfig:part_fig:g_i} shows the coloring for
the ghost nodes of the piece in the upper left of the
circuit graph. The ghost node partition is a second
partition of the all-shared logical region. Note that
unlike prior partitions, the ghost node partitioning
may color some nodes with multiple colors because
some nodes share edges with multiple pieces. We therefore
say that this is an {\em aliased} partition since not all
of the sub-regions are disjoint (line 26). The ability
of the Legion programming model to capture disjointness
and aliasing properties of logical regions will be
crucial for constructing a high-performance implementation
of Legion.
Based on the logical sub-regions that we create, we
can now create circuit pieces for different
logical sub-regions. Lines 5-7 of
Listing~\ref{lst:code_ex} declare the type of
a {\tt CircuitPiece} structure. Each circuit piece is
defined by the private, shared, and ghost logical
sub-regions in the logical region tree of all nodes.
A circuit piece also names the set of wires that it
uses. There is a single disjoint partitioning of the
wires logical region into sub-regions based on pieces
on line 17. Lines 29-31 then assemble the different
circuit pieces for our simulation. It is important
to note that the entire assembly of the circuit pieces
is done dynamically, including the coloring and
partitioning operations. The dynamic nature of this
process allows Legion to adapt to both the circuit
topology as well as the underlying machine structure
at runtime.
A final important detail regarding the partitioning
for the circuit simulation is that the actual choice
of the partitioning into pieces was selected by
the application and not Legion. This reflects our
first design principle that we should decouple policy
from mechanism. It is the responsibility of the
application to choose the partitioning algorithm (policy)
for creating the pieces. Legion then provides the
mechanism for capturing the result: specifically the
colorings for creating partitions and logical sub-regions.
Regardless of the chosen partitioning algorithm,
colorings are flexible enough to capture the resulting
partitioning.
\subsection{Circuit Tasks and Operations}
\label{subsec:circuittasks}
Line 9 of Listing~\ref{lst:code_ex} declares
the main function for the circuit simulation.
The {\tt simulate\_circuit} function is an example
of a Legion task. All Legion tasks are required to
name the logical regions they access along with
the {\em privileges} and {\em coherence modes}
with which they access the logical regions. The
privilege and coherence annotation {\tt RWE} on
line 9 specifies that the {\tt simulate\_circuit}
task will access the logical regions {\tt c.r\_all\_nodes}
and {\tt c.r\_all\_wires} with {\em read-write}
privileges and {\em exclusive} coherence. Privileges
specify the kind of side-effects the task can perform
on the logical region. Coherence specifies what
other tasks can do with regions at the same time
as the task using the regions (if anything). We
discuss tasks and privileges in more detail in
Section~\ref{sec:tasks}. Coherence modes are part
of an extension to the Legion programming model
that we cover in Chapter~\ref{chapter:relaxed}.
Lines 32-58 perform the actual circuit simulation
by making three passes over the circuit for each
time step. Each pass loops over the array of
pieces (constructed on lines 29-31 from the
partitions) and asynchronously launches a sub-task
computation to be performed for each piece. There
are no explicit requests for parallel execution
in Legion nor is there
explicit synchronization between the passes.
Which tasks can be run in parallel within a
pass and the required inter-pass dependencies are
determined automatically by the Legion runtime
based on the region access annotations on the
task declarations. The details of how these
dependences are covered can be found in our
description of the Legion execution model
in Section~\ref{subsec:execmodel}.
The tasks launched on lines 33-35 are
{\em sub-tasks} of the main {\tt simulate\_circuit}
task. A sub-task can only access regions (or
sub-regions) that its parent task could access;
furthermore, the sub-task can only have permissions
on a region compatible with the parent's permissions.
For example, the {\tt calc\_new\_currents} task reads
and writes the wires sub-region and reads
the private, shared, and ghost node sub-regions
for its piece (line 40). The requested regions for the
{\tt calc\_new\_currents} task therefore meet
the requirements of the Legion programming model.
We discuss the additional types of privileges available
for tasks to use in Section~\ref{subsec:privileges}.
The implementation of each of the different {\em leaf}
tasks for our simulation are shown on lines 39-58.
Leaf tasks are tasks that perform only computation
and do not perform any additional Legion operations.
In general, tasks are permitted to recursively launch
additional sub-tasks which we discuss in
Section~\ref{subsec:subtasks}. Tasks are also permitted
to perform other kinds of operations such as inline
mapping, explicit copies, and execution fences that
are discussed in Section~\ref{sec:ops}.
%The {\tt distribute\_charge}
%task reads the piece's wires subregion and
%updates all nodes those wires touch.
%However,
%rather than using read/write privilege for the
%nodes (which would serialize these tasks for
%correctness), the task uses reorderable
%reduction operations and atomic rather than
%exclusive access. The final task
%{\tt update\_voltages} writes the shared
%and private nodes for a piece and reads the
%results of the previous task's reductions.
\section{Logical Regions}
\label{sec:logicalreg}
The primary abstraction for describing data in
Legion is {\em logical regions}. The motivation for
logical regions stems directly from our second design
principle regarding decoupling application specification
from mechanism. While many programming systems have
abstractions for computation, few systems have data
models for abstracting the description of program data.
Logical regions decouple the
description of data from any mapping related data decisions:
\begin{itemize}
\item Logical regions have no implied location or
placement in the memory hierarchy.
\item Logical regions have no implied layout of
data in memory such as struct-of-arrays (SOA),
array-of-structs (AOS), hybrid, etc.
\end{itemize}
As an example, recall from the circuit simulation in
Section~\ref{sec:circuit} how logical regions capture the
structure of the circuit graph and the various important
subsets without committing to the placement or layout
of any data. Logical regions, therefore, provide the
crucial abstraction for describing the structure of
program data without constraining data to any
particular target architecture, thus taking the first
step in decoupling the specification of an application
from its mapping. We now detail how logical regions
are created and used.
\subsection{Relational Data Model}
\label{subsec:relations}
To describe the structure of program
data, we require that logical regions be
general enough to support arbitrary data structures.
To meet this need, we drew inspiration from databases
and the work in \cite{Hawkins11} that demonstrated
that {\em relations} are a general abstraction capable
of encoding most complex data structures in use in
real applications. For example, in the circuit simulation,
the graph of circuit elements is easy to encode as two
separate relations: one for describing the
sets of nodes, and another for describing the
sets of wires. Logical regions are therefore
based on relations and have notions of {\em index
spaces} (e.g. sets of rows) and {\em field spaces}
(e.g. columns). We describe each of these properties
in detail in Sections~\ref{subsec:indexspace} and
\ref{subsec:fieldspace} respectively.
Before proceeding, it is important to note that logical
regions in many ways are not full relations in the
traditional database sense. Relations in databases
normally support a full (or nearly full) implementation
of relational algebra including operations such as inner
and outer joins. In Legion, our goal is not to re-implement
a database system. Instead, we are adapting the
relational data model provided by relations to
serve as the basis for describing data in Legion
programs, and supporting a minimal subset of relational
algebra necessary for describing the operations
used to achieve high performance on our target class of
machines. We highlight the important Legion operations
that have natural analogs to database operations where
appropriate.
\subsection{Index Spaces}
\label{subsec:indexspace}
To describe the rows of logical regions we use
{\em index spaces}. Index spaces encapsulate
the keys for logical regions. Currently, Legion
permits two different kinds of index spaces
to be created for describing the sets of
keys: {\em unstructured} and {\em structured}
index spaces. By specifying different kinds of
index spaces, applications can indicate the best
representation for index spaces. Unstructured
index spaces suggest that there is little
structure in the keys, while structured index
spaces are used for representing index spaces
with one or more collections of dense keys
(e.g. N-dimensional Cartesian grids). We now
describe each of these kinds of index spaces
in turn.
Unstructured index spaces capture arbitrary
collections of keys and allow
for the dynamic allocation and freeing of
opaque keys that we call {\em pointers}. Note
that these pointers are significantly different
from traditional pointers in a language such as
C. Pointers in C imply a specific location in
memory where data has been allocated. In Legion,
pointers are simply keys for accessing logical
regions associated with a given index space.
Thus pointers in Legion are portable and can
be used regardless of where a logical region
has been mapped. One similarity with C pointers
is that Legion permits dynamic allocation and the freeing
of pointers in an unstructured index space, much
like how C permits dynamic memory allocation
and freeing. The logical regions in our
circuit simulation use unstructured index spaces
due to the irregular nature of the graph. Both
the logical region containing node data and the
logical region containing wire data are based on
unstructured index spaces and pointers are allocated
in these index spaces as necessary based on
the topology of the graph.
In contrast to unstructured index spaces, structured
index spaces are represented by one or more
Cartesian grids of points. If more than one grid
is specified, they must all be of the same dimension.
We use these kinds of index spaces for describing
applications with more regular structures such
as linear algebra and applications that work on
regular grids like S3D (see Chapter~\ref{chapter:s3d}).
The Legion programming model is also open to extension
in the variety of kinds of index spaces. Presently,
only two kinds of index spaces are supported; however, we envision
having many different kinds of index spaces for
representing different sparsity patterns. For example,
in the case of sparse matrix applications, it
is useful to represent different sparse matrices
in different ways based on the local structures
they contain within them. For these cases we want
Legion to be able to support index spaces that
capture different ways of efficiently representing
keys for different entries in a logical region.
It is important to note that our design of index
spaces strictly adheres to our first design principle
of separating policy from mechanism. Legion
applications can completely control the
kind of index spaces when creating logical regions,
thereby specifying the policy to use for different
kinds of data. Independently, the mechanisms for
implementing each of the various kinds of index
space are completely contained within the Legion
runtime and opaque to the programmer, thereby
allowing Legion to manage the complexity of the
implementation and to incorporate optimizations
for specific target architectures.
\subsection{Field Spaces}
\label{subsec:fieldspace}
To represent the columns (also called fields) of
logical regions, Legion uses {\em field spaces}.
Field spaces encapsulate the set of all fields
available in a logical region. Each field is
defined by a pair of values: a unique name
for the field (usually an integer), and the
type of the field. The type of a field can
be either a plain old data (POD) type (in the
traditional C sense), or a compound type. Legion
users are free to decide the whether fields should
consist of POD base types or compound types with
the understanding that the finest granularity at
which Legion will be able to reason about data
is at the granularity of individual fields.
Field spaces are another example of the decoupling
of policy and mechanism in Legion. Field spaces
allow users to control the policy of what
constitutes a field in a field space, while
Legion provides the mechanisms for how field
spaces are implemented and used. For our circuit
simulation each POD type associated with either a node
or a wire (e.g. charge, voltage, capacitance, etc.)
is made into an individual field in a field space,
thus giving Legion total visibility of all the fields
within both the node and wire logical regions.
One important restriction of field spaces in
Legion is that we require that the maximum number
of fields in a field space be statically upper
bounded. This restriction is necessary for an
efficient Legion implementation, described
in more detail in Section~\ref{subsec:fieldmasks}.
It is important to note that this restriction does
not limit the expressivity of Legion as applications
are free to create an unlimited number of field spaces
(and corresponding logical regions) and therefore the
total number of fields in any application is unbounded.
The restriction only places a cap on the maximum number
of fields in a single field space.
\subsection{Region Trees}
\label{subsec:trees}
Logical regions in Legion are created by taking
the cross product of a field space with an index
space. Each invocation of this cross product generates
a new logical region. Logical regions created in
this way can also be partitioned into logical sub-regions
that we discuss in Section~\ref{sec:partitioning}. We refer
to a logical region and all of its logical sub-regions as
a {\em region tree}. Since each cross-product of a
field space with an index space results in a new
logical region, we assign to each region tree a
{\em tree ID}. Therefore, each logical region can be
uniquely identified by a 3-tuple consisting of
an index space, a field space, and a tree ID. An
alternative design would have been to define
each cross-product of a field space with an
index space as a unique logical region. Ultimately, we
decided that it is beneficial in many cases
to define multiple logical regions based on the
same field space and index space. We will give an
example of this pattern in Chapter~\ref{chapter:s3d}.
The dynamic nature of logical regions is important.
Field spaces, index spaces, and logical regions can
all be created and destroyed at runtime. This gives
applications the freedom to dynamically determine
both the number and properties of logical regions.
By making all operations associated with index spaces,
field spaces, and logical regions dynamic, Legion is well
positioned to react to the dynamism in both software
and hardware described in Section~\ref{subsec:dynamism}.
\section{Partitioning}
\label{sec:partitioning}
When writing code for distributed memory architectures,
one of the most important components of an application
is determining how data is partitioned. Traditionally,
partitioning of data is done to assign different subsets
of data to different nodes. In Legion, partitioning
is designed to be a more general operation, not tied to
an actual distribution of data to specific nodes. Instead,
partitioning in Legion is an operation that is used
to name different subsets of data that will be
used by different computations. Drawing an analogy
to relational database systems, logical sub-regions
are similar to views onto a subset of a relation.
Furthermore, Legion supports hierarchical partitioning,
allowing subsets to be further refined, in order to
describe tiered sets of data. By creating hierarchical
logical sub-regions through partitioning, applications
can precisely scope the set of data used
by different computations.
In keeping with our first design principle, Legion
does not provide any automated partitioning schemes.
Instead it is the responsibility of the application
to determine the best way to partition data for the
computation it intends to perform, thereby specifying
the policy for how to partition data. In conjunction,
Legion provides the mechanism for capturing the result
of the partition in terms of logical regions. For example,
in the circuit simulation, the partitioning of the circuit
graph into pieces could be done by a third party library
such as METIS. The results of the computed partitioning are
then captured by Legion in the form of partition objects
with logical sub-regions.
Creating partitions with logical sub-regions
is done with {\em colorings}. Colorings are objects
that describe an intended partition of an index space.
Technically, a coloring is a map from {\em colors} to sets of
points in an index space. For unstructured index spaces,
colorings are maps from colors to sets of individual pointers,
while for structured index spaces colorings are maps from
colors to one or more Cartesian groups of points. For each
color in a coloring, Legion will create one logical index
sub-space of the parent index space. For every logical
region based on the parent index space, a corresponding
logical sub-region will exist for the newly created index
sub-space. Figure~\ref{fig:part_fig} illustrates this
process for the circuit simulation.
Partitions in Legion are normally used to create multiple
logical sub-regions simultaneously. Legion records two
important properties about the set of logical sub-regions
created in a partition. First, Legion records whether the
index sub-spaces that are created by a partition are
{\em disjoint}. A partitioning is defined to be disjoint if
each entry in the parent index space of the partition is
assigned to at most one of the index sub-spaces. Legion does
permit colorings in which a single point in the parent
index space is assigned to multiple index sub-spaces.
Partitions containing points colored more than once are
called {\em aliased}, since some index sub-spaces
contain the same point\footnote{
More mathematically inclined readers may prefer to think
of colorings as a relation (in the mathematical sense), with
the relation being considered disjoint if it is a function,
and aliased otherwise.}.
The second property of partitions tracked by Legion is
{\em completeness}. If every point in a parent index space
is mapped to at least one of the index sub-spaces, then
the partition is said to be complete, otherwise it is
incomplete\footnote{For mathematically inclined readers,
this property is surjectivity.}. We will see how Legion
benefits from tracking these properties of partitions
in Chapters~\ref{chapter:logical} and \ref{chapter:physical}.
\subsection{Tunable Variables}
\label{subsec:tunablevars}
Another example of separating specification from mapping
in the design of Legion is {\em tunable
variables}. In many cases, partitioning is done based
on the size of the machine and the number of processors
available for different computations, all of which are
aspects of the target machine. There obviously exists a
natural tension with our second design goal that requires
algorithm specification to be decoupled from the mapping.
To handle this decoupling we borrow an idea from the
Sequoia programming model \cite{Sequoia06}, and provide
tunable variables as a feature of Legion.
Tunable variables are assigned a
value at runtime by a Legion mapper. While we defer our
discussion of Legion mapper objects until
Chapter~\ref{chapter:mapping}, for now it is sufficient
to know that Legion mappers can determine the value of
tunables based on both machine and application specific
information. By explicitly describing
a variable as a tunable, applications can decouple the
specification of an application from its mapping,
ultimately allowing mappers to determine optimal values
for tunables. For example, in the circuit simulation, a
tunable variable is used to determine the number of
pieces to direct METIS to compute for the circuit
graph. The mapper that picks this value is likely
to make the determination of the tunable value based
on the number of nodes or number of processors in
the target machine. For example, in our circuit
simulation we use the tunable variable {\tt num\_pieces}
(line 12 in Listing~\ref{lst:code_ex}) to determine
the number of pieces to create when partitioning
the circuit graph. The mapper will likely determine
the value for this tunable variable based on
the number of nodes or processors in the machine.
There are two important details regarding tunable
variables. First, tunable variables are not specific
to partitioning, and can be used for any purpose in
any task. This allows applications to explicitly declare
any variable that should be explicitly chosen by
a mapper as a tunable variable. Second, unlike the
statically-specified tunable variables in Sequoia,
tunable variables in Legion are dynamically determined.
This allows mappers to change the values that it assigns to
tunable variables across different task instances
and react to changes in the hardware or software
as part of assigning specific values to tunable variables.
\subsection{Multiple Partitions}
\label{subsec:multiple}
One of the most important aspects of the Legion
programming model is that it supports multiple
partitions of an index space. Multiple partitions
allow applications to describe many different
views onto the same collection of data. Supporting
multiple partitions is
important because most data in applications is
viewed in different ways depending on the phase
of the computation. For example, in the circuit
simulation, there are shared nodes that have at
least one edge that crosses a piece boundary. In
some cases, these shared nodes need to be accessed
by computations on the piece that owns them, while
in other cases, shared nodes are accessed as
ghost nodes for an adjacent piece. Being able to
capture these multiple views onto the same data
is crucial to capturing the data usage patterns
of real applications.
\subsection{Recursive Partitions}
\label{subsec:recursivepart}
Another important aspect of Legion is that it
supports recursive partitions. Applications can
recursively partition index sub-spaces into further
index sub-spaces, thus creating arbitrarily deep
index trees and corresponding logical region trees.
The ability to create recursive partitions is
important for two reasons. First, recursive partitions
allow applications to partition data in a way that
aligns with deep memory hierarchies, which was
one of the key insights of the Sequoia programming
language \cite{Sequoia06}. Second, supporting
recursive partitioning is necessary for capturing
non-trivial subsets of data. For example, the circuit
simulation uses two levels of partitioning to first
break nodes into all-private and all-shared nodes, and
then recursively partitions those sub-regions to express
the subsets of nodes in each of the pieces. This
allows the logical region tree to capture important
disjointness information about the different subsets
of nodes.
\subsection{Interaction with Allocation}
\label{subsec:partalloc}
Partitioning in Legion is flexible enough to support
two different approaches that we term
{\em allocate-then-partition} and {\em partition-then-allocate}.
The circuit simulation is an example of allocate-then-partition.
The circuit graph is initially allocated in logical regions for
describing the nodes and wires. We then partition these logical
regions into logical sub-regions. The allocate-then-partition
strategy is used for data structures that are constructed up
front and then later need to be partitioned into sub-regions.
Alternatively, the partition-then-allocate strategy can be
used for creating data structures in parallel. Index trees
and corresponding logical region trees can be created in
which no rows are allocated. This is done by using completely
empty colorings. Rows can then be allocated in different index
sub-spaces in parallel, allowing data structures to be created
in logical regions in parallel. When a row is allocated in a
logical sub-region, it is automatically allocated in all of
the ancestor regions of the logical region (in keeping with
the definition of logical regions). The partition-then-allocate
approach is especially useful for constructing data structures
in parallel when the size of the data structure is sufficiently
large that the parent logical region cannot be manifested
in any memory in the target machine.
It is important to note that partition-then-allocate and
allocate-then-partition are not exclusive. Well-structured
Legion applications employ a mixture of strategies to a
create logical region trees. First, partition-then-allocate
is used to load data structures into logical regions in
parallel. Next, new partitions are created of the
existing logical regions to describe alternative views
onto the already allocated data.
\subsection{Region Tree Properties}
\label{subsec:treeprop}
As a result of partitioning, Legion applications can
create arbitrary index space trees and corresponding
logical region trees for describing the structure of
program data. Logical region tree data structures are
the crux of the Legion programming model. By creating
logical region trees through partitioning, Legion
applications can encode important information about
the structure of program data so that it can be
communicated to an implementation of Legion. Specifically,
logical region trees capture the following properties:
\begin{itemize}
\item Locality - entries in logical regions have an
implied locality. Partitioning rows in a logical
region into the same logical sub-region further
indicates that a task is likely to use the same
rows, strengthening the implied locality.
\item Disjointness - partitions capture information about
which logical regions share data. Disjoint
partitions aid Legion in inferring when two
logical regions are independent.
\item Sub-Regions - knowing sub-region relationships
allows Legion to reason about subsets of data.
\item Aliasing - as the complement to disjointness,
logical regions also allow Legion to infer
when two logical regions may potentially be
aliased, either by aliased partitions or by
sub-region relationships.
\end{itemize}
As we will see in the coming chapters, a Legion
implementation can leverage all of the information
encoded in logical region trees to both achieve
high performance, and make developing Legion
applications easier.
Another important aspect of logical region trees
is that they capture these properties of program
data in a way that is independent of the target
machine. Logical region trees are not tied to
the hardware in any way, thereby allowing applications
to decouple the specification of an application
from how it is mapped onto a target architecture.
\section{Motivating Example: Conjugate Gradient}
\label{sec:cgex}
\begin{lstlisting}[float,floatplacement=H,label={lst:cg_ex},caption={Conjugate Gradient Example}]
struct SparseMatrix {
region lr;
partition<disjoint> part;
int num_rows, elmts_per_row;
}
struct Vector {
region lr;
partition<disjoint> part;
int num_elmts;
}
void conjugate_gradient(SparseMatrix a, Vector x) : RWE(a.lr, v.lr) {
tunable int num_pieces;
a.part = a.lr.partition(num_pieces);
x.part = x.lr.partition(num_pieces);
Vector r_old(x.n_elmts), p(x.n_elmts), A_p(x.n_elmts);
// ... create and partition logical regions with num_pieces
// Ap = A * x
spawn<num_pieces> spmv(a.part, x.part, A_p.part);
// r_old = b - Ap
spawn<num_pieces> subtract(b.part, A_p.part, r_old.part);
// initial norm
const double L2Norm0 = spawn<num_pieces> L2norm(r_old.part);
double L2norm = L2Norm0;
// p = r_old
copy(r_old, p);
predicate loop_pred = true;
future r2_old, pAp, alpha, r2_new, beta;
for (int i = 0; i < MAX_ITERS; i++) {
// Ap = A * p
spawn<num_pieces> @loop_pred spmv(A.part, p.part, A_p.part);
// r2 = r' * r
r2_old = spawn<num_pieces><+> @loop_pred dot(r_old.part, r_old.part, r2_old);
// pAp = p' * A * p
pAp = spawn<num_pieces><+> @loop_pred dot(p.part, A_p.part, alpha);
// alpha = r2 / pAp
alpha = spawn @loop_pred divide(r2_old, pAp);
// x = x + alpha * p
spawn<num_pieces> @loop_pred daxpy(x.part, p.part, alpha);
// r_old = r_old - alpha * A_p
spawn<num_pieces> @loop_pred daxpy(r_old.part, A_p.part, -alpha);
r2_new = spawn<num_pieces><+> @loop_pred dot(r_old.part, r_old.part, r2_new);
beta = spawn @loop_pred divide(r2_new, r2_old);
spawn<num_pieces> @loop_pred daxpy(r_old.part, p.part, beta);
future norm = spawn<num_pieces><+> @loop_pred dot(r_old.part, r_old.part, L2norm);
loop_pred = spawn @loop_pred test_convergence(norm, L2norm) : false;
}
}
\end{lstlisting}
Before proceeding with our description of the rest of the programming
model, we pause briefly to introduce another example that will be
useful for understanding some of the features introduced in the coming
sections. Listing~\ref{lst:cg_ex} shows the basic outline of code
for a simple conjugate gradient (CG) solver written in Legion. The code
operates on a sparse matrix $a$ (stored in a logical region) and a vector
$v$ (stored in another logical region). The solver iterates up to a
maximum number of steps to find a vector $x$ such that $ax = v$. Lines
18-26 show the set-up stages of the computation and lines 30-48
comprise the main iterative loop.
To solve the system in parallel, this example partitions the sparse
matrix by rows and the dense vector into pieces (specified by the
tunable variable on line 12) that correspond to the number of rows.
Additional logical regions are created and partitioned isomorphically
for storing intermediate values. For every operation of the CG solver
a parallel task is launched to handle the different subsets of the
computation. While the partitioning scheme for this example is
straightforward, we will see shortly how index space task launches,
futures, predicates, and explicit region-to-region copies are all
necessary for implementing the CG solver efficiently.
\section{Tasks}
\label{sec:tasks}
While logical regions provide the crucial abstraction
in Legion for describing data in a machine independent
way, tasks serve the same purpose for computations.
Legion tasks describe computations on two kinds of
arguments: parameters that are passed by value (much
like traditional functions) and logical regions. By
describing computations as tasks that operate on logical
regions, Legion programs can specify an application
in a way that is independent from any target architecture.
In this section we cover the details of how Legion
applications describe tasks.
\subsection{Privileges}
\label{subsec:privileges}
Unlike most programming models that maintain a heap
abstraction that can be implicitly accessed by any
computation, Legion requires each task be explicit about
the data that it can touch by naming the logical
regions the task will access. Legion tasks name
the regions they access through {\em region requirements}.
When a Legion task is launched, it provides a set of
region requirements.
In addition to naming the logical regions that a task
will access, region requirements also specify the {\em privileges}
with which the task will access each field of
each region. Privileges in Legion are one of {\tt read-only},
{\tt read-write}, or {\tt reduce} (with reductions
parametrized by a reduction operator). Privileges
specify the kinds of side-effects that the task
is permitted to have on the specific fields of the
logical region in a region requirement.
For example, in the circuit simulation, each
{\tt calc\_currents} task requests {\tt read-write}
privileges on several fields of its wire region, but only
requests {\tt read-only} privileges on its node
region. Privileges in Legion form a semi-lattice
shown in Figure~\ref{fig:privlattice}
\footnote{Note that each different kind of reduction
forms its own entry in the semi-lattice of privileges.}.
As we will describe in Section~\ref{subsec:execmodel},
the use of privileges is important for
the discovery of parallelism in a Legion program.
\subsection{Sub-Tasks}
\label{subsec:subtasks}
The model of computation in Legion is a tree of
tasks. A root task is initially executed on some
processor in the system and this task can launch
an arbitrary number of sub-tasks. Each sub-task
can in turn launch its own sub-tasks. There
is no limit to the depth of the task tree in a
Legion program execution. While we note that this approach
is different from the traditional SPMD programming
model for targeting supercomputers, we claim that
SPMD is a special-case of the Legion programming
model and can be easily replicated using existing
features in Legion, as we will describe in
Chapter~\ref{chapter:relaxed}.
\begin{figure}[t]
\centering
\includegraphics[scale=0.7]{figs/PrivilegeLattice}
\caption{Legion Privileges Semi-Lattice\label{fig:privlattice}}
\end{figure}
The Legion programming model requires that sub-tasks
within a Legion application maintain the following
property: sub-tasks are only permitted to
access fields of logical regions with a subset of the
privileges of their parent task.
We call this property the {\em containment property}.
To be more concrete, in order for the containment
property to hold for a sub-task, then all of the
following must be true for each logical region
a sub-task requests privileges on:
\begin{itemize}
\item The parent task must also have privileges
on the specified logical region.
\item For each field, the parent task must also
have privileges on the specified field.
\item The parent task must have privileges that
are a (non-strict) superset of the privileges
requested by the child task as defined by
the privilege semi-lattice in
Figure~\ref{fig:privlattice}.