Peano
Loading...
Searching...
No Matches
FuseSteps.py
Go to the documentation of this file.
1# This file is part of the Peano project. For conditions of distribution and
2# use, please see the copyright notice at www.peano-framework.org
3from typing import List, Optional, Tuple
4
5import peano4.api
7
8from ..Graph import Graph
9from ..Task import Task
10from .ReportCompiledStepSequence import report_compiled_step_sequence
11
12
14 graph: Graph,
15 identifier: str,
16 peano_project: "peano4.api.Project",
17 non_fusable_task_pairs: Optional[List[Tuple[Task, Task]]] = None,
18) -> List["peano4.api.solversteps.Step"]:
19 """!
20
21 Task graph compiler which fuses tasks into as few mesh traversals as possible
22
23 You can use this task graph compiler in your ExaHyPE 2 project through
24
25 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
26 project.compiler = peano4.api.taskgraph.compiler.FuseSteps.translate
27 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
28
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.
32
33 ## When can two tasks be fused?
34
35 Two tasks end up in the same traversal if and only if all of the
36 following constraints hold:
37
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
48 apply among them.
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.
81
82<br /><br />
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.
93</div>
94
95
96 ## Algorithm
97
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
123 task.
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
128 looks further ahead.
129
130 ## Examples
131
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.
136
137 ### Entity order
138
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.
143
144 @image html fuse-steps-entity-order.png
145
146 ### Parallel tasks
147
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.
152
153 @image html fuse-steps-parallel.png
154
155 ### Conflicting face data
156
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.
161
162 @image html fuse-steps-data-conflict.png
163
164 ### Face task before cell task
165
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.
170
171 @image html fuse-steps-face-before-cell.png
172
173 ## Constructing the fused step
174
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.
183
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().
191
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().
197
198 propagate_use_statements() is not invoked here. It is a postprocessing
199 step called outside of the translation.
200
201 @param graph: peano4.api.taskgraph.Graph
202 The task graph to compile. Assumed to be acyclic.
203
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.
208
209 @param peano_project: peano4.api.Project
210 Not used by this compiler. @ref peano4.api.taskgraph.compiler
211
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.
215
216 @return [peano4.api.solversteps.Step]
217 One Observer per fused group of tasks, in an order that respects
218 every dependency.
219
220 """
221 non_fusable = set()
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))
225
226 def can_be_combined(
227 tasks: List[Task], further_tasks: List[Task], include_cell_data: bool = False, respect_entity_order: bool = False
228 ) -> bool:
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
232 )
233
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]
236 for task in order:
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"
239
240 depth_of_task = {}
241
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)
246 )
247 return depth_of_task[task]
248
249 layers = {}
250 for task in order:
251 layers.setdefault(depth(task), []).append(task)
252
253 # Step 1 and 2: nodes (lists of tasks) per layer
254 nodes = []
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:
259 nodes.append([task])
260 nodes_of_layer = []
261 for task in tasks_in_layer:
262 if task.has_to_repeat:
263 continue
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)
266 else:
267 nodes_of_layer.append([task])
268 for node in nodes_of_layer:
269 for task in node:
270 for action_set in task.action_sets:
271 action_set.parallel = _is_parallel_node(node)
272 nodes += nodes_of_layer
273
274 # Step 3: greedy fusion along the sequence of nodes
275 groups = []
276 current_group = None
277 highest_entity_of_current_group = None
278 for node in nodes:
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
282 if (
283 not is_repeating
284 and current_group is not None
285 and lowest_entity >= highest_entity_of_current_group
286 and can_be_combined(
287 [task for group_node in current_group for task in group_node], node, respect_entity_order=True
288 )
289 ):
290 current_group.append(node)
291 highest_entity_of_current_group = max(highest_entity_of_current_group, highest_entity)
292 else:
293 current_group = [node]
294 highest_entity_of_current_group = highest_entity
295 groups.append(current_group)
296 if is_repeating:
297 current_group = None
298
299 # Constraint 5: append each global task to the group of its last dependency
300 for global_task in global_tasks:
301 groups_with_dependency = [
302 index
303 for index, group in enumerate(groups)
304 if any(dependency in node for node in group for dependency in global_task.dependencies)
305 ]
306 if groups_with_dependency:
307 groups[max(groups_with_dependency)].append([global_task])
308 elif groups:
309 groups[-1].append([global_task])
310 else:
311 groups.append([[global_task]])
312
313 steps = [_create_step(group, identifier) for group in groups]
314
315 peano4.api.Logger().new_section(f"Compile task graph '{identifier}'")
316 report_compiled_step_sequence(steps, title=f"Compiled task graph for {identifier}")
317
318 return steps
319
320
322 tasks: List[Task],
323 further_tasks: List[Task],
324 include_cell_data: bool = False,
325 respect_entity_order: bool = False,
326) -> bool:
327 """!
328
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
331
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()
336
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.
341
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.
350
351 """
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}
355
356 def submodels(task, accesses):
357 return {
358 (kind, submodel)
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
364 }
365
366 for task in tasks:
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)
370 )
371 if respect_entity_order:
372 conflicts = {(kind, submodel) for kind, submodel in conflicts if entity_of_kind.get(kind) != task.entity}
373 if conflicts:
374 return True
375 return False
376
377
378def _task_name(task: Task) -> str:
379 """!
380
381 Turn every whitespace-separated word into its own name segment,
382 capitalising only its first letter - a blind .title() would also
383 lower-case the rest of the word, which mangles existing camel case and
384 acronyms (e.g. "EulerFVSmooth" would become "Eulerfvsmooth").
385
386 """
387 return "_".join(word[:1].upper() + word[1:] for word in task.name.split())
388
389
390def _merge_data_access(accesses: List["Task.DataAccess"]) -> "Task.DataAccess":
391 """!
392
393 Merge the qualifiers several fused tasks attach to one submodel into one
394
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):
399
400 | load | store | merged qualifier |
401 |------|-------|------------------|
402 | yes | yes | READ_AND_WRITE |
403 | yes | no | READ_AND_DISCARD |
404 | no | yes | WRITE |
405 | no | no | TEMPORARY |
406
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.
410
411 """
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)
417 if load and store:
418 return Task.DataAccess.READ_AND_WRITE
419 if load:
420 return Task.DataAccess.READ_AND_DISCARD
421 if store:
422 return Task.DataAccess.WRITE
423 return Task.DataAccess.TEMPORARY
424
425
426def _create_step(nodes: List[List[Task]], identifier: str) -> "peano4.api.solversteps.Step":
427 """!
428
429 Create one step from a group of fused nodes. @ref peano4.api.taskgraph.compiler.FuseSteps.translate()
430
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.
433
434 Whenever a parallel node directly follows another parallel node, we
435 insert a
436 @ref peano4.api.solversteps.SynchroniseActionSets.SynchroniseActionSets,
437 so the two nodes yield two task groups rather than one.
438
439 """
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,
444 )
445 # Repeating tasks are never fused with other non-global tasks, i.e. a
446 # group holds at most one repeating task
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
450
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),
455 ):
456 accesses_of_submodel = {}
457 for task in tasks:
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():
461 use(submodel)
462 step_access[submodel] = _merge_data_access(accesses)
463
464 previous_node_is_parallel = False
465 for node in nodes:
466 node_is_parallel = _is_parallel_node(node)
467 if node_is_parallel and previous_node_is_parallel:
468 step.action_sets.append(peano4.api.solversteps.SynchroniseActionSets())
469 for task in node:
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
473
474 return step
475
476
477def _is_parallel_node(node: List[Task]) -> bool:
478 """!
479
480 A node is parallel if it holds more than one task and each task holds
481 exactly one action set. @ref peano4.api.taskgraph.compiler.FuseSteps.translate()
482
483 """
484 return len(node) > 1 and all(len(task.action_sets) == 1 for task in node)
Singleton logger for the Peano 4 Python API.
Definition Logger.py:37
Marker which ends a group of action sets that run in parallel.
The Python-side model of "what happens during one mesh sweep".
Definition __init__.py:1
bool _is_parallel_node(List[Task] node)
A node is parallel if it holds more than one task and each task holds exactly one action set.
Definition FuseSteps.py:477
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.
Definition FuseSteps.py:18
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,...
Definition FuseSteps.py:326
"Task.DataAccess" _merge_data_access(List["Task.DataAccess"] accesses)
Merge the qualifiers several fused tasks attach to one submodel into one.
Definition FuseSteps.py:390
"peano4.api.solversteps.Step" _create_step(List[List[Task]] nodes, str identifier)
Create one step from a group of fused nodes.
Definition FuseSteps.py:426
str _task_name(Task task)
Turn every whitespace-separated word into its own name segment, capitalising only its first letter - ...
Definition FuseSteps.py:378