Skip to content

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

Draft
vepadulano wants to merge 1 commit into
root-project:masterfrom
vepadulano:df-rntuple-imt-work-stealing
Draft

[df] Rework RNTuple MT scheduling with a split work-stealing approach#23327
vepadulano wants to merge 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

    23 files      23 suites   3d 20h 37m 44s ⏱️
 3 881 tests  3 879 ✅ 0 💤 2 ❌
79 924 runs  79 922 ✅ 0 💤 2 ❌

For more details on these failures, see this check.

Results for commit ece30c1.

♻️ 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.

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.

As a new effort (along with cleaning up ROOT's install tree), I could imagine moving this file to inc/ROOT/Internal.

Comment on lines +54 to +56
friend void ROOT::Internal::IMTUtils::ParallelFor(
TThreadExecutor &pool, std::shared_ptr<ROOT::Internal::IMTUtils::RParallelSplitFileProcessor> proc,
const std::function<void(const ROOT::Internal::IMTUtils::RParallelSplitFileProcessor &)> &body);

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.

Is the private/friend mechanic required?

If the method were public, you could still not call the overload unless you have a shared_ptr to ROOT::Internal::IMTUtils::RParallelSplitFileProcessor.

ULong64_t fFirstEntry = 0; ///< First entry index in fSource
/// End entry index in fSource, e.g. the number of entries in the range is fLastEntry - fFirstEntry
ULong64_t fLastEntry = 0;
ULong64_t fEntryOffset = 0; /// Offset of both first and last entries w.r.t. the page source

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.

Note that the doxygen comment will pertain to the string_view below. If we are changing it anyway, could you explain why it's needed? Why not add the offset to both numbers and not have this field?

@@ -86,10 +109,11 @@ class RNTupleDS final : public ROOT::RDF::RDataSource {
/// The GetEntryRanges() swaps fNextRanges and fCurrentRanges and uses the list of
/// REntryRangeDS records to return the list of ranges ready to use by the RDF loop manager.
struct REntryRangeDS {
std::unique_ptr<ROOT::Internal::RPageSource> fSource;
std::shared_ptr<ROOT::Internal::RPageSource> fSource;

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.

Why do the pages sources become shared_ptr?

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.

Because TBB needs to be able to copy it, it seems.

Comment on lines 946 to +958
if (sampleMap.empty())
return ROOT::RDF::RSampleInfo(
ntupleID, std::make_pair(fCurrentRanges[rangeIdx].fFirstEntry, fCurrentRanges[rangeIdx].fLastEntry), nullptr,
fPrincipalDescriptor.GetNEntries());
return ROOT::RDF::RSampleInfo(ntupleID,
std::make_pair(entryRangeDS.fFirstEntry - entryRangeDS.fEntryOffset,
entryRangeDS.fLastEntry - entryRangeDS.fEntryOffset),
nullptr, fPrincipalDescriptor.GetNEntries());

if (sampleMap.find(ntupleID) == sampleMap.end())
throw std::runtime_error("Full sample identifier '" + ntupleID + "' cannot be found in the available samples.");

return ROOT::RDF::RSampleInfo(
ntupleID, std::make_pair(fCurrentRanges[rangeIdx].fFirstEntry, fCurrentRanges[rangeIdx].fLastEntry),
sampleMap.at(ntupleID), fPrincipalDescriptor.GetNEntries());
return ROOT::RDF::RSampleInfo(ntupleID,
std::make_pair(entryRangeDS.fFirstEntry - entryRangeDS.fEntryOffset,
entryRangeDS.fLastEntry - entryRangeDS.fEntryOffset),
sampleMap.at(ntupleID), fPrincipalDescriptor.GetNEntries());

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.

Initially not part of this PR, but looking at these lines, could we unify them for better readability? I was wondering if all arguments of the constructor for both cases are identical (they pretty much are), but it wasn't so easy to see.

How about this?

   RSample* sample = nullptr; 
   if (!sampleMap.empty()) {
      if (auto result = sampleMap.find(ntupleID); result != sampleMap.end())
         sample = *result;
      else
         throw std::runtime_error("Full sample identifier '" + ntupleID + "' cannot be found in the available samples.");
   }

   return ROOT::RDF::RSampleInfo(ntupleID,
                                 std::make_pair(entryRangeDS.fFirstEntry - entryRangeDS.fEntryOffset,
                                                entryRangeDS.fLastEntry - entryRangeDS.fEntryOffset),
                                 sample, fPrincipalDescriptor.GetNEntries());

Comment on lines +1027 to +1029
newPageSource->SetEntryRange(
{fClusters[newClusterIdxBegin].fFirstEntry,
fClusters[newClusterIdxEnd - 1].fLastEntryPlusOne - fClusters[newClusterIdxBegin].fFirstEntry});

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.

Looks like this is doubled. The constructor seems to do the same.

fClusterIdxBegin(clusterIdxBegin),
fClusterIdxEnd(clusterIdxEnd)
{
fPageSource->SetEntryRange(

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.

Should we pageSource->Attach() for consistency / safety?

auto newClusterIdxBegin = clusterIdxMid;
auto newClusterIdxEnd = fClusterIdxEnd;
auto newPageSource = fPageSource->Clone();
newPageSource->Attach();

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.

Might not be needed if done in constructor.

}
}

auto processFileWithGlobalOffset = [&](std::size_t fileIdx) {

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.

Note to @hageboeck review this. 🙂

Comment on lines +1187 to +1188
const auto &fileName = fFileNames[fileIdx];
auto source = CreatePageSource(fNTupleName, fFileNames[fileIdx]);

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.

For consistency in case somebody changes fileName at some point.

Suggested change
const auto &fileName = fFileNames[fileIdx];
auto source = CreatePageSource(fNTupleName, fFileNames[fileIdx]);
const auto &fileName = fFileNames[fileIdx];
auto source = CreatePageSource(fNTupleName, fileName);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants