16 peano_project:
"peano4.api.Project",
17 non_fusable_task_pairs: Optional[List[Tuple[Task, Task]]] =
None,
18) -> List[
"peano4.api.solversteps.Step"]:
21 Task graph compiler which fuses tasks into as few mesh traversals as possible
23 You can use this task graph compiler in your ExaHyPE 2 project through
25 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
26 project.compiler = peano4.api.taskgraph.compiler.FuseSteps.translate
27 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
29 Where peano4.api.taskgraph.compiler.Sequential.translate() maps every
30 task onto a mesh traversal (Observer) of its own, this compiler packs
31 several tasks into one traversal whenever this is safe.
33 ## When can two tasks be fused?
35 Two tasks end up in the same traversal if and only if all of the
36 following constraints hold:
38 1. **Entity order.** Each grid sweep first does something with the
39 vertices, then with the faces, then with the cells (@ref
40 peano4.api.taskgraph.Task.Task.GridEntity, whose values follow exactly this order). A task that
41 works on a face can therefore be followed by a task working on a face
42 or a cell within the same traversal, but a task working on a vertex
43 has to go into the next traversal. In general, a node can be appended
44 to the traversal built up so far if and only if the lowest grid
45 entity it works on is not lower than the highest grid entity the
46 traversal works on so far. Tasks of one dependency layer (see
47 below) are independent of each other, so this constraint does not
49 2. **No conflicting face or vertex data.** A face or a vertex is shared
50 between several cells. If one task of a traversal writes a face or
51 vertex submodel (WRITE or READ_AND_WRITE) while another task of the
52 same traversal reads or writes the very same submodel, a cell might
53 see the submodel before or after a neighbouring cell has already
54 altered it, depending on the traversal order. Such tasks are never
55 fused (@ref peano4.api.taskgraph.compiler.FuseSteps._have_conflicting_data()). Cell data are not shared
56 between cells and hence do not constrain the fusion. TEMPORARY uses
57 are ignored. There is one exception which follows from the entity
58 order: Peano touches every vertex and face of a cell for the first
59 time before it enters the cell, and every vertex of a face before the
60 face. A FACE task which reads or writes face data therefore has
61 finished with a face before any subsequent face or cell task of the
62 same traversal sees this face. The same holds for a VERTEX task and
63 vertex data. A conflict on face (vertex) data hence is ignored if the
64 earlier of the two tasks is a FACE (VERTEX) task. Two FACE tasks with
65 conflicting face data thus can share one traversal, but they never
66 run in parallel (constraint 2 of the algorithm below). Apart from
67 this exception, the constraint is conservative: it relies only on
68 the Task.DataAccess qualifiers and the tasks' grid entities, and does
69 not know that a cell task might for example write the face data only
70 when it touches a face for the last time.
71 3. **No repetition.** Tasks which have to repeat (Task.has_to_repeat)
72 are never fused. They always yield a traversal of their own.
73 4. **No annotation.** The caller can annotate the fusion through
74 non_fusable_task_pairs: two tasks forming such a pair never end up
75 in the same traversal.
76 5. **Global tasks.** A GLOBAL task (@ref peano4.api.taskgraph.Task.Task.GridEntity)
77 does not touch any mesh entity. It does not take part in the
78 algorithm below. Instead, it is appended to the traversal holding
79 its last dependency, even if this traversal repeats. A GLOBAL task
80 without dependencies goes into the very last traversal.
83<div style="background-color: #fcc ; padding: 10px; border: 1px solid green;">
84A task does not tell the compiler if its action sets plug into touch-first or touch-last
85events. The compiler therefore assumes that a VERTEX (FACE) task works on its vertex
86(face) data within touchVertexFirstTime() (touchFaceFirstTime()), i.e. before any
87adjacent face or cell is entered. A VERTEX or FACE task which writes its data in
88touchVertexLastTime() or touchFaceLastTime() violates this assumption, and the
89compiler might fuse it with a subsequent task that then reads stale data. Conversely,
90a CELL task A might only write face data in touchFaceLastTime(), while its successor
91CELL task B reads this face data in touchCellFirstTime(). We could fuse A and B, but
92this option is hidden from the task compiler.
98 1. Split the graph into layers by dependency depth (a task without
99 dependencies has depth 0, every other task 1 + the deepest of its
100 dependencies - the same notion of depth Graph.visualise() uses).
101 Tasks within one layer can never depend on one another, i.e. they
102 can run in parallel. Within a layer, tasks keep the order in which
103 Graph.ordered_tasks() lists them.
104 2. The tasks of one layer are combined into intermediate nodes. A
105 repeating task forms a node of its own, placed in front of the
106 layer's remaining nodes. The remaining tasks are added one by one to
107 the layer's current node unless they have conflicting data with it
108 or form a non-fusable pair with one of its tasks (constraint 4). In
109 this case, they start a new node. Different to constraint 2, cell
110 data counts as conflicting data here, too: Tasks of one node may be
111 invoked concurrently on the same cell, i.e. they must not write
112 cell data which another task of the node reads or writes. A node
113 might work on several grid entities. A node is parallel if it holds
114 more than one task and each of these tasks holds exactly one action
115 set (@ref peano4.api.taskgraph.compiler.FuseSteps._is_parallel_node()).
116 The action sets of a parallel node are labelled as parallel (@ref
117 peano4.api.solversteps.ActionSet.ActionSet.parallel), i.e. they may
118 be invoked concurrently. All other action sets are labelled as not
119 parallel. A task with several action sets never is part of a
120 parallel node: Its action sets have to be invoked one after the
121 other, and the parallel flags of individual action sets can not
122 express that a sequence of action sets runs in parallel to another
124 3. The resulting sequence of nodes is fused greedily into traversals:
125 a node is appended to the current traversal if constraints 1 to 4
126 hold for the node and all tasks already in the traversal. Otherwise,
127 it starts a new traversal. The fusion never reorders nodes and never
132 The illustrations below are generated by
133 src/peano4/api/taskgraph/compiler/fuse-steps-illustrations.py. Each
134 shows the input task graph on top and the compiled sequence of
135 traversals underneath.
139 A chain of tasks working on a vertex, a face, a cell, a vertex and a
140 cell. The vertex, face and cell tasks are fused into one traversal.
141 The second vertex task has to start a new traversal, which also takes
142 the subsequent cell task.
144 @image html fuse-steps-entity-order.png
148 Two independent vertex tasks form one layer and hence one parallel
149 node. They are fused with the two independent cell tasks of the
150 subsequent layer. Action sets which may run in parallel share a number
151 and are told apart by letters, e.g. 1.a and 1.b.
153 @image html fuse-steps-parallel.png
155 ### Conflicting face data
157 The time step of an ExaHyPE 2 Finite Volume solver: the reconstruction
158 reads the face data that the projection eventually writes. Therefore,
159 the projection cannot be fused with the reconstruction and the update,
160 and starts a traversal of its own.
162 @image html fuse-steps-data-conflict.png
164 ### Face task before cell task
166 A FACE task sets boundary data on the faces, which the subsequent
167 reconstruction reads. As the face task is done with a face before
168 either adjacent cell is entered, all three tasks share one traversal
169 even though the first two share face data.
171 @image html fuse-steps-face-before-cell.png
173 ## Constructing the fused step
175 A fused step's name concatenates the names of its tasks, prefixed with
176 identifier. Its action sets are the tasks' action sets one after the
177 other, so the evaluation order of the tasks is preserved. We do not use
178 peano4.api.solversteps.Step.add_action_set() for this, as it reorders
179 the action sets according to their descend_invocation_order: a
180 reconstruction with a higher descend_invocation_order would then
181 overtake the update of the subsequent task. The descend_invocation_order
182 is applied (stably) only among the action sets of one task.
184 The generated observer spawns consecutive action sets which are labelled
185 as parallel as one task group
186 (@ref peano4.api.solversteps.SynchroniseActionSets.get_task_groups()).
187 If two parallel nodes follow each other directly within a step, their
188 groups would merge. We therefore insert a
189 @ref peano4.api.solversteps.SynchroniseActionSets.SynchroniseActionSets
190 between them - @ref peano4.api.taskgraph.compiler.FuseSteps._create_step().
192 A submodel used by several tasks of a fused step is declared once on
193 the step. Its qualifier is merged from the tasks' qualifiers: TEMPORARY
194 uses are ignored, the step loads the submodel if the first remaining
195 use reads it, and stores it if the last remaining use writes it -
196 @ref peano4.api.taskgraph.compiler.FuseSteps._merge_data_access().
198 propagate_use_statements() is not invoked here. It is a postprocessing
199 step called outside of the translation.
201 @param graph: peano4.api.taskgraph.Graph
202 The task graph to compile. Assumed to be acyclic.
204 @param identifier: str
205 Identifies which of the surrounding project's algorithmic phases this
206 graph belongs to (e.g. "SetupSimulation", "TimeStep", "Plot") and
207 prefixes every generated Observer's name.
209 @param peano_project: peano4.api.Project
210 Not used by this compiler. @ref peano4.api.taskgraph.compiler
212 @param non_fusable_task_pairs: [(peano4.api.taskgraph.Task, peano4.api.taskgraph.Task)]
213 Pairs of tasks of graph which must not end up in the same traversal.
214 The order within a pair does not matter. Defaults to none.
216 @return [peano4.api.solversteps.Step]
217 One Observer per fused group of tasks, in an order that respects
222 for first_task, second_task
in non_fusable_task_pairs
or []:
223 non_fusable.add((first_task, second_task))
224 non_fusable.add((second_task, first_task))
227 tasks: List[Task], further_tasks: List[Task], include_cell_data: bool =
False, respect_entity_order: bool =
False
229 """Constraints 2 and 4 between two lists of tasks, optionally taking cell data and the entity order into account"""
230 return not _have_conflicting_data(tasks, further_tasks, include_cell_data, respect_entity_order)
and not any(
231 (task, further_task)
in non_fusable
for task
in tasks
for further_task
in further_tasks
234 global_tasks = [task
for task
in graph.ordered_tasks()
if task.entity == Task.GridEntity.GLOBAL]
235 order = [task
for task
in graph.ordered_tasks()
if task.entity != Task.GridEntity.GLOBAL]
237 for dependency
in task.dependencies:
238 assert dependency
not in global_tasks, f
"Task {task.name} depends on GLOBAL task {dependency.name}, which is not supported"
242 def depth(task: Task) -> int:
243 if task
not in depth_of_task:
244 depth_of_task[task] = (
245 0
if not task.dependencies
else 1 + max(depth(dependency)
for dependency
in task.dependencies)
247 return depth_of_task[task]
251 layers.setdefault(depth(task), []).append(task)
255 for layer
in sorted(layers.keys()):
256 tasks_in_layer = layers[layer]
257 for task
in tasks_in_layer:
258 if task.has_to_repeat:
261 for task
in tasks_in_layer:
262 if task.has_to_repeat:
264 if nodes_of_layer
and can_be_combined(nodes_of_layer[-1], [task], include_cell_data=
True):
265 nodes_of_layer[-1].append(task)
267 nodes_of_layer.append([task])
268 for node
in nodes_of_layer:
270 for action_set
in task.action_sets:
272 nodes += nodes_of_layer
277 highest_entity_of_current_group =
None
279 lowest_entity = min(task.entity.value
for task
in node)
280 highest_entity = max(task.entity.value
for task
in node)
281 is_repeating = len(node) == 1
and node[0].has_to_repeat
284 and current_group
is not None
285 and lowest_entity >= highest_entity_of_current_group
287 [task
for group_node
in current_group
for task
in group_node], node, respect_entity_order=
True
290 current_group.append(node)
291 highest_entity_of_current_group = max(highest_entity_of_current_group, highest_entity)
293 current_group = [node]
294 highest_entity_of_current_group = highest_entity
295 groups.append(current_group)
300 for global_task
in global_tasks:
301 groups_with_dependency = [
303 for index, group
in enumerate(groups)
304 if any(dependency
in node
for node
in group
for dependency
in global_task.dependencies)
306 if groups_with_dependency:
307 groups[max(groups_with_dependency)].append([global_task])
309 groups[-1].append([global_task])
311 groups.append([[global_task]])
313 steps = [
_create_step(group, identifier)
for group
in groups]
316 report_compiled_step_sequence(steps, title=f
"Compiled task graph for {identifier}")
323 further_tasks: List[Task],
324 include_cell_data: bool =
False,
325 respect_entity_order: bool =
False,
329 Check whether two sets of tasks can not share one mesh traversal due to
330 their face or vertex data, or can not run in parallel
332 True if one of the tasks writes a face or vertex submodel (WRITE or
333 READ_AND_WRITE) which one of the further_tasks reads or writes
334 (READ_AND_DISCARD, WRITE or READ_AND_WRITE), or vice versa. TEMPORARY
335 uses are ignored. @ref peano4.api.taskgraph.compiler.FuseSteps.translate()
337 @param include_cell_data: bool
338 If False, cell data is ignored. This is the check whether tasks can
339 share one mesh traversal. If True, conflicting cell data counts,
340 too. This is the check whether tasks can run in parallel.
342 @param respect_entity_order: bool
343 If True, tasks precede further_tasks within one traversal, and a
344 conflict on a face (vertex) submodel is ignored if the involved task
345 of tasks is a FACE (VERTEX) task: Such a task has finished with a
346 face (vertex) before any subsequent task sees it. This is the
347 exception of constraint 2 in
348 @ref peano4.api.taskgraph.compiler.FuseSteps.translate(). If False,
349 tasks and further_tasks are treated symmetrically.
352 writing_accesses = (Task.DataAccess.WRITE, Task.DataAccess.READ_AND_WRITE)
353 using_accesses = (Task.DataAccess.READ_AND_DISCARD, Task.DataAccess.WRITE, Task.DataAccess.READ_AND_WRITE)
354 entity_of_kind = {
"face": Task.GridEntity.FACE,
"vertex": Task.GridEntity.VERTEX}
356 def submodels(task, accesses):
359 for kind, access_of_submodel
in (
360 ((
"cell", task.cell_data_access),)
if include_cell_data
else ()
361 ) + ((
"face", task.face_data_access), (
"vertex", task.vertex_data_access))
362 for submodel, access
in access_of_submodel.items()
363 if access
in accesses
367 for further_task
in further_tasks:
368 conflicts = (submodels(task, writing_accesses) & submodels(further_task, using_accesses)) | (
369 submodels(further_task, writing_accesses) & submodels(task, using_accesses)
371 if respect_entity_order:
372 conflicts = {(kind, submodel)
for kind, submodel
in conflicts
if entity_of_kind.get(kind) != task.entity}
393 Merge the qualifiers several fused tasks attach to one submodel into one
395 TEMPORARY uses are ignored. The fused step has to load the submodel if
396 the first remaining use reads it (READ_AND_DISCARD or READ_AND_WRITE),
397 and it has to store the submodel if the last remaining use writes it
398 (WRITE or READ_AND_WRITE):
400 | load | store | merged qualifier |
401 |------|-------|------------------|
402 | yes | yes | READ_AND_WRITE |
403 | yes | no | READ_AND_DISCARD |
405 | no | no | TEMPORARY |
407 A submodel produced by one fused task and consumed by a later one
408 (WRITE followed by READ_AND_DISCARD) hence becomes TEMPORARY. For a
409 single access, the merged qualifier equals the access itself.
412 relevant_accesses = [access
for access
in accesses
if access != Task.DataAccess.TEMPORARY]
413 if not relevant_accesses:
414 return Task.DataAccess.TEMPORARY
415 load = relevant_accesses[0]
in (Task.DataAccess.READ_AND_DISCARD, Task.DataAccess.READ_AND_WRITE)
416 store = relevant_accesses[-1]
in (Task.DataAccess.WRITE, Task.DataAccess.READ_AND_WRITE)
418 return Task.DataAccess.READ_AND_WRITE
420 return Task.DataAccess.READ_AND_DISCARD
422 return Task.DataAccess.WRITE
423 return Task.DataAccess.TEMPORARY
426def _create_step(nodes: List[List[Task]], identifier: str) ->
"peano4.api.solversteps.Step":
429 Create one step from a group of fused nodes. @ref peano4.api.taskgraph.compiler.FuseSteps.translate()
431 Every node is a list of tasks: either a single task, or several tasks
432 from one layer of the task graph which can run in parallel.
434 Whenever a parallel node directly follows another parallel node, we
436 @ref peano4.api.solversteps.SynchroniseActionSets.SynchroniseActionSets,
437 so the two nodes yield two task groups rather than one.
440 tasks = [task
for node
in nodes
for task
in node]
442 name=f
"{identifier}_" +
"__".join(
_task_name(task)
for task
in tasks),
443 add_user_defined_actions=
False,
447 repeating_tasks = [task
for task
in tasks
if task.has_to_repeat]
448 assert len(repeating_tasks) <= 1
449 step.has_to_repeat = repeating_tasks[0].has_to_repeat
if repeating_tasks
else None
451 for data_attribute, access_attribute, step_access, use
in (
452 (
"cell_data",
"cell_data_access", step.cell_data_access, step.use_cell),
453 (
"face_data",
"face_data_access", step.face_data_access, step.use_face),
454 (
"vertex_data",
"vertex_data_access", step.vertex_data_access, step.use_vertex),
456 accesses_of_submodel = {}
458 for submodel
in getattr(task, data_attribute):
459 accesses_of_submodel.setdefault(submodel, []).append(getattr(task, access_attribute)[submodel])
460 for submodel, accesses
in accesses_of_submodel.items():
464 previous_node_is_parallel =
False
467 if node_is_parallel
and previous_node_is_parallel:
470 for action_set
in sorted(task.action_sets, key=
lambda action_set: action_set.descend_invocation_order):
471 step.action_sets.append(action_set)
472 previous_node_is_parallel = node_is_parallel
List["peano4.api.solversteps.Step"] translate(Graph graph, str identifier, "peano4.api.Project" peano_project, Optional[List[Tuple[Task, Task]]] non_fusable_task_pairs=None)
Task graph compiler which fuses tasks into as few mesh traversals as possible.
bool _have_conflicting_data(List[Task] tasks, List[Task] further_tasks, bool include_cell_data=False, bool respect_entity_order=False)
Check whether two sets of tasks can not share one mesh traversal due to their face or vertex data,...