Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions roofit/batchcompute/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ if(NOT CMAKE_VERSION VERSION_LESS "3.23.0")
BASE_DIRS res/
FILES
res/RooBatchCompute.h
res/RooExprProgram.h
)
target_sources(
RooBatchCompute_GENERIC
Expand Down
27 changes: 27 additions & 0 deletions roofit/batchcompute/res/RooBatchCompute.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
#ifndef ROOFIT_BATCHCOMPUTE_ROOBATCHCOMPUTE_H
#define ROOFIT_BATCHCOMPUTE_ROOBATCHCOMPUTE_H

#include "RooExprProgram.h"

#include <ROOT/RSpan.hxx>

#include <DllImport.h> //for R__EXTERN, needed for windows
Expand Down Expand Up @@ -171,6 +173,24 @@ class RooBatchComputeInterface {
virtual ~RooBatchComputeInterface() = default;
virtual void compute(Config const &cfg, Computer, std::span<double> output, VarSpan, ArgSpan) = 0;

/// Evaluate a postfix expression program (a formula compiled by RooFit's
/// JIT-free formula backend, see RooExprProgram.h) over a batch of events.
/// Input spans of size 1 are broadcast; `stackDepth` is the program's
/// maximum expression stack depth and must not exceed
/// maxExprProgramStackDepth. The default implementation throws; the CPU
/// backends and, for programs marked cudaCapable, the CUDA backend
/// implement it.
///
/// The CUDA backend follows the same memory convention as compute(): the
/// output and every input span of more than one value are device memory,
/// while a span of one value (or an empty one, for a dependent the formula
/// does not use) is a host value that the backend stages to the device
/// itself. The two are told apart by the span size alone, which is
/// unambiguous because RooFit only schedules a node on the GPU when one of
/// its servers has more than one value, so `output.size()` is then > 1.
virtual void computeExprProgram(Config const &cfg, std::span<const ExprInstr> code, unsigned int stackDepth,
std::span<double> output, VarSpan vars);

virtual double reduceSum(Config const &cfg, InputArr input, size_t n) = 0;
virtual ReduceNLLOutput reduceNLL(Config const &cfg, std::span<const double> probas, std::span<const double> weights,
std::span<const double> offsetProbas) = 0;
Expand Down Expand Up @@ -224,6 +244,13 @@ inline void compute(Config cfg, Computer comp, std::span<double> output,
compute(cfg, comp, output, VarSpan{vars.begin(), vars.end()}, extraArgs);
}

inline void computeExprProgram(Config cfg, std::span<const ExprInstr> code, unsigned int stackDepth,
std::span<double> output, VarSpan vars)
{
auto dispatch = cfg.useCuda() ? dispatchCUDA : dispatchCPU;
dispatch->computeExprProgram(cfg, code, stackDepth, output, vars);
}

inline double reduceSum(Config cfg, InputArr input, size_t n)
{
auto dispatch = cfg.useCuda() ? dispatchCUDA : dispatchCPU;
Expand Down
151 changes: 151 additions & 0 deletions roofit/batchcompute/res/RooExprProgram.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* Project: RooFit
*
* Copyright (c) 2026, CERN
*
* Redistribution and use in source and binary forms,
* with or without modification, are permitted according to the terms
* listed in LICENSE (http://roofit.sourceforge.net/license.txt)
*/

#ifndef ROOFIT_BATCHCOMPUTE_ROOEXPRPROGRAM_H
#define ROOFIT_BATCHCOMPUTE_ROOEXPRPROGRAM_H

#include <cstddef>
#include <cstdint>

namespace RooBatchCompute {

/// Opcodes of the postfix expression programs compiled by RooFit's JIT-free
/// formula backend (see RooFormulaParser in RooFitCore). The same instruction
/// sequence drives both the scalar per-event evaluation in RooFitCore and the
/// chunked, vectorized batch evaluation in
/// RooBatchComputeInterface::computeExprProgram().
enum class ExprOp : std::uint8_t {
Const, ///< push konst
Var, ///< push vars[arg]
Add, ///< a + b
Sub, ///< a - b
Mul, ///< a * b

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

side note: https://www.partow.net/programming/exprtk/index.html
is being used by tree/dataframe and is maybe helpful for some of this stuff.

Div, ///< a / b
Neg, ///< -a
Not, ///< !a (exactly 0.0 or 1.0)
LT, ///< a < b (exactly 0.0 or 1.0, likewise below)
LE, ///< a <= b
GT, ///< a > b
GE, ///< a >= b
EQ, ///< a == b
NE, ///< a != b
And, ///< a && b (no short-circuit: both operands are always evaluated)
Or, ///< a || b (no short-circuit)
Select, ///< c ? a : b (both branches are always evaluated)
Pow, ///< std::pow(a, b), from the `^`/`**` operator or pow()
Sq, ///< a * a, from TFormula's `expr^2` -> TMath::Sq(expr) rewrite
IntNorm, ///< a + 0.0: maps -0.0 to +0.0 where cling would have used integer arithmetic
// Unary calls whose semantics are exactly the corresponding std/libm
// function, split out from Call1 so that batch backends can substitute a
// fast vectorizable implementation (VDT, hardware sqrt). fn1 carries the
// exact scalar function, which is what per-event evaluation calls.
Exp, ///< std::exp(a)
Log, ///< std::log(a)
Sin, ///< std::sin(a)
Cos, ///< std::cos(a)
Sqrt, ///< std::sqrt(a)
Call1, ///< fn1(a)
Call2, ///< fn2(a, b)
Call3, ///< fn3(a, b, c)
Call4 ///< fn4(a, b, c, d)
};

/// Device-representable identity of the function a call instruction calls.
///
/// The host interpreters call through the function pointer in the instruction,
/// which reproduces exactly what the cling-JIT-compiled code called for that
/// spelling. A GPU kernel cannot call a host function pointer, so every call
/// instruction additionally carries this identity, which the CUDA backend
/// switches on. There is one value per distinct host implementation, not per
/// accepted spelling: `sin`, `std::sin` and `TMath::Sin` all resolve to the
/// same libm call and share a value, while `TMath::Erf` (Cephes) is separate
/// from `erf` (libm) because the host implementations differ.
///
/// A call instruction whose function has no device implementation keeps
/// ExprFunc::None; RooFitCore refuses to schedule such a program on the GPU
/// (see RooExprEvaluator::Program::cudaCapable). The five spelling families
/// with their own opcode (Exp, Log, Sin, Cos, Sqrt) are identified by the
/// opcode and do not need a value here, but carry one anyway.
enum class ExprFunc : std::uint8_t {
None = 0, ///< no device implementation
// Unary. The values shared with a dedicated opcode come first.
Exp,
Log,
Sin,
Cos,
Sqrt,
Log10,
Tan,
ASin,
ACos,
ATan,
SinH,
CosH,
TanH,
ASinH,
ACosH,
ATanH,
Floor,
Ceil,
Erf,
Erfc,
TMathErf, ///< TMath::Erf, which is ROOT::Math::erf (Cephes) on the host
TMathErfc, ///< TMath::Erfc, likewise
TGamma,
LGamma,
Abs, ///< std::abs/std::fabs/TMath::Abs
CastInt, ///< the `int(x)` functional cast: truncation towards zero
Square, ///< `sq`/TMath::Sq
SignBit, ///< TMath::SignBit
Gaus1, ///< TMath::Gaus(x)
// Binary.
Pow,
ATan2, ///< std::atan2
TMathATan2, ///< TMath::ATan2, which special-cases x == 0
Fmod,
StdMin, ///< std::min: asymmetric in NaN, unlike TMath::Min
StdMax,
TMathMin,
TMathMax,
CopySign, ///< `sign`/TMath::Sign
Gaus2, ///< TMath::Gaus(x, mean)
// Ternary and quaternary.
Gaus3, ///< TMath::Gaus(x, mean, sigma)
Gaus4 ///< TMath::Gaus(x, mean, sigma, norm)
};

/// One instruction of a postfix expression program. Call instructions carry
/// the resolved function pointer, so evaluation involves no lookup table;
/// `arg` additionally keeps the index into RooFitCore's function allow-list
/// (RooFormulaFunctions) that the call was resolved from, which C++ emission
/// uses to reproduce the exact spelling, and `func` identifies the function
/// for backends that cannot use the host function pointer.
struct ExprInstr {
ExprOp op = ExprOp::Const;
ExprFunc func = ExprFunc::None; ///< calls: which function (for GPU dispatch)
std::uint32_t arg = 0; ///< Var: variable index; calls: function-table index
union {
double konst = 0.0; ///< Const
double (*fn1)(double); ///< Call1 and Exp...Sqrt
double (*fn2)(double, double); ///< Call2
double (*fn3)(double, double, double); ///< Call3
double (*fn4)(double, double, double, double); ///< Call4
};
};

/// Maximum expression stack depth accepted by computeExprProgram(), which
/// stack-allocates one bufferSize-sized chunk buffer per stack slot on the CPU
/// and one per-thread stack of this size on the GPU. Deeper programs must be
/// evaluated with the scalar per-event fallback.
constexpr std::uint32_t maxExprProgramStackDepth = 24;

} // End namespace RooBatchCompute

#endif
10 changes: 10 additions & 0 deletions roofit/batchcompute/src/Initialisation.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ bool &isInitialisedCpu()

namespace RooBatchCompute {

/// Default implementation for backends that do not support evaluating
/// expression programs (currently the CUDA backend). RooFit only routes batch
/// formula evaluation through backends that do.
void RooBatchComputeInterface::computeExprProgram(Config const &, std::span<const ExprInstr>, unsigned int,
std::span<double>, VarSpan)
{
throw std::runtime_error("computeExprProgram() is not implemented by the '" + architectureName() +
"' RooBatchCompute backend");
}

/// Inspect hardware capabilities, and load the optimal library for RooFit computations.
int initCPU()
{
Expand Down
Loading
Loading