RDataFrame ForeachSlot template lambda

Thanks up-front to @Niklas_Nolte for helping me understand the problem so far!

I use a lambda in RDataFrame::ForeachSlot successfully to process specified columns in my dataset.

auto myfunc n= [](Double_t vOpt, Double_t v1, Double_t v2) { < do stuff > };
d.ForeachSlot( myfunc , { "varOpt", "var1", "var2" } );

Now some columns that are only present in certain datasets and the rest that are always present. Since the tasks that involve the always-present and sometimes-present columns are independent, I figured I could have a single lambda that processes the always-present columns and a ‘wrapper’ lambda that can be passed to the ForeachSlot when I know the extra columns will be present.

auto myfunc1 = [](Double_t v1, Double_t v2) { < do stuff > };
auto myfunc2 = [&myfunc1](Double_t vOpt, auto... args) { myfunc1(&args...);};
d.ForeachSlot( myfunc2 , { "var3", "var1", "var2" } );

This avoids specifying all the arguments at multiple steps when all I want myfunc2 to do is pass stuff through to myfunc1.

However, this fails when passed to the ForeachSlot because of the “auto…” that I use to specify the pack in the lambda, thanks to some type checks that ROOT does in TypeTraits.hxx on lines 30-44: ROOT: core/foundation/inc/ROOT/TypeTraits.hxx Source File
ROOT concludes that the lambda is not callable because auto… is not resolved.

You can see an example of a template lambda that should work fine, and the failure that is triggered by ROOT’s type checks here: (prepared by @Niklas_Nolte )

Help?


ROOT Version: 6.24.06
Platform: x86-645 (conda on arch)
Compiler: gcc 11.2.0


I think @eguiraud can help you.

1 Like

Hi @danj1011 ,
unfortunately template lambdas (and callables with overloaded call operators) are not supported in RDF. The reason is that RDF has to be able to deduce the types of the lambda arguments from its signature, at compile time, and that’s not possible if the signature is templated (or overloaded).

You can do this:

void myfunc1(double v1, double v2) { < do stuff > }
void myfunc2 (double vOpt, double v1, double v2) { myfunc1(v1, v2); <do other stuff> }

int main() {
  ...
  if (...) {
     df.ForeachSlot(myfunc1, {"v1", "v2"});
  } else {
     df.ForeachSlot(myfunc2, {"vOpt", "v1", "v2"});
  }
}

Cheers,
Enrico

Hi @eguiraud , thanks for replying so fast!

That’s a real shame, because “v1” and “v2” actually represent about 40 variables, that I now have to repeat an extra 2 or three times because of the RDF limitation :frowning:

The real picture is, horribly:

void myfunc1(double v1, double v2, double v3, double v4, double v5, double v6, double v7, double v8, double v9, double v10, double v11, double v12, double v13, double v14, double v15, double v16, double v17, double v18, double v19, double v20, double v21, double v22, double v23, double v24, double v25, double v26, double v27, double v28, double v29, double v30, double v31, double v32, double v33, double v34, double v35, double v36, double v37, double v38, double v39) { < do stuff > }
void myfunc2 (double vOpt, double v1, double v2, double v3, double v4, double v5, double v6, double v7, double v8, double v9, double v10, double v11, double v12, double v13, double v14, double v15, double v16, double v17, double v18, double v19, double v20, double v21, double v22, double v23, double v24, double v25, double v26, double v27, double v28, double v29, double v30, double v31, double v32, double v33, double v34, double v35, double v36, double v37, double v38, double v39) { myfunc1(v1, v2); <do other stuff> }

int main() {
  ...
  if (...) {
     df.ForeachSlot(myfunc1, {"v1", "v2","v3", "v4","v5", "v6","v7", "v8","v9","v10","v11", "v12","v13", "v14","v15", "v16","v17", "v18","v19","v20","v21", "v22","v23", "v24","v25", "v26","v27", "v28","v29","v30","v31", "v32","v33", "v34","v35", "v36","v37", "v38","v39"});
  } else {
     df.ForeachSlot(myfunc2, {"vOpt", "v1", "v2","v3", "v4","v5", "v6","v7", "v8","v9","v10","v11", "v12","v13", "v14","v15", "v16","v17", "v18","v19","v20","v21", "v22","v23", "v24","v25", "v26","v27", "v28","v29","v30","v31", "v32","v33", "v34","v35", "v36","v37", "v38","v39"});
  }
}

RDF has access to the underlying TTree - why can’t it figure out the column types from that? Though I accept it can’t do that at compile-time.

Either way… it makes sense that, if RDF relies on the lambda specification to deduce the column types, that it would struggle with overloading. But why not allow parameter packing? I don’t see how that introduces ambiguity because the pack has to be expanded somewhere, surely?

That’s just it, the type information from the TTree is a runtime thing, but RDF likes to have the column types at compile time whenever possible.

There is a mechanism to just use the runtime information, and it’s what’s triggered when you write something like df.Filter("v1 > 0") – the type of v1 is recovered from the tree at runtime and a function with the appropriate signature is just-in-time-compiled, at runtime, just before the event loop.
Unfortunately Foreach is one of the few RDF methods that don’t have a just-in-time-compiled version.

That auto…-lambda syntax is effectively defining a function template (or a functor with a variadic template callable operator, if you want to be precise). The pack is “expanded” (or better, the function template is instantiated with some concrete types) at the point of invocation of the function – but that’s too late for RDF, because in order to invoke the function correctly it needs to know its signature.

Now that I see the horrible 40-inputs versions of myfunc1 and myfunc2, however, I can suggest an alternative since all variables have the same type, using ROOT::RDF::PassAsVec:

#include <vector>
#include <ROOT/RDataFrame.hxx>
#include <ROOT/RDFHelpers.hxx>

void myfunc1(double v1, double v2) { ... }
void myfunc2(const std::vector<double> &args) { myfunc1(args[0], args[1]); ... }

int main() {
   ...
   if (...)
     df.ForeachSlot(ROOT::RDF::PassAsVec<5, double>(myfunc2), {"v1", "v2", "v3", "v4", "v5"});
   else
     df.ForeachSlot(myfunc1, {"v1", "v2"});
}

Does that improve things?

Thanks for the quick comments!

Actually I was a little afraid when I copied-and-pasted to make the long example. Some are Double_t, some are Int_t, some are Bool_t, and four are RVecF :frowning:

Ok, then I think you can either bite the bullet and write the function with the horribly long signature, or you can keep the version with the variadic template signature and leverage jitting via a Define + some action that triggers its evaluation at every entry, e.g. Sum:

gInterpreter->Declare("#include <header_with_variadic_helper_function.hxx>");
df.Define("dummy_col",
          "myfunc2(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10)")
  .Sum("dummy_col"); // this Sum forces evaluation of dummy_col at every entry

Thanks @eguiraud that was helpful. I rewrote in terms of RDataFrame::Define and helper functions. Ended up being a tiny bit slower than my original approach, but a factor of two for something that takes 20s is tolerable :wink:

The largest performance hit probably comes from using just-in-time-compiled (jitted) code (e.g. Define("x", "expression") instead of Define("x", [] { return expression; }, {"y", "z"})). In ROOT 6.24, jitted code runs at O0 optimization level. In 6.26 we moved it to O1, so you might be an improvement there, and soon it will get another speed-up. From 6.26 onwards you can also set the environment variable EXTRA_CLING_ARGS='-O3' to force a higher optimization level – we don’t generally recommend to tweak the defaults, but if it makes a large difference for your particular application you might consider it.

Also, out of curiosity, why do you need to handle ~40 columns at the same time? Why can’t that be split in several smaller Define/Filter/Histo1D calls?

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.