Skip to content

R2 RFC for task_group dynamic dependencies #1664

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

Open
wants to merge 46 commits into
base: master
Choose a base branch
from

Conversation

kboyarinov
Copy link
Contributor

Description

Add RFC describing the semantics for concrete task_group dynamic dependencies APIs

Fixes # - issue number(s) if exists

Type of change

Choose one or multiple, leave empty if none of the other choices apply

Add a respective label(s) to PR if you have permissions

  • bug fix - change that fixes an issue
  • new feature - change that adds functionality
  • tests - change in tests
  • infrastructure - change in infrastructure and CI
  • documentation - documentation update

Tests

  • added - required for new features and some bug fixes
  • not needed

Documentation

  • updated in # - add PR number
  • needs to be updated
  • not needed

Breaks backward compatibility

  • Yes
  • No
  • Unknown

Notify the following users

List users with @ to send notifications

Other information

vossmjp and others added 30 commits December 4, 2024 17:34
Co-authored-by: Aleksei Fedotov <aleksei.fedotov@intel.com>
Co-authored-by: Aleksei Fedotov <aleksei.fedotov@intel.com>
Co-authored-by: Aleksei Fedotov <aleksei.fedotov@intel.com>
Co-authored-by: Aleksei Fedotov <aleksei.fedotov@intel.com>
Co-authored-by: Alexandra <alexandra.epanchinzeva@intel.com>
Co-authored-by: Alexandra <alexandra.epanchinzeva@intel.com>
Co-authored-by: Alexandra <alexandra.epanchinzeva@intel.com>
Co-authored-by: Alexandra <alexandra.epanchinzeva@intel.com>
Co-authored-by: Alexandra <alexandra.epanchinzeva@intel.com>
Co-authored-by: Alexandra <alexandra.epanchinzeva@intel.com>
Co-authored-by: Alexandra <alexandra.epanchinzeva@intel.com>
Co-authored-by: Alexandra <alexandra.epanchinzeva@intel.com>
Co-authored-by: Alexandra <alexandra.epanchinzeva@intel.com>
Co-authored-by: Alexandra <alexandra.epanchinzeva@intel.com>
Co-authored-by: Alexey Kukanov <alexey.kukanov@intel.com>
Co-authored-by: Konstantin Boyarinov <konstantin.boyarinov@intel.com>
* Remove concrete proposals from the RFC

* Apply review comments
@kboyarinov kboyarinov added the RFC label Mar 3, 2025
@kboyarinov kboyarinov marked this pull request as ready for review March 6, 2025 12:51
Comment on lines 11 to 14
* Semantics for ``task_group::run``, ``task_group::run_and_wait``, ``task_arena::enqueue`` and ``this_task_arena::enqueue`` should be defined
when the input ``task_handle`` is handling a task in various states.
* Semantics for returning a ``task_handle`` handling a task in various states from the body of the task should be described (while using the existing
preview extensions for ``task_group``).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two sub-items are not sufficient unfortunately. First and foremost, the task_handle class needs to evolve from the current move-only type to a type that can be safely shared between multiple threads and across program scopes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed by introducing the task_tracker.

Comment on lines 41 to 42
(e.g. using ``task_group::run(std::move(task_handle))``) looks misleading even if some guarantees are provided for the referred handle object. It also creates an error-prone
for TBB developers - any move-construct or move-assign from the accepted handle will break the guarantee. Code analyzers?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also creates an error-prone for TBB developers

An error-prone what? And whom specifically you refer to as TBB developers?

Also I would suggest to use "move construction" and "move assignment" here, with no hyphen (the hyphen is needed to form adjectives etc., e.g. "move-assignable", "move-assigned"). This would be consistent with the C++ standard use of the terms.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant it was easy to make a mistake and break the guarantees while having the non-const rvalue reference in the argument to run. This issue should be addressed by introducing the task_tracker.

(e.g. using ``task_group::run(std::move(task_handle))``) looks misleading even if some guarantees are provided for the referred handle object. It also creates an error-prone
for TBB developers - any move-construct or move-assign from the accepted handle will break the guarantee. Code analyzers?

To handle this, the proposal is to extend all functions that take the task handled by the ``task_handle`` with the new overload taking an non-const lvalue reference and provide the following guarantees:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am afraid that taking a non-const lvalue reference is error-prone.

Consider one of the examples from the umbrella RFC (a bit simplified here):

tbb::task_handle add_another_task(tbb::task_group& tg, int work_id) {
    tbb::task_handle new_task = tg.defer([=] { do_work(work_id); });
    // ...
    tg.run(new_task); // takes the handle by reference
    // Return the newly created task to the caller
    return new_task;
}

I hope the problem is obvious.

If we want to support this pattern, taking task_handle by reference seems a bad idea. Of course it can be "copied" internally, or the actual task pointer can be extracted and stored somewhere, but the semantics of taking a handle by lvalue reference just does not seem right.

Probably we can/should think of it in the terms of ownership. The current design supports only a single owner, and it is reflected in the move semantics for submission: the ownership is transferred to the task scheduler. We want either a shared ownership, which can be represented by copies of the task handle, or some form of weak reference/observation (see https://en.cppreference.com/w/cpp/memory/weak_ptr). The API design should conform to the chosen approach.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like the required semantics for ownership is somewhere in the middle. It seems like we need "unique" ownership to submit the task and "weak" ownership to track the task progress and set the dependencies. And the option may be to define 2 handlers of the task:

  • Current task_handle that owns the task and can be used to make any action on it - submit for execution or add dependencies.
  • New handle (let's say task_view) that does not own the task, but can be used to track progress or add dependencies.

task_handle would be almost unchanged. task_view would be copyable and any copy would be able make actions on the underlying task that is owned either by the task_handle or the TBB scheduler.

task_view can be constructible from task_handle but not the opposite.

Submitting functions (task_group::run and others) would also remain unchanged and allow only the task_handle argument.

Functions for adding dependencies would provide overloads taking both task_handle and task_view where the task in any state can be taken, and only the task_handle where only the task in created state in allowed (like the successor side of make_edge).

Copy link
Contributor

@akukanov akukanov Mar 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have come to essentially the same conclusion. Even though a new class is an API complication, it seems that trying to extend task_handle semantics and adjust all the related APIs would complicate everything even more.

I would not use task_view for the name, as it is not really a view to a task. Rather, it's a weak_task_handle - though that it is also not ideal because, unlike weak_ptr, this type should not allow "promotion" to a "strong" task handle.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

task_tracker with the semantics stated above was introduced.

Copy link
Contributor

@vossmjp vossmjp Mar 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also don't like having another class, but I agree that it simplifies thinking about the problem, so while complicating the API, it makes it easier to reason about.

Comment on lines 83 to 84
Extra care must be taken while working with this method anyway since even if it returns ``false``, it may be unsafe to submit the ``task_handle`` since the state
can be changed by the other thread.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems related to the ownership question. Maybe some form of "exclusive" ownership is needed to submit the task.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed by introducing the task_tracker

<img src="transferring_between_two_separate_tracking_new_successors.png" width=800>

Such an approach can be beneficial if ``current`` task is kind of generator task that collects the set of successors on each iteration of the loop
and then transfers it to the newly created task.
Copy link
Contributor

@vossmjp vossmjp Mar 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you provide a sketch of such a generator example that shows where the loop would be, and how additional successors would be added to the current task from an outside task or thread. I think the more common use case would be recursive decomposition, such as the merge sort example included in the umbrella RFC, where a task body refines its work into a subgraph. So an initial node N becomes N -> N1. N1, when executed, becomes N1 -> N2, etc. When viewed from outside the task, the original N represents a complete piece of work and so outside tasks or threads would want to make successors of the complete work represented by the N's new subgraph. So an outside task, might want to add N->M. Or, if N is in some state of refinement N->N1->N2->M. In those cases, I can't think of a reason why an outside task or thread would want to insert an additional successor of N ignoring its refinement into a subgraph, such as N->{M,N1->N2}. Maybe your generator case is such as counter-example, but I'm not sure I can immediately imagine practical generator scenarios. Do you have some in mind?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have thought once more about that and seems like the generator example I had in mind is not realistic. Initially it was something like:

tbb::task_group tg;
tbb::task_tracker tracker;

tbb::task_handle handle = tg.defer([] {
    while (exit_condition) {
        tbb::task_handle new_task = tg.defer(...);
        tbb::task_group::current_task::transfer_successors_to(new_task);
        tg.run(new_task);
    }
});

tracker = handle;

tg.run(std::move(handle));

// Adding successors to tracker for each iteration of the loop

But the loop inside of the task should wait somehow for the new successors to be added which seems inefficient.
I think now it looks more like finding a problem for the solution:)
I have removed this paragraph from the RFC for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The add_successor API was removed from the proposal. I have added the separate section describing alternatives.

### Adding successors to the current task

Consider use-case of parallel wavefront pattern on the 2-d grid. Each cell is computed as part of a separate task in ``task_group``. Each cell task computes
itself and creates more tasks to process the cell below and the cell on the right.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually the executing task does not create the other tasks in a wavefront, since there are typically multiple predecessors. For example, the task to the right of a task A, let's call it C, may also be the task below B. More typically, wavefront tasks are pre-allocated and dependencies set from outside of the tasks themselves. Otherwise, who is responsible for creating C? A or B? What if A creates C, adds itself as a predecessor, and completes before B executes? Would there be an edge from B to C?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, definitely there should be some mechanism to synchronize on the task creation. So, at least there should be some pre-allocated and pre-set counter somewhere that would gate a particular task instantiation and/or execution.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have found an example of a divide-and-conquer wavefront where edges are made from within an executing task and also from external tasks, so my previous comment is moot.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The add_successor API was removed from the proposal. I have added the separate section describing alternatives.
I have also added the "Advanced examples" section describing all wavefront patterns we found with the possible implementations.


<img src="transferring_between_two_separate_tracking_new_successors.png" width=800>

Alternative approach is to keep tracking ``current`` and ``target`` together after transferring. This requires introducing the new state of task - a `proxy` state.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can think of examples where combining tracking is useful. I think we need a counter example, where separating is useful. Otherwise, the choice is obvious.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems I missed the use case where adding successors to the task being executed is useful. If there are no one such use case, I believe it would be a way more easier to disallow adding successors to being executed tasks. So, we will have only that tbb::task_group::current_task::transfer_successors_to(task_handle), where task_handle can be only in created or submitted state.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latest version proposes the combined tracking. Added the "Eager + classic" wavefront in the advanced examples section as a motivation.

…ntics.md

Co-authored-by: Mike Voss <michaelj.voss@intel.com>
Copy link
Contributor

@aleksei-fedotov aleksei-fedotov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are overcomplicating things without proofs we actually need to support all the described behaviors.

and the task handled by ``successor_task_handle`` -
``task_group::make_edge(predecessor_task_handle, successor_task_handle)``.

As it was stated in the parent RFC document, we would like to allow adding predecessors in any state described above and to limit the successor to be a task in created state since it can be too late to add predecessors to
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to allow adding predecessors to the task in submitted state as well? In case the user knows that the successor's execution cannot be started even if it is in submitted state (e.g. because of its predecessors are still in created state), I think it may be reasonable to allow adding more predecessors to such a submitted task.

Suggested change
As it was stated in the parent RFC document, we would like to allow adding predecessors in any state described above and to limit the successor to be a task in created state since it can be too late to add predecessors to
As it was stated in the parent RFC document, we would like to allow adding predecessors in any state described above and to limit the successor to be a task in created or submitted state since it can be too late to add predecessors to

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But if the user doesn't know that there are other predecessors in the created state then adding a predecessor to a submitted task results in a race, where it may or may not become a predecessor quickly enough. Is such a race desirable? I understand the case you've described, but I'm not sure we should allow it. Whichever way we decide to go, this should be a an open question that we want answered if its released as a preview.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. That's the point. If we want building an API that does not allow all these unsafe peculiarities, then I would just start with existing task_handle, even not allowing to specify successors for already submitted tasks, but only for created ones. Let's then see if it won't be enough for the users.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this as an open question.

### Adding successors to the current task

Consider use-case of parallel wavefront pattern on the 2-d grid. Each cell is computed as part of a separate task in ``task_group``. Each cell task computes
itself and creates more tasks to process the cell below and the cell on the right.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, definitely there should be some mechanism to synchronize on the task creation. So, at least there should be some pre-allocated and pre-set counter somewhere that would gate a particular task instantiation and/or execution.

Comment on lines 120 to 122
If there is a strong dependency between the computation of the current cell and the computations of the following cells, it is required to add
currently executed task as a predecessor of the tasks representing the cells below and on the right. Since there is no ``task_handle`` or ``task_tracker`` representing the
currently executed task, the ``make_edge`` function described above cannot be used to set these dependencies.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are speaking here about the tasks created right inside being executed task, there is no straightforward way to automatically know their task_handles in the other tasks. Therefore, the dependency described here could be just made implicitly by submitting these tasks as the last step which is done in the task being executed. Not to mention that the body of one of the created tasks can simply be run without any task creation and therefore task submission at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This API is removed from the latest version of the proposal because of possibility to spawn the dependent tasks after doing the computations.


<img src="transferring_between_two_separate_tracking_new_successors.png" width=800>

Alternative approach is to keep tracking ``current`` and ``target`` together after transferring. This requires introducing the new state of task - a `proxy` state.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems I missed the use case where adding successors to the task being executed is useful. If there are no one such use case, I believe it would be a way more easier to disallow adding successors to being executed tasks. So, we will have only that tbb::task_group::current_task::transfer_successors_to(task_handle), where task_handle can be only in created or submitted state.

Comment on lines 223 to 224
task_tracker(const task_tracker& other);
task_tracker(task_tracker&& other);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For which use cases we need to allow copying and/or moving of this class instances? Is not it enough to only have possibility to create task_tracker from task_handle?
Similar question about *-assignment operators.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it can be useful for storing task_tracker in the containers. I have added a section with advanced examples and possible implementations. Recursive eager wavefront shows the necessity of copy semantics as well (while storing the trackers to the parent subtasks in the child task).

The alternative approaches are to keep only the ``task_handle`` as the only was to track the task, set the
dependencies and submit the task for execution.

### ``task_handle`` as a unique owner
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that having a task_handle as oneTBB has it now should be enough to cover all mentioned use cases with a few extensions to it that will allow adding dependencies between valid task_handles. I am not sure we need to answer right now the question about setting already moved task_handle as a predecessor. We can just prohibit this behavior in the first iteration of this extension for task dynamic dependencies until we find the use cases and users who will benefit from it.
And even if we prove the necessity in such support we may just relax the wording about moved task_handle by saying that the move operation only affects that the task cannot be submitted multiple times, but still allow setting it as a predecessor for another tasks. I believe this will not be confusing to users because "the edges" between the tasks represent the weak thing by itself. Meaning that moving them with the body of the task or leaving them with the task_handle after the move does not make any influence on the execution of the tasks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recursive eager wavefront example added to the "Advanced examples" section shows the necessity of creating a predecessor-successor dependencies between the task in any state and the newly created task.
It also shows the necessity of copy and move semantics for task_tracker. If this would be implemented using the task_handle - we will need to make it a shared owner of the task and define the behavior when one of the copies is scheduled for execution.

@kboyarinov kboyarinov added this to the 2022.3.0 milestone Apr 22, 2025
After the transfer, the successors of ``current`` and ``target`` are tracked separately and adding new successors to one of them would only
affect the successors list of one task:

<img src="transferring_between_two_separate_tracking_new_successors.png" width=600>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<img src="transferring_between_two_separate_tracking_new_successors.png" width=600>
<img src="assets/transferring_between_two_separate_tracking_new_successors.png" width=600>

static void make_edge(task_tracker& pred, task_handle& succ);

struct current_task {
static void transfer_successors_to(task_handle& h);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be great if the name of this function better reflected the behavior where current_task becomes a sort of proxy to h. I am not a native English speaker but transfer sounds like a one-time move of something somewhere. I don't think the following names are 100% better but I think they are worth considering:

  • redirect/forward/delegate_successors_to
  • transform_as/become_proxy_for

Copy link
Contributor

@aleksei-fedotov aleksei-fedotov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still not a full review.

Existing API only allows having a non-empty ``task_handle`` object handling the created task and an empty one that does not
handle any tasks.
* API for setting the dependencies between tasks in various states.
* API for transferring the dependencies from the executing task to the task in various states.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps, rephrase to avoid misinterpretation:

Suggested change
* API for transferring the dependencies from the executing task to the task in various states.
* API for transferring the dependent tasks (i.e. successors) from the executing task to the task in various states.


In this case, no semantic changes are needed for the submission methods, because the ``task_handle`` semantics is not changed.

Alternative approaches for handling tasks in different states are described in the [separate section](#alternative-approaches).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The link points to non-existing target.

and the task handled by ``successor_task_handle`` -
``task_group::make_edge(predecessor_task_handle, successor_task_handle)``.

As it was stated in the parent RFC document, we would like to allow adding predecessors in any state described above and to limit the successor to be a task in created state since it can be too late to add predecessors to
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we allow adding predecessors to a successor in submitted state when the user knows that the successor cannot start because its other dependencies are not completed yet?

Comment on lines +90 to +91
returned pointer is ``nullptr``. Otherwise, the successor task pointer is returned. It is required to allow bypassing one of the successor tasks
if the body of the predecessor task did not return other task that should be bypassed.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think requiring to allow bypassing is somewhat overstated. It would be good to but not required, isn't it? I believe you instead wanted to write something like: If the body of the predecessor task does not bypass another task, the reference counting mechanism that decreases counters of successor tasks may take advantage and bypass one of these successor tasks if it becomes eligible for execution.

undefined.

Current proposal limits the applicability of this API and allows only transferring from the executing task to the created task.
Transferring to tasks in `scheduled`, `executing` or `completed` states are described in a [separate section](#transferring-successors-to-the-task-in-any-state)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The link is broken as it seems there is no transferring-successors-to-the-task-in-any-state target for it.

// north dependency
if (j != 0) { tbb::task_group::make_edge(cell_tasks[i, j - 1], cell_tasks[i][j]); }

// west dependency
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// west dependency
// upper left dependency

?

tg.run(std::move(cell_tasks[i][j]));
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to call the wait method?

void eager_wavefront(std::size_t n) {
tbb::task_group tg;

std::size_t num_divisions = log2(n / eager_wavefront_task::wavefront_grainsize) + 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eager_wavefront_task::wavefront_grainsize is private.

Comment on lines +958 to +959
private:
static constexpr std::size_t wavefront_grainsize = 5;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private:
static constexpr std::size_t wavefront_grainsize = 5;
static constexpr std::size_t wavefront_grainsize = 5;
private:

Comment on lines +828 to +831
// Number of elements in the grid on each level of division
// On the first level - 4 subtasks total (2 on each dimension)
// on the second level - 16 subtasks total (4 on each dimension), etc.
std::size_t n_grid = (2 << n_division);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems from the description, depending on the value of n_division, which I assume could be any value from a natural number set, the n_grid variable should been having values equal to the power of two, i.e. 2, 4, 8, 16, 32, etc. However, the description says they are 4, 16, 64, 256, and so on.
So what are the values that n_grid variable can store?

static void publish_predecessor(std::size_t n_division, std::size_t x_index, std::size_t y_index,
tbb::task_tracker&& pred)
{
std::size_t n_grid = (2 << n_division>>);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line seems incorrect.

std::size_t n_grid = (2 << n_division>>);

[[maybe_unused]] auto it = predecessors[n_division].emplace(x_index * n_grid + y_index, std::move(pred));
assert(it != predecessors[n_division].end());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that getting the past the end iterator is possible for the map. I believe the expression in the assert statement always equals to true.

Comment on lines +859 to +861
if (i_size <= wavefront_grainsize) {
assert(j_size <= wavefront_grainsize);
serial_wavefront(m_i0, m_in, m_j0, m_jn);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i_size and j_size do not exist as well as m_i0, m_in, m_j0, and m_jn. Did you mean x_size, y_size, m_x0, m_xn, m_y0, m_yn?

Suggested change
if (i_size <= wavefront_grainsize) {
assert(j_size <= wavefront_grainsize);
serial_wavefront(m_i0, m_in, m_j0, m_jn);
if (x_size <= wavefront_grainsize) {
assert(y_size <= wavefront_grainsize);
serial_wavefront(m_x0, m_xn, m_y0, m_yn);

Comment on lines +833 to +834
auto it = predecessors[n_division].find(x_index * n_grid + y_index);
return it != predecessors[n_division].end() ? it->second : tbb::task_tracker{};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I understand how predecessor tasks cannot be found. They are added unconditionally and the dependencies are there by the algorithm design.


``explicit operator bool() const noexcept``

Checks if `this`` tracks any task object.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Checks if `this`` tracks any task object.
Checks if ``this`` tracks any task object.

* API improvements and enhancements should be considered (may be a criteria for moving the feature to `supported`):
* Should comparison functions between ``task_tracker`` and ``task_handle`` be defined?
* Should ``task_tracker`` tracking the task in created or submitted state (not always) be allowed as a
successor in ``make_edge``? See [separate section](#using-a-task_tracker-as-a-successor) for more details.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Broken link - it refers to non-existing paragraph.

Perhaps, the following will work:

Suggested change
successor in ``make_edge``? See [separate section](#using-a-task_tracker-as-a-successor) for more details.
successor in ``make_edge``? See [separate section](#Using a ``task_tracker`` as a successor) for more details.

* Should ``task_tracker`` tracking the task in created or submitted state (not always) be allowed as a
successor in ``make_edge``? See [separate section](#using-a-task_tracker-as-a-successor) for more details.
* Should ``empty_task`` helper API be provided to optimize creating and spawning the empty sync-only tasks. See
[separate section](#empty_task-api) for more details.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Broken link - it refers to non-existing paragraph.

Perhaps, the following will work:

Suggested change
[separate section](#empty_task-api) for more details.
[separate section](#``empty_task`` API) for more details.

Comment on lines +130 to +134
Alternative approach is to keep tracking ``current`` and ``target`` together after transferring. This requires introducing a special state
of ``task_tracker` - a kind of `proxy` state. If the executing task calls `transfer_successors_to` from the body, all trackers, initially created
to track this task are changing the state to `proxy`.

Tracker to the ``current`` and ``target`` are sharing the single merged list of successors if ``current`` tracker is in `proxy` state.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my point of view, introducing another entity for essentially the same thing from the user-perspective complicates the design. The user will need to keep them separate in mind and remember what they are good for and what they are not.

As far as I understand, implementation-wise task_tracker represents this kind of shared-ownership, as it still needs to have a pointer to representation of at least the task state and the list of successors shared among task_tracker instances pointing to the same task. In this sense, it differs from the task_handle only in impossibility to be submitted for execution.
To avoid possible confusion about acting on moved task_handle, can we just deprecate the move semantics and add only lvalue signatures? After all, the same function object can be submitted several times into task_group.

Comment on lines +1268 to +1275
class task_group {
static void make_edge(task_handle& pred, task_handle& succ);
static void make_edge(task_tracker& pred, task_handle& succ);

struct current_task {
static void transfer_successors_to(task_handle& h);
};
}; // class task_group
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should consider introducing the return of a [[maybe_unused]] task_handle/task_tracker by the task_group::run(Func&& f)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants