-
Notifications
You must be signed in to change notification settings - Fork 1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RFC] task_group_dynamic_dependencies #1469
base: master
Are you sure you want to change the base?
Conversation
to use the task_group or the flow graph APIs to express patterns that were | ||
previously expressed using with the lowest level tasking API. And for most | ||
cases, this has been sufficient. However, there are some use cases that create | ||
dynamically expanding graphs of dependent tasks are awkward to express using |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't get this grammar. And I think awkward is likely not a good wording.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: However, there is one use case which is not straightforward to express by the revised API: Dynamic task graphs which are not trees.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
The current class definition in the latest oneTBB specification for | ||
`tbb::task_group` is shown below. Note the `defer` function because this | ||
function and its return type, `task_handle`, are the foundation of our proposed | ||
extensions: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add an "existing" before defer to highlight that this exists already?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
void run(Func&& f); | ||
|
||
template<typename Func> | ||
task_handle defer(Func&& f); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I notice that task_handle is not extensively documented atm. The docu says what it does, but not why. Can we add more docu there?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I will open a task in internal tracking to improve documentation related to task_handle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
|
||
For the sake of discussion, let’s label four points in a task’s lifetime: | ||
|
||
1. pre-submitted |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense to call this deferred?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatively, call it "created" in line with the text below
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like "deferred" since it matches the defer function name. I think it will accurately imply that it is not yet submitted. I had been concerned that a submitted task that has not-yet-ready-to-run because of dependencies could also be thought of as "deferred", but on second thought, I think deferred is different enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest that we are careful with the term "deferred". A deferred task has a specific meaning in OpenMP. While OpenMP is not the "bible", I think it is clever to stick to their semantics or to explicitly avoid their terms where we disagree with their usage. So in this sense, my idea with "deferred" was wrong. I should have thought about this more carefully.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Already changed it. Do you have a better suggestion instead? So maybe "created" then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, created is better. So the story is: People create tasks. By default, they sit within a task group and are deferred or they are part of a task graph and are deferred, too. This is in line with TBB's terminology as well: https://www.intel.com/content/www/us/en/docs/onetbb/developer-guide-api-reference/2021-12/migrating-from-low-level-task-api.html#DEFERRED-TASK-CREATION. With the new interface, they are not deferred but actually immediately spawned even though we add further dependencies later on. Does that make sense?
For the sake of discussion, let’s label four points in a task’s lifetime: | ||
|
||
1. pre-submitted | ||
2. submitted for dependence tracking |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The addition "for dependence tracking" is confusing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was trying, again, to differentiate with ready-to-run. Submitted to what? Submitted to the scheduler to be tracked before executing or submitted to the ready queue. But again, I was probably over thinking this. I agree that states of 1. deferred, 2. submitted, 3. executing and 4. completed are clear enough.
The obvious next extension is to add a mechanism for specifying dependencies | ||
between tasks. In the most conservative view, it should only be legal to add | ||
additional predecessors / in-dependencies to tasks in the pre-submitted state. | ||
After a task starts executing or is completed, it doesn’t make sense to add |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is not 100% correct: If a task is already running and we add a new predecessor, we can read this as a suspend/interrupt. Isn't that what you want to add? We don't need it in our code btw.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I was not intending to support a suspend. But yes, the language "doesn't make sense" is inaccurate since, as you just pointed out, it can make sense. I'll fix this to match my intention and not be overly broad.
I think this proposal is lacking the final definition of |
1. Extend semantics and useful lifetime of `task_handle`. We propose `task_handle` to represent tasks for the purpose of adding dependencies. The useful lifetime and semantics of `task_handle` will need to be extended to include tasks that have been submitted, are currently executing, or have been completed. | ||
2. Add functions to set task dependencies. In the current `task_group`, tasks can only be waited for as a group and there is no direct way to add any before-after relationships between individual tasks. We will discuss options for spelling. | ||
3. Add a function to move successors from a currently executing task to a new task. This functionality is necessary for recursively generated task graphs. This case represents a situation where it is safe to modify dependencies for an already submitted task. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please split the long lines.
the existing interfaces and so this proposal expands tbb::task_group to make | ||
additional use cases easier to express. | ||
|
||
The current class definition in the latest oneTBB specification for |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here it is better to refer to a certain version of the specification, rather than to "current" and "latest" which will change over time. I would also add a link to (or a [short.name] of) the respective section of the spec
`task_handle` so that remains valid after it is passed to run and it can | ||
represent tasks in any state, including submitted, executing, and completed | ||
tasks. Similarly, a `task_handle` in the submitted state may represent a task | ||
that has predecessors that must complete before it can executed, and so passing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A typo:
that has predecessors that must complete before it can executed, and so passing | |
that has predecessors that must complete before it can execute, and so passing |
our task_group extension to cover. | ||
|
||
The key capability required for recursive decomposition is the ability to | ||
create work while executing a task and insert this newly create work before |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A typo:
create work while executing a task and insert this newly create work before | |
create work while executing a task and insert this newly created work before |
this function from outside a task, or passing anything other than a task in | ||
the created state is undefined behavior. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider being more precise here to avoid variations in interpretation of the meaning:
this function from outside a task, or passing anything other than a task in | |
the created state is undefined behavior. | |
this function from outside a task, or passing anything other than a `task_handle` | |
representing a task in the created state is undefined behavior. |
OR, perhaps, we might want to give more flexibility to the user (see my suggestion about API below). In this case, something like the following may work:
this function from outside a task, or passing anything other than a task in | |
the created state is undefined behavior. | |
this function from outside a task, or passing anything other than an object | |
representing a task in the created state is undefined behavior. |
void add_successor(task_handle& th); | ||
}; | ||
|
||
void transfer_successors_to(task_handle& th); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this method is related to the currently executing task, what about including this API into tbb::this_task::
namespace? By analogy with tbb::this_task_arena::
namespace.
|
||
#### void task_handle::add_predecessor(task_handle& th); | ||
|
||
Adds `th` as a predecessor that must complete before the task represnted by |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A typo:
Adds `th` as a predecessor that must complete before the task represnted by | |
Adds `th` as a predecessor that must complete before the task represented by |
- Are the suggested APIs sufficient? | ||
- Are there additional use cases that should be considered that we missed in our analysis? | ||
- Are there other parts of the pre-oneTBB tasking API that developers have struggled to find a good alternative for? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are interesting questions.
While reading this RFC, I kind of rushed proposing additional syntax sugar that besides being more user-friendly, since it seems to represent popular use cases, can save some CPU cycles. So I am posting them here for a discussion.
- Would it be useful to have a method that simultaneously joins (or even fuses?) the instantiation of a task and transferring of the current task successors?
Something like:
template <typename Func>
task_handle transfer_successors_to(Func&& f);
For the recursive decomposition scenario, instantiating a new task within an executing task and transferring successors to that new task seems to be the main model of writing such algorithms. Although, it seems to be not saving much (only one call to the library and assign to a couple of pointers?), there is always(?) going to be such a sequence in the code. Otherwise, how else an execution of already submitted tasks can be postponed?
Shall we also consider a question of having that API instead or in addition to the one proposed?
- As for
add_predecessor(s)
andadd_successor(s)
I have a couple of thoughts.
a. It seems again that it might be useful to merge instantiation of the new task handles and adding them as successors/predecessors:b. Also, I think having an API that would allow adding more than one predecessor/successor at once can be useful, since usually a number of successors/predecessors are instantiated. I only think that we don't need to limit ourselves to only two parameters as it was proposed optionally, but allow passing of an arbitrary size of task handles or even user lambdas. Of course, a pattern of having a single task producer might be viewed as a limiting one, but there actually might be the cases where tasks cannot be made available to the scheduler (i.e. spawned) until all of them are gathered together from different producers, which essentially represents a barrier in the execution pipeline. Not to mention that the spawning of a bunch of tasks all together are done faster than regular one by one spawnings. Spawning of a bunch of tasks at once was implemented in the old TBB, as far as I remember.template <typename Func> void add_predecessor(Func&& f); template <typename Func> void add_successor(Func&& f);
So here I suggest to have something like:However, this also seems to ask for having thetemplate <typename... Func> void add_predecessors(Func&& ...f); template <typename... Func> void add_successor(Func&& ...f);
task_group::run()
method to accept an arbitrary size of task handles and/or functors. So, perhaps, it is more related to another RFC/extension...
Description
A proposal to extend task_group:
task_handle
. We proposetask_handle
to represent tasks for the purpose of adding dependencies. The useful lifetime and semantics oftask_handle
will need to be extended to include tasks that have been submitted, are currently executing, or have been completed.task_group
, tasks can only be waited for as a group and there is no direct way to add any before-after relationships between individual tasks. We will discuss options for spelling.Type of change
Tests
Documentation
Breaks backward compatibility
Notify the following users
List users with
@
to send notificationsOther information