Skip to content

[df] Rework RNTuple MT scheduling with a split work-stealing approach - #23327

Merged
vepadulano merged 1 commit into
root-project:masterfrom
vepadulano:df-rntuple-imt-work-stealing
Oct 23, 2026
Merged

vepadulano merged 1 commit into
root-project:masterfrom
vepadulano:df-rntuple-imt-work-stealing

Conversation

@vepadulano

Copy link
Copy Markdown
Member

Alternative to #22631 , implementing native IMT work-stealing for the RNTuple data source

@github-actions

github-actions Bot commented Sep 9, 2026 •

Copy link
Copy Markdown

Test Results

    24 files      24 suites   4d 4h 39m 55s ⏱️
 3 893 tests  3 891 ✅ 0 💤 2 ❌
83 373 runs  83 371 ✅ 0 💤 2 ❌

For more details on these failures, see this check.

Results for commit 20bebc0.

♻️ This comment has been updated with latest results.

@hageboeck hageboeck left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🚀

Here's a first pass with a few suggestions and a few questions.

Comment thread core/imt/inc/ROOT/InternalIMTUtils.hxx
Comment thread core/imt/inc/ROOT/TThreadExecutor.hxx Outdated
Comment thread tree/dataframe/inc/ROOT/RNTupleDS.hxx
Comment thread tree/dataframe/inc/ROOT/RNTupleDS.hxx
Comment thread tree/dataframe/src/RNTupleDS.cxx
Comment thread tree/dataframe/src/RNTupleDS.cxx Outdated
Comment thread tree/dataframe/src/RNTupleDS.cxx Outdated
Comment thread tree/dataframe/src/RNTupleDS.cxx Outdated
Comment thread tree/dataframe/src/RNTupleDS.cxx Outdated
Comment thread tree/dataframe/src/RNTupleDS.cxx Outdated

@jblomer jblomer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you for this work!

I think it may be easier to follow the code if the lambdas became class methods. I would also make documentation easier. Generally, I think a high-level documentation of the scheduling approach would be helpful.

Regarding the use of TBB: would it be possible/easier to simply use a TTaskGroup?

Comment thread core/imt/inc/ROOT/InternalIMTUtils.hxx Outdated
Comment thread core/imt/inc/ROOT/InternalIMTUtils.hxx Outdated
Comment thread tree/dataframe/src/RNTupleDS.cxx Outdated
@vepadulano
vepadulano force-pushed the df-rntuple-imt-work-stealing branch from ece30c1 to 06ccada Compare October 19, 2026 08:54
@vepadulano
vepadulano marked this pull request as ready for review October 19, 2026 08:58
@vepadulano

Copy link
Copy Markdown
Member Author

HI @hageboeck @jblomer , I have addressed your PR reviews and updated the changes. Documentation has been added throughout, and I've also memorialised the strategy in a separate markdown document as requested by @jblomer . Let me know your thoughts, thanks!

Comment thread core/imt/inc/ROOT/TThreadExecutor.hxx Outdated
Comment thread core/imt/src/TThreadExecutor.cxx Outdated
Comment thread core/imt/inc/ROOT/InternalIMTUtils.hxx Outdated
Comment thread core/imt/inc/ROOT/InternalIMTUtils.hxx Outdated
Comment thread tree/dataframe/src/RLoopManager.cxx Outdated
Comment thread tree/dataframe/src/RNTupleDS.cxx
Comment thread tree/dataframe/src/RNTupleDS.cxx Outdated
Comment thread tree/dataframe/src/RNTupleDS.cxx Outdated
Comment thread tree/dataframe/src/RNTupleDS.cxx Outdated
Comment thread tree/dataframe/src/RNTupleDS.cxx Outdated
@vepadulano
vepadulano force-pushed the df-rntuple-imt-work-stealing branch 2 times, most recently from da362e3 to 5c79602 Compare October 19, 2026 18:06
Comment thread tree/dataframe/src/RLoopManager.cxx
Comment thread tree/dataframe/doc/parallel_scheduling.md
Comment thread tree/dataframe/doc/parallel_scheduling.md Outdated
Comment thread tree/dataframe/doc/parallel_scheduling.md
Comment thread tree/dataframe/doc/parallel_scheduling.md Outdated
@vepadulano
vepadulano force-pushed the df-rntuple-imt-work-stealing branch from 5c79602 to 167b27a Compare October 19, 2026 22:57

@jblomer jblomer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks!

Some first comments on the documentation. I'll review the code later today.

Comment thread tree/dataframe/doc/parallel_scheduling.md Outdated
Comment thread tree/dataframe/doc/parallel_scheduling.md Outdated
Comment thread tree/dataframe/doc/parallel_scheduling.md Outdated
Comment thread tree/dataframe/doc/parallel_scheduling.md Outdated
Comment thread tree/dataframe/doc/parallel_scheduling.md Outdated
Comment thread tree/dataframe/doc/parallel_scheduling.md
@vepadulano
vepadulano force-pushed the df-rntuple-imt-work-stealing branch from 167b27a to efdf7a8 Compare October 20, 2026 15:07
@vepadulano

Copy link
Copy Markdown
Member Author

@jblomer thanks for the review! I have rebased the branch, updated the code to use the new LoadAllPageLists method to ensure clusters are available, added a test with many input files to check the data source keeps the number of open files limited. I have also implemented your comments to the parallel_scheduling markdown.

@jblomer jblomer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In principle looks good to me. Some minor comments and one potentially larger questions on the work splitting.

Comment thread tree/dataframe/test/datasource_ntuple.cxx
Comment thread tree/dataframe/inc/ROOT/RDF/RLoopManager.hxx
Comment thread tree/dataframe/inc/ROOT/RNTupleDS.hxx
Comment thread tree/dataframe/inc/ROOT/RNTupleDS.hxx
Comment thread tree/dataframe/src/RNTupleDS.cxx
Comment thread tree/dataframe/src/RNTupleDS.cxx Outdated
Comment thread tree/dataframe/src/RNTupleDS.cxx
Comment thread tree/dataframe/src/RNTupleDS.cxx

@hahnjo hahnjo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Disclaimer: I've not carefully inspected every line in this PR, so feel free to point out if the following breaks down for very obvious reasons that I'm missing.

From a high level, it seems to me that "forcing" RNTuple scheduling into splittable ranges for consumption by TBB is both complex and limiting at the same time. The main "restriction" that we set ourselves is that we expect a created task A to work on file 1, task B on file 2, etc. I wonder if we can get more flexibility by just picking the next file in each task. This will also allow staging files in the background, done in the current implementation but likely not functional anymore in this PR (to be confirmed).

Roughly something like the following:

  1. Start the background thread to stage files according to the number of slots.
  2. Start as many tasks as files.
  3. Upon execution, pick the next file and extend the sliding window of files to be staged.
  4. Open the file and start iterating clusters (potentially with another loop for lazy loading of cluster group details).
  5. When at some point, all queued tasks are executing (easily noticed by all files being picked up), go into tail-scheduling mode: create a (nested) task group and schedule tasks for all remaining clusters. Each cluster-task clones the page source from the "parent" file-task and then works on only that cluster. This should efficiently distribute the remaining work.

The number of open files should be limited to twice the number of slots:

  • During staging, one per file-task and one per slot in the background thread.
  • In the tail-scheduling mode, one per file-task and one for each executing cluster-task.

Comment thread tree/dataframe/doc/parallel_scheduling.md Outdated
The MT scheduling of RNTupleDS is changed. The data source adopts now a similar pattern as TTreeDS, implementing a ProcessMT(ROOT::Detail::RDF::RLoopManager &lm) method. This allows the data source to manage the creation and scheduling of multithreaded tasks, inside of which the loop manager is invoked to apply the computation graph to the range of entries in the task.

A brief summary of the scheduling approach follows:

1. One task is launched in parallel per input file
2. At the beginning of the file-based task, a page source is created and the list of clusters is computed
  a. If the user has not specified a global entry range, it is the actual list of clusters of the page source. This happens in ScheduleTasksForFile.
  b. If the user has specified a global entry range, before launching the file-based tasks the data source had computed a full list of global entry offsets across all input files. The list of clusters of the page source is then adjusted according to the position of the global entry range w.r.t. the global offsets of this file. This happens in ScheduleTasksForFileWithGlobalOffset.
3. The task goes on within ScheduleTaskForPageSource, which invokes TBB's parallel_for with two arguments, one respecting the Range and the other respecting the Body requirements. See the docs respectively at https://oneapi-spec.uxlfoundation.org/specifications/oneapi/latest/elements/onetbb/source/named_requirements/algorithms/range and https://oneapi-spec.uxlfoundation.org/specifications/oneapi/latest/elements/onetbb/source/named_requirements/algorithms/par_for_body.
  a. The Range argument is of type RPageSourceSplittableRange, a struct representing a range of entries (N consecutive clusters) tied to a specific page source to be processed.
  b. The first time parallel_for is invoked, an RPageSourceSplittableRange instance is passed to it which includes the whole list of clusters for that page source.
  c. TBB immediately calls the Range object's SplitWork method, which will split the cluster range in roughly two equal parts, the first part is re-assigned to the same object, the second part is used to create a new RPageSourceSplittableRange.
  d. TBB continues to split the task recursively, until a list of all non-splittable tasks is available.
  e. Only then, the TBB parallel_for will schedule the various calls to the ParallelForBody function, each taking one of the cluster ranges generated before.
  f. Within ParallelForBody, the computation graph is executed by reading data from the page source, restricting it to the assigned cluster range, by calling RLoopManager::RNTupleThreadTask.

This approach delegates to the scheduling engine (TBB in this case) the responsibility to optimise the task processing and split tasks that may be running for too long (e.g. some files may contain many more entries than others).

The strategy is also documented in a markdown file for posterity.
@vepadulano
vepadulano force-pushed the df-rntuple-imt-work-stealing branch from efdf7a8 to 20bebc0 Compare October 21, 2026 20:57
@vepadulano

Copy link
Copy Markdown
Member Author

Thanks @jblomer for the review. After your comment on the parallel_scheduling.md file, about discussing the strategy to keep the number of open files checked, as well as the question on exactly one TBB may split a Range-based task, I spent some time studying TBB more carefully. I found the oneAPI docs a bit lacking, so I looked for other resources and found this book https://link.springer.com/chapter/10.1007/978-1-4842-4398-5_16. Chapter 9, pages 263 and onwards explain the splitting strategy quite clearly (at least to me). Splitting is done eagerly, as soon as one Range is created, it will be split recursively until only non-splittable sub-Range objects remain. These are then used concurrently as arguments to ParallelForBody. I also changed the logic to have the page source only be Cloned in the ParallelForBody, rather than within a Range. This in practice leads to a guarantee of at most 2*nSlots open page sources, which I've documented in the markdown and in code.

Thanks also to @hahnjo for the comment and the insightful conversation we had offline. Following that conversation, I agree that there is a generally more optimised strategy to be devised, which includes re-enabling the page source staging that this PR is currently disabling for the MT case.

I would propose to move ahead with the general logic proposed in this PR, as it shows better performance than the one currently available in master, and have this as the new MT strategy for 6.42. As a follow up, I will attempt to implement the strategy proposed by @hahnjo and @jblomer, and further refine it for the following ROOT release so that it will be also comparable with the strategy proposed by this PR.

@vepadulano
vepadulano requested a review from jblomer October 21, 2026 21:13
@jblomer

jblomer commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Fine for me to move forward with this now. I think it would be a good idea to open an issue where we summarize the subsequent planned changes.

Regarding the splitting: doesn't this mean that we end up with all tasks processing at most 3 clusters? This would mean that the cluster prefetching does not work very well because, by setting the entry range on the page source, we prevent prefetching across task boundaries.

We can probably work around this, but I'm less convinced now that TBB splittable tasks is what we want. I think for the next iteration, we should reconsider a design where we are in control of the exact task creation (e.g. along the lines of what @hahnjo suggests) together with a shared data structure that may require locking at every cluster boundary.

@vepadulano

Copy link
Copy Markdown
Member Author

Regarding the splitting: doesn't this mean that we end up with almost all tasks processing 3 clusters plus the tail tasks with potentially fewer clusters? This would mean that the cluster prefetching does not work very well because, by setting the entry range on the page source, we prevent prefetching across task boundaries.

Yes, exactly. This is the main motivation for the stopping condition given by the IsDivisible method, having at least three clusters gives the page source some chance of prefetching the next clusters, although clearly this is less optimal for I/O purposes than reading the whole file, sequentially.

We can probably work around this, but I'm less convinced now that TBB splittable tasks is what we want. I think for the next iteration, we should reconsider a design where we are in control of the exact task creation (e.g. along the lines of what @hahnjo suggests) together with a shared data structure that may require locking at every cluster boundary.

We have discussed a strategy offline somewhat along those lines yes, the finer details will need to be seen in practice.

@vepadulano vepadulano added the clean build Ask CI to do non-incremental build on PR label Oct 23, 2026
@vepadulano vepadulano closed this Oct 23, 2026
@vepadulano vepadulano reopened this Oct 23, 2026
@vepadulano
vepadulano merged commit b87cfa2 into root-project:master Oct 23, 2026
51 of 75 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clean build Ask CI to do non-incremental build on PR in:RDataFrame in:RNTuple

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants