13from ..Graph
import Graph
14from ..Task
import Task
15from .
import FuseSteps
16from .
import Sequential
17from .SpawnCellTaskActionSet
import SpawnCellTaskActionSet
18from .WaitForCellTaskActionSet
import WaitForCellTaskActionSet
24 peano_project:
"peano4.api.Project",
25) -> List[
"peano4.api.solversteps.Step"]:
28 Split tasks writing cell data into producer and consumer, and map each task onto its own mesh traversal
30 You can use this task graph compiler in your ExaHyPE 2 project through
32 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
33 project.compiler = peano4.api.taskgraph.compiler.CellTaskProducerConsumer.translate_with_sequential
34 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
36 The rewritten graph is handed over to
37 @ref peano4.api.taskgraph.compiler.Sequential.translate(). See
38 @ref peano4.api.taskgraph.compiler.CellTaskProducerConsumer._translate()
39 for a description of the rewrite and of the parameters.
42 return _translate(graph, identifier, peano_project, use_fusion=
False)
48 peano_project:
"peano4.api.Project",
49) -> List[
"peano4.api.solversteps.Step"]:
52 Split tasks writing cell data into producer and consumer, and fuse tasks into as few mesh traversals as possible
54 You can use this task graph compiler in your ExaHyPE 2 project through
56 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
57 project.compiler = peano4.api.taskgraph.compiler.CellTaskProducerConsumer.translate_with_fusion
58 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
60 The rewritten graph is handed over to
61 @ref peano4.api.taskgraph.compiler.FuseSteps.translate(), which never
62 fuses a producer with its consumer. See
63 @ref peano4.api.taskgraph.compiler.CellTaskProducerConsumer._translate()
64 for a description of the rewrite and of the parameters.
67 return _translate(graph, identifier, peano_project, use_fusion=
True)
73 peano_project:
"peano4.api.Project",
75) -> List[
"peano4.api.solversteps.Step"]:
78 Task graph compiler which splits tasks writing cell data into a producer spawning tasks and a consumer waiting for them (work in progress)
80 Do not use this routine directly. Pick
81 @ref peano4.api.taskgraph.compiler.CellTaskProducerConsumer.translate_with_sequential()
83 @ref peano4.api.taskgraph.compiler.CellTaskProducerConsumer.translate_with_fusion()
86 This compiler adds a preamble which rewrites the task graph, and then
87 hands the rewritten graph over to either
88 @ref peano4.api.taskgraph.compiler.Sequential.translate() or
89 @ref peano4.api.taskgraph.compiler.FuseSteps.translate().
93 Every task which exclusively writes cell data is replaced by two tasks.
94 A task exclusively writes cell data if it declares at least one cell
95 submodel as WRITE or READ_AND_WRITE, but no face or vertex submodel as
96 WRITE or READ_AND_WRITE (@ref peano4.api.taskgraph.compiler.CellTaskProducerConsumer._exclusively_writes_cell_data()). It may
97 read face or vertex data. All other tasks are kept as they are.
99 A task is only split if this is safe
100 (@ref peano4.api.taskgraph.compiler.CellTaskProducerConsumer._has_events_after_touch_cell_first_time()):
101 The producer spawns tasks which might still run after the producer's
102 mesh traversal. If any action set of the task yields code for
103 unprepareTraversal(), endTraversal() or one of the touch...LastTime()
104 events, these events might be invoked before the spawned tasks have
105 terminated. Such a task is hence not split, and the compiler writes a
106 warning. You can enforce the split by setting the task's flag
107 @ref peano4.api.taskgraph.Task.Task.may_spawn_background_tasks to True,
108 which skips this check. If the flag is False, the task is never split,
109 and the compiler writes no warning.
113 1. The producer, named after the original task plus "Producer". It
114 inherits all action sets and data declarations of the original
116 2. The consumer, named after the original task plus "Consumer". It
117 works on cells, too, and depends on the producer. It declares every
118 cell submodel that the producer writes (WRITE or READ_AND_WRITE) as
121 Every task that depended on the original task depends on the consumer
122 instead. Producer and consumer are marked as non-fusable, i.e. if we
123 use FuseSteps, it never puts them into the same mesh traversal. A
124 consumer however might be fused with subsequent tasks. In an ExaHyPE 2
125 setup, for example, the initial condition yields a producer and a
126 consumer, and the latter is fused with the subsequent projection onto
129 The preamble does not alter the graph passed in. It works on a copy of
134 If a peano_project is given, the producer spawns the touchCellFirstTime()
135 events of its action sets as tasks, and the consumer waits for these
136 tasks (@ref peano4.api.taskgraph.compiler.CellTaskProducerConsumer._spawn_tasks()):
138 - Every action set of the producer whose touchCellFirstTime() has a
139 non-empty body is wrapped into a
140 @ref peano4.api.taskgraph.compiler.SpawnCellTaskActionSet.SpawnCellTaskActionSet.
141 The wrapper evaluates the guard. If it holds, the wrapper takes a new
142 task number from the tarch, stores it in the cell's task label (see
143 below), and spawns the body as a task with this number. The task
144 depends on the task spawned by the previous wrapped action set of the
145 same producer, so the bodies are executed in their original order.
146 - Action sets whose touchCellFirstTime() has an empty body, such as
147 @ref exahype2.api.fv.actionsets.ApplyDirichletBoundaryConditions.ApplyDirichletBoundaryConditions, are not
148 part of the producer-consumer pattern. They remain plain action sets
149 of the producer, i.e. all their events are invoked by the producer.
150 - The same holds for action sets which do not want to run in a task
151 (@ref peano4.api.solversteps.ActionSet.ActionSet.may_run_in_background_task()),
153 @ref exahype2.api.fv.actionsets.ReadjustTimeStepSizeForOptimisticGlobalTimestepping.ReadjustTimeStepSizeForOptimisticGlobalTimestepping.
154 Their touchCellFirstTime() is invoked directly by the producer, i.e.
155 possibly while tasks spawned by previous action sets of the same
156 producer still run. They hence must not depend on these tasks.
157 - The consumer is given a cousin
158 @ref peano4.api.taskgraph.compiler.WaitForCellTaskActionSet.WaitForCellTaskActionSet
159 per wrapped action set. If the cell's task label holds a valid task
160 number, it waits for this task, returns the number to the task
161 enumerator and resets the label to NoNumber=-1.
162 - All other events of the wrapped action sets remain within the
165 The spawned tasks outlive touchCellFirstTime(). The compiler therefore
166 warns about constructs which might not be safe within a task. It does
167 not inspect the code but only searches the generated C++ text for the
168 names of data and attributes, i.e. the warnings state that a task
169 might access something. Every warning names the generator which
170 realises the data in question and suggests a fix. It has to be
171 accepted or the generation is aborted
172 (@ref peano4.api.Logger.Logger.warn_and_confirm()):
174 - The task body might access fine or coarse grid cell data which is not
175 held via a smart pointer, e.g. via @ref peano4.api.datamodel.PatchToDoubleArray or
176 @ref peano4.api.datamodel.PatchToDoubleArrayOnHeap. The task copies
177 the data, and writes to the copy are lost. The fix is a smart-pointer
179 @ref peano4.api.datamodel.PatchToDoubleArrayWithSmartPointer.
180 Heap-based generators do not suffice, as they deep-copy their data
181 whenever Peano copies an object.
182 - The task body might access vertex or face data via an enumerator
183 whose data is not held via a smart pointer. The enumerators hold
184 pointers to the objects on Peano's stacks, which are invalid once
185 touchCellFirstTime() has terminated. The task therefore always
186 captures them as peano4::datamanagement::FaceHeapDataEnumerator or
187 peano4::datamanagement::VertexHeapDataEnumerator, which copy the
188 faces or vertices. Without smart pointers, these copies are deep,
189 i.e. writes are lost. With smart pointers, no warning is issued.
190 - The task body might use a non-static attribute of the action set.
191 The task accesses an object which is destroyed after the producer's
193 - An action set of the producer has to be modified by the user
194 (@ref peano4.api.solversteps.ActionSet.ActionSet.user_should_modify_template()).
195 Such a producer is not altered, i.e. it spawns no tasks.
199 The compiler adds a new cell data type <identifier>_TaskLabels to the
200 project's data model. It is modelled via DaStGen2 and holds one integer
201 per producer action set with a non-empty touchCellFirstTime(). The
202 integer's name is the CamelCase version of the original task's name
203 followed by ActionSet, the action set's index within the task and
204 TaskNumber, e.g. EulerFVSmoothInitialConditionActionSet0TaskNumber
205 (@ref peano4.api.taskgraph.compiler.CellTaskProducerConsumer._task_number_attribute_name()).
206 The labels furthermore hold a static tarch::multicore::TaskEnumerator
207 (getTaskEnumerator()), from which the producers take their task numbers
208 and to which the consumers return them. The producer declares the task
209 labels as WRITE, the consumer as READ_AND_DISCARD: The producer sets
210 the label in every sweep, whether it spawns a task or not, so the
211 consumer's reset does not have to be kept. If the consumer declared
212 READ_AND_WRITE, a fused producer step that directly follows a consumer
213 would read the labels, too
214 (@ref peano4.api.taskgraph.compiler.PropagateUseStatements), and the
215 labels would be relayed through steps of other phases which run before
216 the labels are ever stored.
218 @param graph: peano4.api.taskgraph.Graph
219 The task graph to compile. Assumed to be acyclic.
221 @param identifier: str
222 @ref peano4.api.taskgraph.compiler.FuseSteps.translate()
224 @param peano_project: peano4.api.Project
225 Project the task labels are added to. If None, no task labels are
226 added and the producers spawn no tasks.
228 @param use_fusion: bool
229 If True, hand the rewritten graph over to FuseSteps, otherwise to
232 @return [peano4.api.solversteps.Step]
233 @ref peano4.api.taskgraph.compiler.FuseSteps.translate()
238 if peano_project
is not None and split_tasks:
240 task_labels.data.add_attribute(
242 name=
"taskEnumerator",
243 type=
"tarch::multicore::TaskEnumerator",
244 include=
'#include "tarch/multicore/TaskEnumerator.h"',
245 qualifier=dastgen2.attributes.Attribute.Qualifier.STATIC,
246 user_type_has_toString_method=
False,
250 peano_project.datamodel.add_cell(task_labels)
252 non_fusable_task_pairs = [(producer, consumer)
for _, producer, consumer
in split_tasks]
254 return FuseSteps.translate(producer_consumer_graph, identifier, peano_project, non_fusable_task_pairs)
256 return Sequential.translate(producer_consumer_graph, identifier, peano_project)
262 CamelCase version of the task's name followed by ActionSet, the action
263 set index and TaskNumber. Words are separated by whitespace or
264 underscores, and only the first letter of each word is capitalised, so
265 existing camel case and acronyms are preserved. "EulerFVSmooth_Update
266 cell" with action set index 0 for example becomes
267 EulerFVSmoothUpdateCellActionSet0TaskNumber.
270 words = task.name.replace(
"_",
" ").split()
271 return "".join(word[:1].upper() + word[1:]
for word
in words) + f
"ActionSet{action_set_index}TaskNumber"
277 Shallow copy of a task with its own lists and dictionaries, so we can
278 alter the copy's dependencies without touching the original graph. The
279 action sets and the submodels themselves are shared.
282 result = copy.copy(task)
283 result.dependencies = []
284 result.action_sets = list(task.action_sets)
285 result.cell_data = list(task.cell_data)
286 result.face_data = list(task.face_data)
287 result.vertex_data = list(task.vertex_data)
288 result.cell_data_access = dict(task.cell_data_access)
289 result.face_data_access = dict(task.face_data_access)
290 result.vertex_data_access = dict(task.vertex_data_access)
297 True if task declares at least one cell submodel as WRITE or
298 READ_AND_WRITE, and no face or vertex submodel as WRITE or
299 READ_AND_WRITE. @ref peano4.api.taskgraph.compiler.CellTaskProducerConsumer._translate()
302 writing_accesses = (Task.DataAccess.WRITE, Task.DataAccess.READ_AND_WRITE)
303 writes_cell_data = any(access
in writing_accesses
for access
in task.cell_data_access.values())
304 writes_other_data = any(
305 access
in writing_accesses
306 for access
in list(task.face_data_access.values()) + list(task.vertex_data_access.values())
308 return writes_cell_data
and not writes_other_data
314 List the events which the task's action sets realise and which a
315 producer would invoke before its spawned tasks have necessarily
316 terminated: unprepareTraversal(), endTraversal() and the
317 touch...LastTime() events. An event counts if the action set yields
321 One entry "<action set name>::<event>" per such event. The list is
322 empty if the task can be split safely.
326 peano4.api.solversteps.ActionSet.OPERATION_END_TRAVERSAL,
327 peano4.api.solversteps.ActionSet.OPERATION_TOUCH_VERTEX_LAST_TIME,
328 peano4.api.solversteps.ActionSet.OPERATION_TOUCH_FACE_LAST_TIME,
329 peano4.api.solversteps.ActionSet.OPERATION_TOUCH_CELL_LAST_TIME,
332 for action_set
in task.action_sets:
333 if action_set.get_implementation_of_unprepareTraversal().strip() !=
"":
334 result.append(f
"{action_set.get_action_set_name()}::unprepareTraversal")
335 for operation_name
in late_operations:
336 if action_set.get_implementation_of_operation(operation_name).strip() !=
"":
337 result.append(f
"{action_set.get_action_set_name()}::{operation_name}")
344 Build the producer-consumer graph. @ref peano4.api.taskgraph.compiler.CellTaskProducerConsumer._translate()
346 @return (peano4.api.taskgraph.Graph, [(Task, Task, Task)])
347 The new graph and the list of (original task, producer, consumer)
348 triples. Producer and consumer must not be fused.
351 producer_of_task = {}
352 consumer_of_task = {}
353 for task
in graph.tasks:
358 if late_events
and task.may_spawn_background_tasks
is not True:
360 f
"Warning: Task {task.name} is not split into a producer and a consumer task, as a split-up might be unsafe: "
361 f
"The producer's spawned tasks might still run when the following events are invoked: "
362 +
", ".join(late_events)
363 + f
". Set may_spawn_background_tasks=True on task {task.name} to enforce the split.",
364 peano4.api.LoggerPrintOption.ALWAYS_PRINT,
367 producer_of_task[task].name = f
"{task.name} Producer"
368 consumer =
Task(name=f
"{task.name} Consumer", entity=Task.GridEntity.CELL)
369 for submodel
in task.cell_data:
370 if task.cell_data_access[submodel]
in (Task.DataAccess.WRITE, Task.DataAccess.READ_AND_WRITE):
371 consumer.use_cell(submodel, Task.DataAccess.READ_AND_WRITE)
372 consumer_of_task[task] = consumer
375 for task
in graph.tasks:
376 producer = producer_of_task[task]
377 for dependency
in task.dependencies:
380 producer.add_dependency(consumer_of_task.get(dependency, producer_of_task[dependency]))
381 result.add_task(producer)
382 if task
in consumer_of_task:
383 consumer = consumer_of_task[task]
384 consumer.add_dependency(producer)
385 result.add_task(consumer)
387 split_tasks = [(task, producer_of_task[task], consumer_of_task[task])
for task
in graph.tasks
if task
in consumer_of_task]
388 return result, split_tasks
394 Names of the non-static attributes declared in the C++ snippet returned
395 by an action set's get_attributes(). This is a heuristic: We split the
396 snippet into statements, skip all statements which are static or
397 contain a bracket (functions), and take the last identifier before an
398 optional array size or initialisation.
401 attributes = re.sub(
r"/\*.*?\*/",
"", attributes, flags=re.S)
402 attributes = re.sub(
r"//[^\n]*",
"", attributes)
404 for statement
in attributes.split(
";"):
405 words = statement.split()
406 if not words
or "static" in words
or "(" in statement:
408 match = re.search(
r"([A-Za-z_]\w*)\s*(\[[^\]]*\]\s*)*(=.*)?$", statement.strip(), re.S)
410 result.append(match.group(1))
417 Name of the generator class which realises the submodel in C++.
420 generator = getattr(submodel,
"generator",
None)
421 return "no generator" if generator
is None else type(generator).__name__
427 True if the submodel's generator holds the data via a smart pointer,
428 i.e. if copies of a C++ object share the data rather than cloning it.
434_SMART_POINTER_HINT = (
435 "Realise the data via a smart-pointer generator, e.g. peano4.api.datamodel.PatchToDoubleArrayWithSmartPointer. "
436 "Heap-based generators such as peano4.api.datamodel.PatchToDoubleArrayOnHeap do not suffice, as they deep-copy "
437 "their data whenever an object is copied, and Peano copies the objects when it moves them between its stacks."
444 Names of the vertex and face enumerators which a task body might use,
445 i.e. which occur in the generated C++ text. They are realised as
446 peano4::datamanagement::FaceEnumerator or
447 peano4::datamanagement::VertexEnumerator and hold pointers into Peano's
448 stacks, so the spawned task has to capture copies
449 (@ref peano4.api.taskgraph.compiler.SpawnCellTaskActionSet.SpawnCellTaskActionSet).
451 @return ([str], [str])
452 Names of the face enumerators and of the vertex enumerators.
456 f
"{prefix}{submodel.name}"
457 for prefix
in (
"fineGridFaces",
"coarseGridFaces")
458 for submodel
in task.face_data
459 if re.search(rf
"\b{prefix}{submodel.name}\b", task_body)
461 vertex_enumerators = [
462 f
"{prefix}{submodel.name}"
463 for prefix
in (
"fineGridVertices",
"coarseGridVertices")
464 for submodel
in task.vertex_data
465 if re.search(rf
"\b{prefix}{submodel.name}\b", task_body)
467 return face_enumerators, vertex_enumerators
470def _warn_about_unsafe_task(task: Task, action_set_index: int, action_set:
"peano4.api.solversteps.ActionSet", task_body: str):
473 Issue the warnings about task bodies which might not
474 be safe. @ref peano4.api.taskgraph.compiler.CellTaskProducerConsumer._translate()
476 We do not inspect the code. We only search the generated C++ text for
477 the names of data and attributes, i.e. the warnings say that a task
478 might access something.
482 description = f
"action set {action_set_index} ({action_set.get_action_set_name()}) of task {task.name}"
484 for accessor
in (
"fineGridCell",
"coarseGridCell"):
485 for submodel
in task.cell_data:
487 logger.warn_and_confirm(
488 f
"The touchCellFirstTime() task of {description} might access cell data {submodel.name} ({accessor}{submodel.name}). "
489 f
"{submodel.name} is realised via the generator {_generator_name(submodel)}, which does not hold its data via a smart "
490 f
"pointer. The task captures the cell data by value, i.e. it works on a copy, and all writes are lost. "
491 + _SMART_POINTER_HINT
495 for kind, submodels, enumerators, prefixes, enumerator_type
in (
496 (
"face", task.face_data, face_enumerators, (
"fineGridFaces",
"coarseGridFaces"),
"peano4::datamanagement::FaceHeapDataEnumerator"),
497 (
"vertex", task.vertex_data, vertex_enumerators, (
"fineGridVertices",
"coarseGridVertices"),
"peano4::datamanagement::VertexHeapDataEnumerator"),
499 for submodel
in submodels:
500 for enumerator
in [f
"{prefix}{submodel.name}" for prefix
in prefixes
if f
"{prefix}{submodel.name}" in enumerators]:
502 logger.warn_and_confirm(
503 f
"The touchCellFirstTime() task of {description} might access {kind} data {submodel.name} via {enumerator}. "
504 f
"The task therefore captures {enumerator} as {enumerator_type}, which copies the {kind}s. {submodel.name} is "
505 f
"realised via the generator {_generator_name(submodel)}, which does not hold its data via a smart pointer, i.e. "
506 f
"the copies are deep: The task reads the data as it was when the task has been spawned, and all writes are lost. "
507 + _SMART_POINTER_HINT
511 if re.search(rf
"\b{attribute_name}\b", task_body):
512 logger.warn_and_confirm(
513 f
"The touchCellFirstTime() task of {description} might use the non-static attribute {attribute_name}. The task then "
514 f
"accesses an action set object which is destroyed after the producer's mesh traversal."
518def _spawn_tasks(split_tasks, task_labels:
"peano4.api.datamodel.DaStGen2"):
521 Make the producers spawn tasks and the consumers wait for them, and add
522 one task number per spawning action set to the task labels.
523 @ref peano4.api.taskgraph.compiler.CellTaskProducerConsumer._translate()
525 @param split_tasks: [(Task, Task, Task)]
526 Triples of original task, producer and consumer.
528 @param task_labels: peano4.api.datamodel.DaStGen2
529 The task labels. This routine adds the attributes.
532 Number of attributes added to the task labels.
536 number_of_task_numbers = 0
537 for task, producer, consumer
in split_tasks:
538 user_action_sets = [action_set
for action_set
in task.action_sets
if action_set.user_should_modify_template()]
540 logger.warn_and_confirm(
541 f
"Task {task.name} holds action sets which the user has to modify ("
542 +
", ".join(action_set.get_action_set_name()
for action_set
in user_action_sets)
543 +
"). The task is split into producer and consumer, but spawns no tasks."
547 producer_action_sets = []
548 consumer_action_sets = []
549 previous_task_number_attribute_name =
None
550 for action_set_index, action_set
in enumerate(task.action_sets):
551 task_body = action_set.get_body_of_operation(peano4.api.solversteps.ActionSet.OPERATION_TOUCH_CELL_FIRST_TIME)
552 if task_body.strip() ==
"" or not action_set.may_run_in_background_task():
556 producer_action_sets.append(action_set)
562 number_of_task_numbers += 1
568 task_number_attribute_name,
569 previous_task_number_attribute_name,
573 producer_action_sets.append(producer_action_set)
575 previous_task_number_attribute_name = task_number_attribute_name
577 producer.action_sets = producer_action_sets
578 consumer.action_sets = consumer_action_sets
580 if previous_task_number_attribute_name
is not None:
581 producer.use_cell(task_labels, Task.DataAccess.WRITE)
586 consumer.use_cell(task_labels, Task.DataAccess.READ_AND_DISCARD)
588 return number_of_task_numbers
Wrapper around user-defined attribute.
Singleton logger for the Peano 4 Python API.
Default superclass for any data model in Peano which is stored within the grid.
Holds a sequence of Task instances.
One task within a peano4.api.taskgraph.Graph.
Producer-side wrapper around an action set, which spawns touchCellFirstTime() as a task.
Consumer-side cousin of a peano4.api.taskgraph.compiler.SpawnCellTaskActionSet.SpawnCellTaskActionSet...
The Python-side model of "what happens during one mesh sweep".
str _task_number_attribute_name(Task task, int action_set_index)
CamelCase version of the task's name followed by ActionSet, the action set index and TaskNumber.
List["peano4.api.solversteps.Step"] translate_with_fusion(Graph graph, str identifier, "peano4.api.Project" peano_project)
Split tasks writing cell data into producer and consumer, and fuse tasks into as few mesh traversals ...
List["peano4.api.solversteps.Step"] translate_with_sequential(Graph graph, str identifier, "peano4.api.Project" peano_project)
Split tasks writing cell data into producer and consumer, and map each task onto its own mesh travers...
List[str] _has_events_after_touch_cell_first_time(Task task)
List the events which the task's action sets realise and which a producer would invoke before its spa...
List["peano4.api.solversteps.Step"] _translate(Graph graph, str identifier, "peano4.api.Project" peano_project, bool use_fusion)
Task graph compiler which splits tasks writing cell data into a producer spawning tasks and a consume...
_enumerators_used_by(Task task, str task_body)
Names of the vertex and face enumerators which a task body might use, i.e.
bool _exclusively_writes_cell_data(Task task)
True if task declares at least one cell submodel as WRITE or READ_AND_WRITE, and no face or vertex su...
_warn_about_unsafe_task(Task task, int action_set_index, "peano4.api.solversteps.ActionSet" action_set, str task_body)
Issue the warnings about task bodies which might not be safe.
str _generator_name(submodel)
Name of the generator class which realises the submodel in C++.
Task _copy_task(Task task)
Shallow copy of a task with its own lists and dictionaries, so we can alter the copy's dependencies w...
_split_cell_tasks(Graph graph)
Build the producer-consumer graph.
List[str] _non_static_attribute_names(str attributes)
Names of the non-static attributes declared in the C++ snippet returned by an action set's get_attrib...
bool _is_held_via_smart_pointer(submodel)
True if the submodel's generator holds the data via a smart pointer, i.e.
_spawn_tasks(split_tasks, "peano4.api.datamodel.DaStGen2" task_labels)
Make the producers spawn tasks and the consumers wait for them, and add one task number per spawning ...